From 9389170ce20760e5de04ae0efd87cdb1afe10491 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 22 Oct 2024 18:17:33 +0200 Subject: [PATCH 01/31] refactor: preparing codebase for gradual feature parity sync with utils-ts - Moving all v2 code into legacy_v2 folder inside src - Moving all v2 tests into legacy_v2_tests - Moving all files in beta into more granular folder structure --- .github/workflows/check-python.yaml | 19 +- legacy_v2_tests/__init__.py | 0 .../app_client_test.json | 0 {tests => legacy_v2_tests}/app_client_test.py | 0 .../app_multi_underscore_template_var.py | 0 {tests => legacy_v2_tests}/app_resolve.json | 0 {tests => legacy_v2_tests}/app_v1.json | 0 {tests => legacy_v2_tests}/app_v2.json | 0 {tests => legacy_v2_tests}/app_v3.json | 0 legacy_v2_tests/conftest.py | 211 +++ {tests => legacy_v2_tests}/test_account.py | 2 +- {tests => legacy_v2_tests}/test_app.py | 0 {tests => legacy_v2_tests}/test_app_client.py | 0 ...test_readonly_call_with_error.approved.txt | 0 ...ith_error_debug_mode_disabled.approved.txt | 0 ...rror_with_imported_source_map.approved.txt | 0 ...new_client_missing_source_map.approved.txt | 0 ...ew_client_provided_source_map.approved.txt | 0 ...ient_provided_template_values.approved.txt | 0 .../test_app_client_call.py | 4 +- .../test_app_client_clear_state.py | 2 +- ...test_abi_close_out_args_fails.approved.txt | 0 .../test_app_client_close_out.py | 2 +- ...st_create_auto_find_ambiguous.approved.txt | 0 .../test_app_client_create.py | 2 +- .../test_abi_delete_args_fails.approved.txt | 0 .../test_app_client_delete.py | 2 +- .../test_app_client_deploy.py | 2 +- .../test_abi_update_args_fails.approved.txt | 0 .../test_app_client_opt_in.py | 2 +- .../test_app_client_prepare.py | 0 .../test_app_client_resolve.py | 2 +- .../test_app_client_signer_sender.py | 0 .../test_app_client_template_values.py | 4 +- .../test_abi_update_args_fails.approved.txt | 0 .../test_app_client_update.py | 2 +- {tests => legacy_v2_tests}/test_asset.py | 2 +- .../test_build_teal_sourcemaps.approved.txt | 0 ...al_sourcemaps_without_sources.approved.txt | 0 .../test_debug_utils.py | 8 +- .../test_comment_stripping.approved.txt | 0 .../test_template_substitution.approved.txt | 0 {tests => legacy_v2_tests}/test_deploy.py | 4 +- ...e_equals_replace_app_succeeds.approved.txt | 0 ...cannot_determine_if_updatable.approved.txt | 0 ..._existing_immutable_app_fails.approved.txt | 0 ...reak_equals_replace_app_fails.approved.txt | 0 ...cannot_determine_if_deletable.approved.txt | 0 ..._existing_permanent_app_fails.approved.txt | 0 ...ils_and_doesnt_create_2nd_app.approved.txt | 0 ...isting_updatable_app_succeeds.approved.txt | 0 ...with_no_existing_app_succeeds.approved.txt | 0 ..._changing_parameters_succeeds.approved.txt | 0 ...il-Updatable.No-Deletable.No].approved.txt | 0 ...l-Updatable.No-Deletable.Yes].approved.txt | 0 ...l-Updatable.Yes-Deletable.No].approved.txt | 0 ...-Updatable.Yes-Deletable.Yes].approved.txt | 0 ...pp-Updatable.No-Deletable.No].approved.txt | 0 ...p-Updatable.No-Deletable.Yes].approved.txt | 0 ...p-Updatable.Yes-Deletable.No].approved.txt | 0 ...-Updatable.Yes-Deletable.Yes].approved.txt | 0 ...schema_breaking_change_append.approved.txt | 0 ...il-Updatable.No-Deletable.No].approved.txt | 0 ...l-Updatable.No-Deletable.Yes].approved.txt | 0 ...l-Updatable.Yes-Deletable.No].approved.txt | 0 ...-Updatable.Yes-Deletable.Yes].approved.txt | 0 ...pp-Updatable.No-Deletable.No].approved.txt | 0 ...p-Updatable.No-Deletable.Yes].approved.txt | 0 ...p-Updatable.Yes-Deletable.No].approved.txt | 0 ...-Updatable.Yes-Deletable.Yes].approved.txt | 0 ...pp-Updatable.No-Deletable.No].approved.txt | 0 ...p-Updatable.No-Deletable.Yes].approved.txt | 0 ...p-Updatable.Yes-Deletable.No].approved.txt | 0 ...-Updatable.Yes-Deletable.Yes].approved.txt | 0 ...est_deploy_with_update_append.approved.txt | 0 .../test_deploy_scenarios.py | 4 +- .../test_dispenser_api_client.py | 0 .../test_network_clients.py | 0 ...t_transfer_algo_max_fee_fails.approved.txt | 0 ..._transfer_asset_max_fee_fails.approved.txt | 0 {tests => legacy_v2_tests}/test_transfer.py | 4 +- poetry.lock | 24 +- pyproject.toml | 1 + src/algokit_utils/__init__.py | 45 +- src/algokit_utils/_debugging.py | 2 +- src/algokit_utils/_legacy_v2/__init__.py | 0 .../{ => _legacy_v2}/_ensure_funded.py | 10 +- .../{ => _legacy_v2}/_transfer.py | 2 +- src/algokit_utils/_legacy_v2/account.py | 183 +++ .../_legacy_v2/application_client.py | 1449 ++++++++++++++++ .../_legacy_v2/application_specification.py | 206 +++ src/algokit_utils/_legacy_v2/asset.py | 168 ++ src/algokit_utils/_legacy_v2/common.py | 28 + src/algokit_utils/_legacy_v2/deploy.py | 897 ++++++++++ src/algokit_utils/_legacy_v2/logic_error.py | 85 + src/algokit_utils/{ => _legacy_v2}/models.py | 4 +- .../_legacy_v2/network_clients.py | 130 ++ src/algokit_utils/account.py | 184 +-- src/algokit_utils/accounts/__init__.py | 0 .../{beta => accounts}/account_manager.py | 64 +- src/algokit_utils/accounts/models.py | 0 src/algokit_utils/application_client.py | 1450 +---------------- .../application_specification.py | 207 +-- src/algokit_utils/applications/__init__.py | 0 src/algokit_utils/applications/models.py | 0 src/algokit_utils/asset.py | 169 +- src/algokit_utils/assets/__init__.py | 0 src/algokit_utils/assets/models.py | 0 src/algokit_utils/clients/__init__.py | 0 .../{beta => clients}/algorand_client.py | 37 +- .../{beta => clients}/client_manager.py | 26 +- .../clients/dispenser_api_client.py | 178 ++ src/algokit_utils/clients/models.py | 0 src/algokit_utils/common.py | 29 +- src/algokit_utils/deploy.py | 898 +--------- src/algokit_utils/dispenser_api.py | 179 +- src/algokit_utils/errors/__init__.py | 0 src/algokit_utils/logic_error.py | 86 +- src/algokit_utils/models/__init__.py | 1 + src/algokit_utils/models/common.py | 0 src/algokit_utils/network_clients.py | 131 +- src/algokit_utils/transactions/__init__.py | 0 src/algokit_utils/transactions/models.py | 0 .../transaction_composer.py} | 110 +- tests/conftest.py | 2 +- tests/test_algorand_client.py | 4 +- 126 files changed, 3680 insertions(+), 3587 deletions(-) create mode 100644 legacy_v2_tests/__init__.py rename {tests => legacy_v2_tests}/app_client_test.json (100%) rename {tests => legacy_v2_tests}/app_client_test.py (100%) rename {tests => legacy_v2_tests}/app_multi_underscore_template_var.py (100%) rename {tests => legacy_v2_tests}/app_resolve.json (100%) rename {tests => legacy_v2_tests}/app_v1.json (100%) rename {tests => legacy_v2_tests}/app_v2.json (100%) rename {tests => legacy_v2_tests}/app_v3.json (100%) create mode 100644 legacy_v2_tests/conftest.py rename {tests => legacy_v2_tests}/test_account.py (88%) rename {tests => legacy_v2_tests}/test_app.py (100%) rename {tests => legacy_v2_tests}/test_app_client.py (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_call.py (98%) rename {tests => legacy_v2_tests}/test_app_client_clear_state.py (97%) rename {tests => legacy_v2_tests}/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_close_out.py (96%) rename {tests => legacy_v2_tests}/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_create.py (99%) rename {tests => legacy_v2_tests}/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_delete.py (95%) rename {tests => legacy_v2_tests}/test_app_client_deploy.py (96%) rename {tests => legacy_v2_tests}/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_opt_in.py (96%) rename {tests => legacy_v2_tests}/test_app_client_prepare.py (100%) rename {tests => legacy_v2_tests}/test_app_client_resolve.py (97%) rename {tests => legacy_v2_tests}/test_app_client_signer_sender.py (100%) rename {tests => legacy_v2_tests}/test_app_client_template_values.py (97%) rename {tests => legacy_v2_tests}/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_app_client_update.py (96%) rename {tests => legacy_v2_tests}/test_asset.py (98%) rename {tests => legacy_v2_tests}/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt (100%) rename {tests => legacy_v2_tests}/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt (100%) rename {tests => legacy_v2_tests}/test_debug_utils.py (95%) rename {tests => legacy_v2_tests}/test_deploy.approvals/test_comment_stripping.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy.approvals/test_template_substitution.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy.py (93%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt (100%) rename {tests => legacy_v2_tests}/test_deploy_scenarios.py (98%) rename {tests => legacy_v2_tests}/test_dispenser_api_client.py (100%) rename {tests => legacy_v2_tests}/test_network_clients.py (100%) rename {tests => legacy_v2_tests}/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt (100%) rename {tests => legacy_v2_tests}/test_transfer.py (98%) create mode 100644 src/algokit_utils/_legacy_v2/__init__.py rename src/algokit_utils/{ => _legacy_v2}/_ensure_funded.py (95%) rename src/algokit_utils/{ => _legacy_v2}/_transfer.py (99%) create mode 100644 src/algokit_utils/_legacy_v2/account.py create mode 100644 src/algokit_utils/_legacy_v2/application_client.py create mode 100644 src/algokit_utils/_legacy_v2/application_specification.py create mode 100644 src/algokit_utils/_legacy_v2/asset.py create mode 100644 src/algokit_utils/_legacy_v2/common.py create mode 100644 src/algokit_utils/_legacy_v2/deploy.py create mode 100644 src/algokit_utils/_legacy_v2/logic_error.py rename src/algokit_utils/{ => _legacy_v2}/models.py (97%) create mode 100644 src/algokit_utils/_legacy_v2/network_clients.py create mode 100644 src/algokit_utils/accounts/__init__.py rename src/algokit_utils/{beta => accounts}/account_manager.py (63%) create mode 100644 src/algokit_utils/accounts/models.py create mode 100644 src/algokit_utils/applications/__init__.py create mode 100644 src/algokit_utils/applications/models.py create mode 100644 src/algokit_utils/assets/__init__.py create mode 100644 src/algokit_utils/assets/models.py create mode 100644 src/algokit_utils/clients/__init__.py rename src/algokit_utils/{beta => clients}/algorand_client.py (96%) rename src/algokit_utils/{beta => clients}/client_manager.py (73%) create mode 100644 src/algokit_utils/clients/dispenser_api_client.py create mode 100644 src/algokit_utils/clients/models.py create mode 100644 src/algokit_utils/errors/__init__.py create mode 100644 src/algokit_utils/models/__init__.py create mode 100644 src/algokit_utils/models/common.py create mode 100644 src/algokit_utils/transactions/__init__.py create mode 100644 src/algokit_utils/transactions/models.py rename src/algokit_utils/{beta/composer.py => transactions/transaction_composer.py} (77%) diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index ea7a5d9f..e8464b76 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -45,12 +45,13 @@ jobs: - name: Check types with mypy run: poetry run mypy - - name: Check docs are up to date - run: | - poetry run poe docs - git diff --quiet --exit-code \ - ':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \ - ':!docs/html/apidocs/algokit_utils/algokit_utils.html' \ - ':!docs/html/searchindex.js' \ - ':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \ - docs/ + # TODO: uncomment after bulk of feature parity with ts is addressed + # - name: Check docs are up to date + # run: | + # poetry run poe docs + # git diff --quiet --exit-code \ + # ':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \ + # ':!docs/html/apidocs/algokit_utils/algokit_utils.html' \ + # ':!docs/html/searchindex.js' \ + # ':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \ + # docs/ diff --git a/legacy_v2_tests/__init__.py b/legacy_v2_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/app_client_test.json b/legacy_v2_tests/app_client_test.json similarity index 100% rename from tests/app_client_test.json rename to legacy_v2_tests/app_client_test.json diff --git a/tests/app_client_test.py b/legacy_v2_tests/app_client_test.py similarity index 100% rename from tests/app_client_test.py rename to legacy_v2_tests/app_client_test.py diff --git a/tests/app_multi_underscore_template_var.py b/legacy_v2_tests/app_multi_underscore_template_var.py similarity index 100% rename from tests/app_multi_underscore_template_var.py rename to legacy_v2_tests/app_multi_underscore_template_var.py diff --git a/tests/app_resolve.json b/legacy_v2_tests/app_resolve.json similarity index 100% rename from tests/app_resolve.json rename to legacy_v2_tests/app_resolve.json diff --git a/tests/app_v1.json b/legacy_v2_tests/app_v1.json similarity index 100% rename from tests/app_v1.json rename to legacy_v2_tests/app_v1.json diff --git a/tests/app_v2.json b/legacy_v2_tests/app_v2.json similarity index 100% rename from tests/app_v2.json rename to legacy_v2_tests/app_v2.json diff --git a/tests/app_v3.json b/legacy_v2_tests/app_v3.json similarity index 100% rename from tests/app_v3.json rename to legacy_v2_tests/app_v3.json diff --git a/legacy_v2_tests/conftest.py b/legacy_v2_tests/conftest.py new file mode 100644 index 00000000..e3997a2c --- /dev/null +++ b/legacy_v2_tests/conftest.py @@ -0,0 +1,211 @@ +import inspect +import math +import random +import subprocess +from pathlib import Path +from typing import TYPE_CHECKING +from uuid import uuid4 + +import algosdk.transaction +import pytest +from algokit_utils import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + Account, + ApplicationClient, + ApplicationSpecification, + EnsureBalanceParameters, + ensure_funded, + get_account, + get_algod_client, + get_indexer_client, + get_kmd_client_from_algod_client, + replace_template_variables, +) +from dotenv import load_dotenv + +from legacy_v2_tests import app_client_test + +if TYPE_CHECKING: + from algosdk.kmd import KMDClient + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + +@pytest.fixture(autouse=True, scope="session") +def _environment_fixture() -> None: + env_path = Path(__file__).parent / ".." / "example.env" + load_dotenv(env_path) + + +def check_output_stability(logs: str, *, test_name: str | None = None) -> None: + """Test that the contract output hasn't changed for an Application, using git diff""" + caller_frame = inspect.stack()[1] + caller_path = Path(caller_frame.filename).resolve() + caller_dir = caller_path.parent + test_name = test_name or caller_frame.function + caller_stem = Path(caller_frame.filename).stem + output_dir = caller_dir / f"{caller_stem}.approvals" + output_dir.mkdir(exist_ok=True) + output_file = output_dir / f"{test_name}.approved.txt" + output_file_str = str(output_file) + output_file_did_exist = output_file.exists() + output_file.write_text(logs, encoding="utf-8") + + git_diff = subprocess.run( + [ + "git", + "diff", + "--exit-code", + "--no-ext-diff", + "--no-color", + output_file_str, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + # first fail if there are any changes to already committed files, you must manually add them in that case + assert git_diff.returncode == 0, git_diff.stdout + + # if first time running, fail in case of accidental change to output directory + if not output_file_did_exist: + pytest.fail( + f"New output folder created at {output_file_str} from test {test_name} - " + "if this was intentional, please commit the files to the git repo" + ) + + +def read_spec( + file_name: str, + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> ApplicationSpecification: + path = Path(__file__).parent / file_name + spec = ApplicationSpecification.from_json(Path(path).read_text(encoding="utf-8")) + + template_variables = template_values or {} + if updatable is not None: + template_variables["UPDATABLE"] = int(updatable) + + if deletable is not None: + template_variables["DELETABLE"] = int(deletable) + + spec.approval_program = ( + replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) + return spec + + +def get_specs( + updatable: bool | None = None, + deletable: bool | None = None, +) -> tuple[ApplicationSpecification, ApplicationSpecification, ApplicationSpecification]: + return ( + read_spec("app_v1.json", updatable=updatable, deletable=deletable), + read_spec("app_v2.json", updatable=updatable, deletable=deletable), + read_spec("app_v3.json", updatable=updatable, deletable=deletable), + ) + + +def get_unique_name() -> str: + name = str(uuid4()).replace("-", "") + assert name.isalnum() + return name + + +def is_opted_in(client_fixture: ApplicationClient) -> bool: + _, sender = client_fixture.resolve_signer_sender() + account_info = client_fixture.algod_client.account_info(sender) + assert isinstance(account_info, dict) + apps_local_state = account_info["apps-local-state"] + return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) + + +@pytest.fixture(scope="session") +def algod_client() -> "AlgodClient": + return get_algod_client() + + +@pytest.fixture(scope="session") +def kmd_client(algod_client: "AlgodClient") -> "KMDClient": + return get_kmd_client_from_algod_client(algod_client) + + +@pytest.fixture(scope="session") +def indexer_client() -> "IndexerClient": + return get_indexer_client() + + +@pytest.fixture() +def creator(algod_client: "AlgodClient") -> Account: + creator_name = get_unique_name() + return get_account(algod_client, creator_name) + + +@pytest.fixture(scope="session") +def funded_account(algod_client: "AlgodClient") -> Account: + creator_name = get_unique_name() + return get_account(algod_client, creator_name) + + +@pytest.fixture(scope="session") +def app_spec() -> ApplicationSpecification: + app_spec = app_client_test.app.build() + path = Path(__file__).parent / "app_client_test.json" + path.write_text(app_spec.to_json()) + return read_spec("app_client_test.json", deletable=True, updatable=True, template_values={"VERSION": 1}) + + +def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int | None) -> int: + if total is None: + total = math.floor(random.random() * 100) + 20 + + decimals = 0 + asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}" + + params = algod_client.suggested_params() + + txn = algosdk.transaction.AssetConfigTxn( + sender=sender.address, + sp=params, + total=total * 10**decimals, + decimals=decimals, + default_frozen=False, + unit_name="", + asset_name=asset_name, + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + url="https://path/to/my/asset/details", + metadata_hash=None, + note=None, + lease=None, + rekey_to=None, + ) # type: ignore[no-untyped-call] + + signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call] + algod_client.send_transaction(signed_transaction) + ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call] + + if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): + return ptx["asset-index"] + else: + raise ValueError("Unexpected response from pending_transaction_info") + + +def assure_funds(algod_client: "AlgodClient", account: Account) -> None: + ensure_funded( + algod_client, + EnsureBalanceParameters( + account_to_fund=account, + min_spending_balance_micro_algos=300000, + min_funding_increment_micro_algos=1, + ), + ) diff --git a/tests/test_account.py b/legacy_v2_tests/test_account.py similarity index 88% rename from tests/test_account.py rename to legacy_v2_tests/test_account.py index 1536bd68..bb0ee272 100644 --- a/tests/test_account.py +++ b/legacy_v2_tests/test_account.py @@ -2,7 +2,7 @@ from algokit_utils import get_account -from tests.conftest import get_unique_name +from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app.py b/legacy_v2_tests/test_app.py similarity index 100% rename from tests/test_app.py rename to legacy_v2_tests/test_app.py diff --git a/tests/test_app_client.py b/legacy_v2_tests/test_app_client.py similarity index 100% rename from tests/test_app_client.py rename to legacy_v2_tests/test_app_client.py diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt diff --git a/tests/test_app_client_call.py b/legacy_v2_tests/test_app_client_call.py similarity index 98% rename from tests/test_app_client_call.py rename to legacy_v2_tests/test_app_client_call.py index 6d72f037..67acd4d5 100644 --- a/tests/test_app_client_call.py +++ b/legacy_v2_tests/test_app_client_call.py @@ -19,7 +19,7 @@ ) from algosdk.transaction import ApplicationCallTxn, PaymentTxn -from tests.conftest import check_output_stability, get_unique_name +from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.abi import Method @@ -40,7 +40,7 @@ def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecificati # If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. @pytest.fixture(autouse=True) def mock_config() -> Generator[Mock, None, None]: - with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + with patch("algokit_utils._legacy_v2.application_client.config", new_callable=Mock) as mock_config: mock_config.debug = True mock_config.project_root = None yield mock_config diff --git a/tests/test_app_client_clear_state.py b/legacy_v2_tests/test_app_client_clear_state.py similarity index 97% rename from tests/test_app_client_clear_state.py rename to legacy_v2_tests/test_app_client_clear_state.py index c8de6eba..f26a7094 100644 --- a/tests/test_app_client_clear_state.py +++ b/legacy_v2_tests/test_app_client_clear_state.py @@ -8,7 +8,7 @@ ApplicationSpecification, ) -from tests.conftest import is_opted_in +from legacy_v2_tests.conftest import is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt b/legacy_v2_tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt diff --git a/tests/test_app_client_close_out.py b/legacy_v2_tests/test_app_client_close_out.py similarity index 96% rename from tests/test_app_client_close_out.py rename to legacy_v2_tests/test_app_client_close_out.py index b5ba1cd3..5ee5e9c6 100644 --- a/tests/test_app_client_close_out.py +++ b/legacy_v2_tests/test_app_client_close_out.py @@ -8,7 +8,7 @@ LogicError, ) -from tests.conftest import check_output_stability, is_opted_in +from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt b/legacy_v2_tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt similarity index 100% rename from tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt rename to legacy_v2_tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt diff --git a/tests/test_app_client_create.py b/legacy_v2_tests/test_app_client_create.py similarity index 99% rename from tests/test_app_client_create.py rename to legacy_v2_tests/test_app_client_create.py index be29a5e4..1da7bbf7 100644 --- a/tests/test_app_client_create.py +++ b/legacy_v2_tests/test_app_client_create.py @@ -13,7 +13,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionSigner from algosdk.transaction import ApplicationCallTxn, GenericSignedTransaction, OnComplete, Transaction -from tests.conftest import check_output_stability, get_unique_name +from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt b/legacy_v2_tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt diff --git a/tests/test_app_client_delete.py b/legacy_v2_tests/test_app_client_delete.py similarity index 95% rename from tests/test_app_client_delete.py rename to legacy_v2_tests/test_app_client_delete.py index 6fc3ec5a..353bbfab 100644 --- a/tests/test_app_client_delete.py +++ b/legacy_v2_tests/test_app_client_delete.py @@ -8,7 +8,7 @@ LogicError, ) -from tests.conftest import check_output_stability +from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_deploy.py b/legacy_v2_tests/test_app_client_deploy.py similarity index 96% rename from tests/test_app_client_deploy.py rename to legacy_v2_tests/test_app_client_deploy.py index d1c8eba5..4eed49b6 100644 --- a/tests/test_app_client_deploy.py +++ b/legacy_v2_tests/test_app_client_deploy.py @@ -10,7 +10,7 @@ transfer, ) -from tests.conftest import get_unique_name, read_spec +from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt b/legacy_v2_tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt diff --git a/tests/test_app_client_opt_in.py b/legacy_v2_tests/test_app_client_opt_in.py similarity index 96% rename from tests/test_app_client_opt_in.py rename to legacy_v2_tests/test_app_client_opt_in.py index 9244a826..816e96f0 100644 --- a/tests/test_app_client_opt_in.py +++ b/legacy_v2_tests/test_app_client_opt_in.py @@ -8,7 +8,7 @@ LogicError, ) -from tests.conftest import check_output_stability, is_opted_in +from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_prepare.py b/legacy_v2_tests/test_app_client_prepare.py similarity index 100% rename from tests/test_app_client_prepare.py rename to legacy_v2_tests/test_app_client_prepare.py diff --git a/tests/test_app_client_resolve.py b/legacy_v2_tests/test_app_client_resolve.py similarity index 97% rename from tests/test_app_client_resolve.py rename to legacy_v2_tests/test_app_client_resolve.py index 2482149a..6c6023f3 100644 --- a/tests/test_app_client_resolve.py +++ b/legacy_v2_tests/test_app_client_resolve.py @@ -6,7 +6,7 @@ DefaultArgumentDict, ) -from tests.conftest import read_spec +from legacy_v2_tests.conftest import read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_signer_sender.py b/legacy_v2_tests/test_app_client_signer_sender.py similarity index 100% rename from tests/test_app_client_signer_sender.py rename to legacy_v2_tests/test_app_client_signer_sender.py diff --git a/tests/test_app_client_template_values.py b/legacy_v2_tests/test_app_client_template_values.py similarity index 97% rename from tests/test_app_client_template_values.py rename to legacy_v2_tests/test_app_client_template_values.py index 0bf5ab70..5b27f320 100644 --- a/tests/test_app_client_template_values.py +++ b/legacy_v2_tests/test_app_client_template_values.py @@ -3,7 +3,7 @@ import algokit_utils import pytest -from tests.conftest import get_unique_name, read_spec +from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -153,7 +153,7 @@ def test_deploy_with_multi_underscore_template_value( indexer_client: "IndexerClient", funded_account: algokit_utils.Account, ) -> None: - from tests.app_multi_underscore_template_var import app + from legacy_v2_tests.app_multi_underscore_template_var import app some_value = 123 app_spec = app.build(algod_client) diff --git a/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt b/legacy_v2_tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt diff --git a/tests/test_app_client_update.py b/legacy_v2_tests/test_app_client_update.py similarity index 96% rename from tests/test_app_client_update.py rename to legacy_v2_tests/test_app_client_update.py index 24dcf366..60cd10d9 100644 --- a/tests/test_app_client_update.py +++ b/legacy_v2_tests/test_app_client_update.py @@ -8,7 +8,7 @@ LogicError, ) -from tests.conftest import check_output_stability +from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_asset.py b/legacy_v2_tests/test_asset.py similarity index 98% rename from tests/test_asset.py rename to legacy_v2_tests/test_asset.py index d5612d26..3d75fa86 100644 --- a/tests/test_asset.py +++ b/legacy_v2_tests/test_asset.py @@ -16,7 +16,7 @@ from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient -from tests.conftest import assure_funds, generate_test_asset, get_unique_name +from legacy_v2_tests.conftest import assure_funds, generate_test_asset, get_unique_name @pytest.fixture() diff --git a/tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt similarity index 100% rename from tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt diff --git a/tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt similarity index 100% rename from tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt diff --git a/tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py similarity index 95% rename from tests/test_debug_utils.py rename to legacy_v2_tests/test_debug_utils.py index 459bd126..9b6d8ca8 100644 --- a/tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -9,8 +9,8 @@ persist_sourcemaps, simulate_and_persist_response, ) +from algokit_utils._legacy_v2.application_client import ApplicationClient from algokit_utils.account import get_account -from algokit_utils.application_client import ApplicationClient from algokit_utils.application_specification import ApplicationSpecification from algokit_utils.common import Program from algokit_utils.models import Account @@ -21,7 +21,7 @@ ) from algosdk.transaction import PaymentTxn -from tests.conftest import check_output_stability, get_unique_name +from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -123,7 +123,7 @@ def test_simulate_and_persist_response_via_app_call( client_fixture: ApplicationClient, mocker: Mock, ) -> None: - mock_config = mocker.patch("algokit_utils.application_client.config") + mock_config = mocker.patch("algokit_utils._legacy_v2.application_client.config") mock_config.debug = True mock_config.trace_all = True mock_config.trace_buffer_size_mb = 256 @@ -145,7 +145,7 @@ def test_simulate_and_persist_response_via_app_call( def test_simulate_and_persist_response( tmp_path_factory: pytest.TempPathFactory, client_fixture: ApplicationClient, mocker: Mock, funded_account: Account ) -> None: - mock_config = mocker.patch("algokit_utils.application_client.config") + mock_config = mocker.patch("algokit_utils._legacy_v2.application_client.config") mock_config.debug = True mock_config.trace_all = True cwd = tmp_path_factory.mktemp("cwd") diff --git a/tests/test_deploy.approvals/test_comment_stripping.approved.txt b/legacy_v2_tests/test_deploy.approvals/test_comment_stripping.approved.txt similarity index 100% rename from tests/test_deploy.approvals/test_comment_stripping.approved.txt rename to legacy_v2_tests/test_deploy.approvals/test_comment_stripping.approved.txt diff --git a/tests/test_deploy.approvals/test_template_substitution.approved.txt b/legacy_v2_tests/test_deploy.approvals/test_template_substitution.approved.txt similarity index 100% rename from tests/test_deploy.approvals/test_template_substitution.approved.txt rename to legacy_v2_tests/test_deploy.approvals/test_template_substitution.approved.txt diff --git a/tests/test_deploy.py b/legacy_v2_tests/test_deploy.py similarity index 93% rename from tests/test_deploy.py rename to legacy_v2_tests/test_deploy.py index 6a806f5d..51708f52 100644 --- a/tests/test_deploy.py +++ b/legacy_v2_tests/test_deploy.py @@ -1,9 +1,9 @@ from algokit_utils import ( replace_template_variables, ) -from algokit_utils.deploy import strip_comments +from algokit_utils._legacy_v2.deploy import strip_comments -from tests.conftest import check_output_stability +from legacy_v2_tests.conftest import check_output_stability def test_template_substitution() -> None: diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt diff --git a/tests/test_deploy_scenarios.py b/legacy_v2_tests/test_deploy_scenarios.py similarity index 98% rename from tests/test_deploy_scenarios.py rename to legacy_v2_tests/test_deploy_scenarios.py index d2740876..309fe4a3 100644 --- a/tests/test_deploy_scenarios.py +++ b/legacy_v2_tests/test_deploy_scenarios.py @@ -20,7 +20,7 @@ get_localnet_default_account, ) -from tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec +from legacy_v2_tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ # If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. @pytest.fixture(autouse=True) def mock_config(tmp_path_factory: pytest.TempPathFactory) -> Generator[Mock, None, None]: - with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + with patch("algokit_utils._legacy_v2.application_client.config", new_callable=Mock) as mock_config: mock_config.debug = True cwd = tmp_path_factory.mktemp("cwd") mock_config.project_root = cwd diff --git a/tests/test_dispenser_api_client.py b/legacy_v2_tests/test_dispenser_api_client.py similarity index 100% rename from tests/test_dispenser_api_client.py rename to legacy_v2_tests/test_dispenser_api_client.py diff --git a/tests/test_network_clients.py b/legacy_v2_tests/test_network_clients.py similarity index 100% rename from tests/test_network_clients.py rename to legacy_v2_tests/test_network_clients.py diff --git a/tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt b/legacy_v2_tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt similarity index 100% rename from tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt rename to legacy_v2_tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt diff --git a/tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt b/legacy_v2_tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt similarity index 100% rename from tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt rename to legacy_v2_tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt diff --git a/tests/test_transfer.py b/legacy_v2_tests/test_transfer.py similarity index 98% rename from tests/test_transfer.py rename to legacy_v2_tests/test_transfer.py index 7e13cdfb..8253a5eb 100644 --- a/tests/test_transfer.py +++ b/legacy_v2_tests/test_transfer.py @@ -24,8 +24,8 @@ from algosdk.util import algos_to_microalgos from pytest_httpx import HTTPXMock -from tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name -from tests.test_network_clients import DEFAULT_TOKEN +from legacy_v2_tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name +from legacy_v2_tests.test_network_clients import DEFAULT_TOKEN if TYPE_CHECKING: from algosdk.kmd import KMDClient diff --git a/poetry.lock b/poetry.lock index eb3dedd7..3544afa0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -2072,6 +2072,26 @@ files = [ {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] +[[package]] +name = "setuptools" +version = "75.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, + {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] + [[package]] name = "six" version = "1.16.0" @@ -2636,4 +2656,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "59127574db0011d8eb6e5a2d55be3048e9cb4a68e34c9c3e5f4a836d488b7318" +content-hash = "66e85df44cca4d3edccb50f730dfb4e9dccf93582e78fa0074dc9b47baa925e2" diff --git a/pyproject.toml b/pyproject.toml index f82ba2d8..4e3a99a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ pytest-httpx = "^0.21.3" pytest-xdist = "^3.4.0" sphinx-markdown-builder = "^0.6.6" linkify-it-py = "^2.0.3" +setuptools = "^75.2.0" [build-system] requires = ["poetry-core"] diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 02a5e341..77959758 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -1,7 +1,7 @@ from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response -from algokit_utils._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded -from algokit_utils._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset -from algokit_utils.account import ( +from algokit_utils._legacy_v2._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded +from algokit_utils._legacy_v2._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset +from algokit_utils._legacy_v2.account import ( create_kmd_wallet_account, get_account, get_account_from_mnemonic, @@ -10,14 +10,14 @@ get_localnet_default_account, get_or_create_kmd_wallet_account, ) -from algokit_utils.application_client import ( +from algokit_utils._legacy_v2.application_client import ( ApplicationClient, execute_atc_with_logic_error, get_next_version, get_sender_from_signer, num_extra_program_pages, ) -from algokit_utils.application_specification import ( +from algokit_utils._legacy_v2.application_specification import ( ApplicationSpecification, AppSpecStateDict, CallConfig, @@ -27,9 +27,9 @@ MethodHints, OnCompleteActionName, ) -from algokit_utils.asset import opt_in, opt_out -from algokit_utils.common import Program -from algokit_utils.deploy import ( +from algokit_utils._legacy_v2.asset import opt_in, opt_out +from algokit_utils._legacy_v2.common import Program +from algokit_utils._legacy_v2.deploy import ( DELETABLE_TEMPLATE_NAME, NOTE_PREFIX, UPDATABLE_TEMPLATE_NAME, @@ -56,32 +56,24 @@ get_creator_apps, replace_template_variables, ) -from algokit_utils.dispenser_api import ( - DISPENSER_ACCESS_TOKEN_KEY, - DISPENSER_REQUEST_TIMEOUT, - DispenserFundResponse, - DispenserLimitResponse, - TestNetDispenserApiClient, -) -from algokit_utils.logic_error import LogicError -from algokit_utils.models import ( +from algokit_utils._legacy_v2.logic_error import LogicError +from algokit_utils._legacy_v2.models import ( ABIArgsDict, ABIMethod, ABITransactionResponse, Account, - CommonCallParameters, # noqa: F401 - CommonCallParametersDict, # noqa: F401 + CommonCallParameters, + CommonCallParametersDict, CreateCallParameters, CreateCallParametersDict, CreateTransactionParameters, OnCompleteCallParameters, OnCompleteCallParametersDict, - RawTransactionParameters, # noqa: F401 TransactionParameters, TransactionParametersDict, TransactionResponse, ) -from algokit_utils.network_clients import ( +from algokit_utils._legacy_v2.network_clients import ( AlgoClientConfig, get_algod_client, get_algonode_config, @@ -92,8 +84,16 @@ is_mainnet, is_testnet, ) +from algokit_utils.clients.dispenser_api_client import ( + DISPENSER_ACCESS_TOKEN_KEY, + DISPENSER_REQUEST_TIMEOUT, + DispenserFundResponse, + DispenserLimitResponse, + TestNetDispenserApiClient, +) __all__ = [ + # ==== LEGACY V2 EXPORTS BEGIN ==== "create_kmd_wallet_account", "get_account_from_mnemonic", "get_or_create_kmd_wallet_account", @@ -120,6 +120,8 @@ "CreateCallParameters", "CreateCallParametersDict", "CreateTransactionParameters", + "CommonCallParameters", + "CommonCallParametersDict", "DeployCallArgs", "DeployCreateCallArgs", "DeployCallArgsDict", @@ -179,4 +181,5 @@ "persist_sourcemaps", "PersistSourceMapInput", "simulate_and_persist_response", + # ==== LEGACY V2 EXPORTS END ==== ] diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index 5563a08e..e8c0ef52 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -14,7 +14,7 @@ from algosdk.encoding import checksum from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig -from algokit_utils.common import Program +from algokit_utils._legacy_v2.common import Program if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/src/algokit_utils/_legacy_v2/__init__.py b/src/algokit_utils/_legacy_v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py similarity index 95% rename from src/algokit_utils/_ensure_funded.py rename to src/algokit_utils/_legacy_v2/_ensure_funded.py index b80734e4..23c87860 100644 --- a/src/algokit_utils/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -5,14 +5,14 @@ from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient -from algokit_utils._transfer import TransferParameters, transfer -from algokit_utils.account import get_dispenser_account -from algokit_utils.dispenser_api import ( +from algokit_utils._legacy_v2._transfer import TransferParameters, transfer +from algokit_utils._legacy_v2.account import get_dispenser_account +from algokit_utils._legacy_v2.models import Account +from algokit_utils._legacy_v2.network_clients import is_testnet +from algokit_utils.clients.dispenser_api_client import ( DispenserAssetName, TestNetDispenserApiClient, ) -from algokit_utils.models import Account -from algokit_utils.network_clients import is_testnet @dataclass(kw_only=True) diff --git a/src/algokit_utils/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py similarity index 99% rename from src/algokit_utils/_transfer.py rename to src/algokit_utils/_legacy_v2/_transfer.py index 0103b172..baca5b2b 100644 --- a/src/algokit_utils/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -7,7 +7,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams -from algokit_utils.models import Account +from algokit_utils._legacy_v2.models import Account if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py new file mode 100644 index 00000000..819a448f --- /dev/null +++ b/src/algokit_utils/_legacy_v2/account.py @@ -0,0 +1,183 @@ +import logging +import os +from typing import TYPE_CHECKING, Any + +from algosdk.account import address_from_private_key +from algosdk.mnemonic import from_private_key, to_private_key +from algosdk.util import algos_to_microalgos + +from algokit_utils._legacy_v2._transfer import TransferParameters, transfer +from algokit_utils._legacy_v2.models import Account +from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.kmd import KMDClient + from algosdk.v2client.algod import AlgodClient + +__all__ = [ + "create_kmd_wallet_account", + "get_account", + "get_account_from_mnemonic", + "get_dispenser_account", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_or_create_kmd_wallet_account", +] + +logger = logging.getLogger(__name__) +_DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 + + +def get_account_from_mnemonic(mnemonic: str) -> Account: + """Convert a mnemonic (25 word passphrase) into an Account""" + private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call] + address = address_from_private_key(private_key) # type: ignore[no-untyped-call] + return Account(private_key=private_key, address=address) + + +def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: + """Creates a wallet with specified name""" + wallet_id = kmd_client.create_wallet(name, "")["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + kmd_client.generate_key(wallet_handle) + + key_ids: list[str] = kmd_client.list_keys(wallet_handle) + account_key = key_ids[0] + + private_account_key = kmd_client.export_key(wallet_handle, "", account_key) + return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] + + +def get_or_create_kmd_wallet_account( + client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None +) -> Account: + """Returns a wallet with specified name, or creates one if not found""" + kmd_client = kmd_client or get_kmd_client_from_algod_client(client) + account = get_kmd_wallet_account(client, kmd_client, name) + + if account: + account_info = client.account_info(account.address) + assert isinstance(account_info, dict) + if account_info["amount"] > 0: + return account + logger.debug(f"Found existing account in LocalNet with name '{name}', but no funds in the account.") + else: + account = create_kmd_wallet_account(kmd_client, name) + + logger.debug( + f"Couldn't find existing account in LocalNet with name '{name}'. " + f"So created account {account.address} with keys stored in KMD." + ) + + logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs") + + if fund_with_algos: + transfer( + client, + TransferParameters( + from_account=get_dispenser_account(client), + to_address=account.address, + micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call] + ), + ) + + return account + + +def _is_default_account(account: dict[str, Any]) -> bool: + return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) + + +def get_localnet_default_account(client: "AlgodClient") -> Account: + """Returns the default Account in a LocalNet instance""" + if not is_localnet(client): + raise Exception("Can't get a default account from non LocalNet network") + + account = get_kmd_wallet_account( + client, get_kmd_client_from_algod_client(client), "unencrypted-default-wallet", _is_default_account + ) + assert account + return account + + +def get_dispenser_account(client: "AlgodClient") -> Account: + """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" + if is_localnet(client): + return get_localnet_default_account(client) + return get_account(client, "DISPENSER") + + +def get_kmd_wallet_account( + client: "AlgodClient", + kmd_client: "KMDClient", + name: str, + predicate: "Callable[[dict[str, Any]], bool] | None" = None, +) -> Account | None: + """Returns wallet matching specified name and predicate or None if not found""" + wallets: list[dict] = kmd_client.list_wallets() + + wallet = next((w for w in wallets if w["name"] == name), None) + if wallet is None: + return None + + wallet_id = wallet["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + key_ids: list[str] = kmd_client.list_keys(wallet_handle) + matched_account_key = None + if predicate: + for key in key_ids: + account = client.account_info(key) + assert isinstance(account, dict) + if predicate(account): + matched_account_key = key + else: + matched_account_key = next(key_ids.__iter__(), None) + + if not matched_account_key: + return None + + private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key) + return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] + + +def get_account( + client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None +) -> Account: + """Returns an Algorand account with private key loaded by convention based on the given name identifier. + + # Convention + + **Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret + Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a + secret storage service rather than the file system. + + **LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will + create it and fund the account for you + + This allows you to write code that will work seamlessly in production and local development (LocalNet) without + manual config locally (including when you reset the LocalNet). + + # Example + If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get + that private key loaded into an account object: + ```python + account = get_account('ACCOUNT', algod) + ``` + + If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account + that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + """ + + mnemonic_key = f"{name.upper()}_MNEMONIC" + mnemonic = os.getenv(mnemonic_key) + if mnemonic: + return get_account_from_mnemonic(mnemonic) + + if is_localnet(client): + account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) + os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call] + return account + + raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'") diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py new file mode 100644 index 00000000..32851fa4 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -0,0 +1,1449 @@ +import base64 +import copy +import json +import logging +import re +import typing +from math import ceil +from pathlib import Path +from typing import Any, Literal, cast, overload + +import algosdk +from algosdk import transaction +from algosdk.abi import ABIType, Method, Returns +from algosdk.account import address_from_private_key +from algosdk.atomic_transaction_composer import ( + ABI_RETURN_HASH, + ABIResult, + AccountTransactionSigner, + AtomicTransactionComposer, + AtomicTransactionResponse, + LogicSigTransactionSigner, + MultisigTransactionSigner, + SimulateAtomicTransactionResponse, + TransactionSigner, + TransactionWithSigner, +) +from algosdk.constants import APP_PAGE_MAX_SIZE +from algosdk.logic import get_application_address +from algosdk.source_map import SourceMap + +import algokit_utils._legacy_v2.application_specification as au_spec +import algokit_utils._legacy_v2.deploy as au_deploy +from algokit_utils._debugging import ( + PersistSourceMapInput, + persist_sourcemaps, + simulate_and_persist_response, + simulate_response, +) +from algokit_utils._legacy_v2.common import Program +from algokit_utils._legacy_v2.logic_error import LogicError, parse_logic_error +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIArgType, + ABIMethod, + ABITransactionResponse, + Account, + CreateCallParameters, + CreateCallParametersDict, + OnCompleteCallParameters, + OnCompleteCallParametersDict, + SimulationTrace, + TransactionParameters, + TransactionParametersDict, + TransactionResponse, +) +from algokit_utils.config import config + +if typing.TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + +logger = logging.getLogger(__name__) + + +"""A dictionary `dict[str, Any]` representing ABI argument names and values""" + +__all__ = [ + "ApplicationClient", + "execute_atc_with_logic_error", + "get_next_version", + "get_sender_from_signer", + "num_extra_program_pages", +] + +"""Alias for {py:class}`pyteal.ABIReturnSubroutine`, {py:class}`algosdk.abi.method.Method` or a {py:class}`str` +representing an ABI method name or signature""" + + +def num_extra_program_pages(approval: bytes, clear: bytes) -> int: + """Calculate minimum number of extra_pages required for provided approval and clear programs""" + + return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) + + +class ApplicationClient: + """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" + + @overload + def __init__( + self, + algod_client: "AlgodClient", + app_spec: au_spec.ApplicationSpecification | Path, + *, + app_id: int = 0, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + ): ... + + @overload + def __init__( + self, + algod_client: "AlgodClient", + app_spec: au_spec.ApplicationSpecification | Path, + *, + creator: str | Account, + indexer_client: "IndexerClient | None" = None, + existing_deployments: au_deploy.AppLookup | None = None, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + app_name: str | None = None, + ): ... + + def __init__( # noqa: PLR0913 + self, + algod_client: "AlgodClient", + app_spec: au_spec.ApplicationSpecification | Path, + *, + app_id: int = 0, + creator: str | Account | None = None, + indexer_client: "IndexerClient | None" = None, + existing_deployments: au_deploy.AppLookup | None = None, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + app_name: str | None = None, + ): + """ApplicationClient can be created with an app_id to interact with an existing application, alternatively + it can be created with a creator and indexer_client specified to find existing applications by name and creator. + + :param AlgodClient algod_client: AlgoSDK algod client + :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one + :param int app_id: The app_id of an existing application, to instead find the application by creator and name + use the creator and indexer_client parameters + :param str | Account creator: The address or Account of the app creator to resolve the app_id + :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by + creator and app name + :param AppLookup existing_deployments: + :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and + creator was passed as an Account will use that. + :param str sender: Address to use as the sender for all transactions, will use the address associated with the + signer if not specified. + :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should + *NOT* include the TMPL_ prefix + :param str | None app_name: Name of application to use when deploying, defaults to name defined on the + Application Specification + """ + self.algod_client = algod_client + self.app_spec = ( + au_spec.ApplicationSpecification.from_json(app_spec.read_text()) if isinstance(app_spec, Path) else app_spec + ) + self._app_name = app_name + self._approval_program: Program | None = None + self._approval_source_map: SourceMap | None = None + self._clear_program: Program | None = None + + self.template_values: au_deploy.TemplateValueMapping = template_values or {} + self.existing_deployments = existing_deployments + self._indexer_client = indexer_client + if creator is not None: + if not self.existing_deployments and not self._indexer_client: + raise Exception( + "If using the creator parameter either existing_deployments or indexer_client must also be provided" + ) + self._creator: str | None = creator.address if isinstance(creator, Account) else creator + if self.existing_deployments and self.existing_deployments.creator != self._creator: + raise Exception( + "Attempt to create application client with invalid existing_deployments against" + f"a different creator ({self.existing_deployments.creator} instead of " + f"expected creator {self._creator}" + ) + self.app_id = 0 + else: + self.app_id = app_id + self._creator = None + + self.signer: TransactionSigner | None + if signer: + self.signer = ( + signer if isinstance(signer, TransactionSigner) else AccountTransactionSigner(signer.private_key) + ) + elif isinstance(creator, Account): + self.signer = AccountTransactionSigner(creator.private_key) + else: + self.signer = None + + self.sender = sender + self.suggested_params = suggested_params + + @property + def app_name(self) -> str: + return self._app_name or self.app_spec.contract.name + + @app_name.setter + def app_name(self, value: str) -> None: + self._app_name = value + + @property + def app_address(self) -> str: + return get_application_address(self.app_id) + + @property + def approval(self) -> Program | None: + return self._approval_program + + @property + def approval_source_map(self) -> SourceMap | None: + if self._approval_source_map: + return self._approval_source_map + if self._approval_program: + return self._approval_program.source_map + return None + + @approval_source_map.setter + def approval_source_map(self, value: SourceMap) -> None: + self._approval_source_map = value + + @property + def clear(self) -> Program | None: + return self._clear_program + + def prepare( + self, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> "ApplicationClient": + """Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. + Will also substitute provided template_values into the associated app_spec in the copy""" + new_client: ApplicationClient = copy.copy(self) + new_client._prepare( # noqa: SLF001 + new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values + ) + return new_client + + def _prepare( # noqa: PLR0913 + self, + target: "ApplicationClient", + *, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> None: + target.app_id = self.app_id if app_id is None else app_id + target.signer, target.sender = target.get_signer_sender( + AccountTransactionSigner(signer.private_key) if isinstance(signer, Account) else signer, sender + ) + target.template_values = {**self.template_values, **(template_values or {})} + + def deploy( # noqa: PLR0913 + self, + version: str | None = None, + *, + signer: TransactionSigner | None = None, + sender: str | None = None, + allow_update: bool | None = None, + allow_delete: bool | None = None, + on_update: au_deploy.OnUpdate = au_deploy.OnUpdate.Fail, + on_schema_break: au_deploy.OnSchemaBreak = au_deploy.OnSchemaBreak.Fail, + template_values: au_deploy.TemplateValueMapping | None = None, + create_args: au_deploy.ABICreateCallArgs + | au_deploy.ABICreateCallArgsDict + | au_deploy.DeployCreateCallArgs + | None = None, + update_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, + delete_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, + ) -> au_deploy.DeployResponse: + """Deploy an application and update client to reference it. + + Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator + account, including deploy-time template placeholder substitutions. + To understand the architecture decisions behind this functionality please see + + + ```{note} + If there is a breaking state schema change to an existing app (and `on_schema_break` is set to + 'ReplaceApp' the existing app will be deleted and re-created. + ``` + + ```{note} + If there is an update (different TEAL code) to an existing app (and `on_update` is set to 'ReplaceApp') + the existing app will be deleted and re-created. + ``` + + :param str version: version to use when creating or updating app, if None version will be auto incremented + :param algosdk.atomic_transaction_composer.TransactionSigner signer: signer to use when deploying app + , if None uses self.signer + :param str sender: sender address to use when deploying app, if None uses self.sender + :param bool allow_delete: Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app + can be deleted + :param bool allow_update: Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app + can be updated + :param OnUpdate on_update: Determines what action to take if an application update is required + :param OnSchemaBreak on_schema_break: Determines what action to take if an application schema requirements + has increased beyond the current allocation + :param dict[str, int|str|bytes] template_values: Values to use for `TMPL_*` template variables, dictionary keys + should *NOT* include the TMPL_ prefix + :param ABICreateCallArgs create_args: Arguments used when creating an application + :param ABICallArgs | ABICallArgsDict update_args: Arguments used when updating an application + :param ABICallArgs | ABICallArgsDict delete_args: Arguments used when deleting an application + :return DeployResponse: details action taken and relevant transactions + :raises DeploymentError: If the deployment failed + """ + # check inputs + if self.app_id: + raise au_deploy.DeploymentFailedError( + f"Attempt to deploy app which already has an app index of {self.app_id}" + ) + try: + resolved_signer, resolved_sender = self.resolve_signer_sender(signer, sender) + except ValueError as ex: + raise au_deploy.DeploymentFailedError(f"{ex}, unable to deploy app") from None + if not self._creator: + raise au_deploy.DeploymentFailedError("No creator provided, unable to deploy app") + if self._creator != resolved_sender: + raise au_deploy.DeploymentFailedError( + f"Attempt to deploy contract with a sender address {resolved_sender} that differs " + f"from the given creator address for this application client: {self._creator}" + ) + + # make a copy and prepare variables + template_values = {**self.template_values, **(template_values or {})} + au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) + + existing_app_metadata_or_reference = self._load_app_reference() + + self._approval_program, self._clear_program = substitute_template_and_compile( + self.algod_client, self.app_spec, template_values + ) + + if config.debug and config.project_root: + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" + ), + PersistSourceMapInput( + compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" + ), + ], + project_root=config.project_root, + client=self.algod_client, + with_sources=True, + ) + + deployer = au_deploy.Deployer( + app_client=self, + creator=self._creator, + signer=resolved_signer, + sender=resolved_sender, + new_app_metadata=self._get_app_deploy_metadata(version, allow_update, allow_delete), + existing_app_metadata_or_reference=existing_app_metadata_or_reference, + on_update=on_update, + on_schema_break=on_schema_break, + create_args=create_args, + update_args=update_args, + delete_args=delete_args, + ) + + return deployer.deploy() + + def compose_create( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" + approval_program, clear_program = self._check_is_compiled() + transaction_parameters = _convert_transaction_parameters(transaction_parameters) + + extra_pages = transaction_parameters.extra_pages or num_extra_program_pages( + approval_program.raw_binary, clear_program.raw_binary + ) + + self.add_method_call( + atc, + app_id=0, + abi_method=call_abi_method, + abi_args=abi_kwargs, + on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC, + call_config=au_spec.CallConfig.CREATE, + parameters=transaction_parameters, + approval_program=approval_program.raw_binary, + clear_program=clear_program.raw_binary, + global_schema=self.app_spec.global_state_schema, + local_schema=self.app_spec.local_state_schema, + extra_pages=extra_pages, + ) + + @overload + def create( + self, + call_abi_method: Literal[False], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def create( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def create( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def create( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" + + atc = AtomicTransactionComposer() + + self.compose_create( + atc, + call_abi_method, + transaction_parameters, + **abi_kwargs, + ) + create_result = self._execute_atc_tr(atc) + self.app_id = au_deploy.get_app_id_from_tx_id(self.algod_client, create_result.tx_id) + return create_result + + def compose_update( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=UpdateApplication to atc""" + approval_program, clear_program = self._check_is_compiled() + + self.add_method_call( + atc=atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.UpdateApplicationOC, + approval_program=approval_program.raw_binary, + clear_program=clear_program.raw_binary, + ) + + @overload + def update( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def update( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def update( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def update( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=UpdateApplication""" + + atc = AtomicTransactionComposer() + self.compose_update( + atc, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_delete( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=DeleteApplication to atc""" + + self.add_method_call( + atc, + call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.DeleteApplicationOC, + ) + + @overload + def delete( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def delete( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def delete( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def delete( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=DeleteApplication""" + + atc = AtomicTransactionComposer() + self.compose_delete( + atc, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_call( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with specified parameters to atc""" + _parameters = _convert_transaction_parameters(transaction_parameters) + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=_parameters, + on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC, + ) + + @overload + def call( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def call( + self, + call_abi_method: Literal[False], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def call( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def call( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with specified parameters""" + atc = AtomicTransactionComposer() + _parameters = _convert_transaction_parameters(transaction_parameters) + self.compose_call( + atc, + call_abi_method=call_abi_method, + transaction_parameters=_parameters, + **abi_kwargs, + ) + + method = self._resolve_method( + call_abi_method, abi_kwargs, _parameters.on_complete or transaction.OnComplete.NoOpOC + ) + if method: + hints = self._method_hints(method) + if hints and hints.read_only: + if config.debug and config.project_root and config.trace_all: + simulate_and_persist_response( + atc, config.project_root, self.algod_client, config.trace_buffer_size_mb + ) + + return self._simulate_readonly_call(method, atc) + + return self._execute_atc_tr(atc) + + def compose_opt_in( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=OptIn to atc""" + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.OptInOC, + ) + + @overload + def opt_in( + self, + call_abi_method: ABIMethod | Literal[True] = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def opt_in( + self, + call_abi_method: Literal[False] = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + ) -> TransactionResponse: ... + + @overload + def opt_in( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def opt_in( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=OptIn""" + atc = AtomicTransactionComposer() + self.compose_opt_in( + atc, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_close_out( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=CloseOut to ac""" + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.CloseOutOC, + ) + + @overload + def close_out( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def close_out( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def close_out( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def close_out( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=CloseOut""" + atc = AtomicTransactionComposer() + self.compose_close_out( + atc, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_clear_state( + self, + atc: AtomicTransactionComposer, + /, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + app_args: list[bytes] | None = None, + ) -> None: + """Adds a signed transaction with on_complete=ClearState to atc""" + return self.add_method_call( + atc, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.ClearStateOC, + app_args=app_args, + ) + + def clear_state( + self, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + app_args: list[bytes] | None = None, + ) -> TransactionResponse: + """Submits a signed transaction with on_complete=ClearState""" + atc = AtomicTransactionComposer() + self.compose_clear_state( + atc, + transaction_parameters=transaction_parameters, + app_args=app_args, + ) + return self._execute_atc_tr(atc) + + def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: + """Gets the global state info associated with app_id""" + global_state = self.algod_client.application_info(self.app_id) + assert isinstance(global_state, dict) + return cast( + dict[bytes | str, bytes | str | int], + _decode_state(global_state.get("params", {}).get("global-state", {}), raw=raw), + ) + + def get_local_state(self, account: str | None = None, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: + """Gets the local state info for associated app_id and account/sender""" + + if account is None: + _, account = self.resolve_signer_sender(self.signer, self.sender) + + acct_state = self.algod_client.account_application_info(account, self.app_id) + assert isinstance(acct_state, dict) + return cast( + dict[bytes | str, bytes | str | int], + _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), + ) + + def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes: + """Resolves the default value for an ABI method, based on app_spec""" + + def _data_check(value: object) -> int | str | bytes: + if isinstance(value, int | str | bytes): + return value + raise ValueError(f"Unexpected type for constant data: {value}") + + match to_resolve: + case {"source": "constant", "data": data}: + return _data_check(data) + case {"source": "global-state", "data": str() as key}: + global_state = self.get_global_state(raw=True) + return global_state[key.encode()] + case {"source": "local-state", "data": str() as key}: + _, sender = self.resolve_signer_sender(self.signer, self.sender) + acct_state = self.get_local_state(sender, raw=True) + return acct_state[key.encode()] + case {"source": "abi-method", "data": dict() as method_dict}: + method = Method.undictify(method_dict) + response = self.call(method) + assert isinstance(response, ABITransactionResponse) + return _data_check(response.return_value) + + case {"source": source}: + raise ValueError(f"Unrecognized default argument source: {source}") + case _: + raise TypeError("Unable to interpret default argument specification") + + def _get_app_deploy_metadata( + self, version: str | None, allow_update: bool | None, allow_delete: bool | None + ) -> au_deploy.AppDeployMetaData: + updatable = ( + allow_update + if allow_update is not None + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC + ) + ) + deletable = ( + allow_delete + if allow_delete is not None + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC + ) + ) + + app = self._load_app_reference() + + if version is None: + if app.app_id == 0: + version = "v1.0" + else: + assert isinstance(app, au_deploy.AppDeployMetaData) + version = get_next_version(app.version) + return au_deploy.AppDeployMetaData(self.app_name, version, updatable=updatable, deletable=deletable) + + def _check_is_compiled(self) -> tuple[Program, Program]: + if self._approval_program is None or self._clear_program is None: + self._approval_program, self._clear_program = substitute_template_and_compile( + self.algod_client, self.app_spec, self.template_values + ) + + if config.debug and config.project_root: + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" + ), + PersistSourceMapInput( + compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" + ), + ], + project_root=config.project_root, + client=self.algod_client, + with_sources=True, + ) + + return self._approval_program, self._clear_program + + def _simulate_readonly_call( + self, method: Method, atc: AtomicTransactionComposer + ) -> ABITransactionResponse | TransactionResponse: + response = simulate_response(atc, self.algod_client) + traces = None + if config.debug: + traces = _create_simulate_traces(response) + if response.failure_message: + raise _try_convert_to_logic_error( + response.failure_message, + self.app_spec.approval_program, + self._get_approval_source_map, + traces, + ) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}") + + return TransactionResponse.from_atr(response) + + def _load_reference_and_check_app_id(self) -> None: + self._load_app_reference() + self._check_app_id() + + def _load_app_reference(self) -> au_deploy.AppReference | au_deploy.AppMetaData: + if not self.existing_deployments and self._creator: + assert self._indexer_client + self.existing_deployments = au_deploy.get_creator_apps(self._indexer_client, self._creator) + + if self.existing_deployments: + app = self.existing_deployments.apps.get(self.app_name) + if app: + if self.app_id == 0: + self.app_id = app.app_id + return app + + return au_deploy.AppReference(self.app_id, self.app_address) + + def _check_app_id(self) -> None: + if self.app_id == 0: + raise Exception( + "ApplicationClient is not associated with an app instance, to resolve either:\n" + "1.) provide an app_id on construction OR\n" + "2.) provide a creator address so an app can be searched for OR\n" + "3.) create an app first using create or deploy methods" + ) + + def _resolve_method( + self, + abi_method: ABIMethod | bool | None, + args: ABIArgsDict | None, + on_complete: transaction.OnComplete, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, + ) -> Method | None: + matches: list[Method | None] = [] + match abi_method: + case str() | Method(): # abi method specified + return self._resolve_abi_method(abi_method) + case bool() | None: # find abi method + has_bare_config = ( + call_config in au_deploy.get_call_config(self.app_spec.bare_call_config, on_complete) + or on_complete == transaction.OnComplete.ClearStateOC + ) + abi_methods = self._find_abi_methods(args, on_complete, call_config) + if abi_method is not False: + matches += abi_methods + if has_bare_config and abi_method is not True: + matches += [None] + case _: + return abi_method.method_spec() + + if len(matches) == 1: # exact match + return matches[0] + elif len(matches) > 1: # ambiguous match + signatures = ", ".join((m.get_signature() if isinstance(m, Method) else "bare") for m in matches) + raise Exception( + f"Could not find an exact method to use for {on_complete.name} with call_config of {call_config.name}, " + f"specify the exact method using abi_method and args parameters, considered: {signatures}" + ) + else: # no match + raise Exception( + f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" + ) + + def _get_approval_source_map(self) -> SourceMap | None: + if self.approval_source_map: + return self.approval_source_map + + try: + approval, _ = self._check_is_compiled() + except au_deploy.DeploymentFailedError: + return None + return approval.source_map + + def export_source_map(self) -> str | None: + """Export approval source map to JSON, can be later re-imported with `import_source_map`""" + source_map = self._get_approval_source_map() + if source_map: + return json.dumps( + { + "version": source_map.version, + "sources": source_map.sources, + "mappings": source_map.mappings, + } + ) + return None + + def import_source_map(self, source_map_json: str) -> None: + """Import approval source from JSON exported by `export_source_map`""" + source_map = json.loads(source_map_json) + self._approval_source_map = SourceMap(source_map) + + def add_method_call( # noqa: PLR0913 + self, + atc: AtomicTransactionComposer, + abi_method: ABIMethod | bool | None = None, + *, + abi_args: ABIArgsDict | None = None, + app_id: int | None = None, + parameters: TransactionParameters | TransactionParametersDict | None = None, + on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, + local_schema: transaction.StateSchema | None = None, + global_schema: transaction.StateSchema | None = None, + approval_program: bytes | None = None, + clear_program: bytes | None = None, + extra_pages: int | None = None, + app_args: list[bytes] | None = None, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, + ) -> None: + """Adds a transaction to the AtomicTransactionComposer passed""" + if app_id is None: + self._load_reference_and_check_app_id() + app_id = self.app_id + parameters = _convert_transaction_parameters(parameters) + method = self._resolve_method(abi_method, abi_args, on_complete, call_config) + sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() + signer, sender = self.resolve_signer_sender(parameters.signer, parameters.sender) + if parameters.boxes is not None: + # TODO: algosdk actually does this, but it's type hints say otherwise... + encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in parameters.boxes] + else: + encoded_boxes = None + + encoded_lease = parameters.lease.encode("utf-8") if isinstance(parameters.lease, str) else parameters.lease + + if not method: # not an abi method, treat as a regular call + if abi_args: + raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}") + atc.add_transaction( + TransactionWithSigner( + txn=transaction.ApplicationCallTxn( # type: ignore[no-untyped-call] + sender=sender, + sp=sp, + index=app_id, + on_complete=on_complete, + approval_program=approval_program, + clear_program=clear_program, + global_schema=global_schema, + local_schema=local_schema, + extra_pages=extra_pages, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, + boxes=encoded_boxes, + note=parameters.note, + lease=encoded_lease, + rekey_to=parameters.rekey_to, + app_args=app_args, + ), + signer=signer, + ) + ) + return + # resolve ABI method args + args = self._get_abi_method_args(abi_args, method) + atc.add_method_call( + app_id, + method, + sender, + sp, + signer, + method_args=args, + on_complete=on_complete, + local_schema=local_schema, + global_schema=global_schema, + approval_program=approval_program, + clear_program=clear_program, + extra_pages=extra_pages or 0, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, + boxes=encoded_boxes, + note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, + lease=encoded_lease, + rekey_to=parameters.rekey_to, + ) + + def _get_abi_method_args(self, abi_args: ABIArgsDict | None, method: Method) -> list: + args: list = [] + hints = self._method_hints(method) + # copy args so we don't mutate original + abi_args = dict(abi_args or {}) + for method_arg in method.args: + name = method_arg.name + if name in abi_args: + argument = abi_args.pop(name) + if isinstance(argument, dict): + if hints.structs is None or name not in hints.structs: + raise Exception(f"Argument missing struct hint: {name}. Check argument name and type") + + elements = hints.structs[name]["elements"] + + argument_tuple = tuple(argument[field_name] for field_name, field_type in elements) + args.append(argument_tuple) + else: + args.append(argument) + + elif hints.default_arguments is not None and name in hints.default_arguments: + default_arg = hints.default_arguments[name] + if default_arg is not None: + args.append(self.resolve(default_arg)) + else: + raise Exception(f"Unspecified argument: {name}") + if abi_args: + raise Exception(f"Unused arguments specified: {', '.join(abi_args)}") + return args + + def _method_matches( + self, + method: Method, + args: ABIArgsDict | None, + on_complete: transaction.OnComplete, + call_config: au_spec.CallConfig, + ) -> bool: + hints = self._method_hints(method) + if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): + return False + method_args = {m.name for m in method.args} + provided_args = set(args or {}) | set(hints.default_arguments) + + # TODO: also match on types? + return method_args == provided_args + + def _find_abi_methods( + self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: au_spec.CallConfig + ) -> list[Method]: + return [ + method + for method in self.app_spec.contract.methods + if self._method_matches(method, args, on_complete, call_config) + ] + + def _resolve_abi_method(self, method: ABIMethod) -> Method: + if isinstance(method, str): + try: + return next(iter(m for m in self.app_spec.contract.methods if m.get_signature() == method)) + except StopIteration: + pass + return self.app_spec.contract.get_method_by_name(method) + elif hasattr(method, "method_spec"): + return method.method_spec() + else: + return method + + def _method_hints(self, method: Method) -> au_spec.MethodHints: + sig = method.get_signature() + if sig not in self.app_spec.hints: + return au_spec.MethodHints() + return self.app_spec.hints[sig] + + def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: + result = self.execute_atc(atc) + return TransactionResponse.from_atr(result) + + def execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: + return execute_atc_with_logic_error( + atc, + self.algod_client, + approval_program=self.app_spec.approval_program, + approval_source_map=self._get_approval_source_map, + ) + + def get_signer_sender( + self, signer: TransactionSigner | None = None, sender: str | None = None + ) -> tuple[TransactionSigner | None, str | None]: + """Return signer and sender, using default values on client if not specified + + Will use provided values if given, otherwise will fall back to values defined on client. + If no sender is specified then will attempt to obtain sender from signer""" + resolved_signer = signer or self.signer + resolved_sender = sender or get_sender_from_signer(signer) or self.sender or get_sender_from_signer(self.signer) + return resolved_signer, resolved_sender + + def resolve_signer_sender( + self, signer: TransactionSigner | None = None, sender: str | None = None + ) -> tuple[TransactionSigner, str]: + """Return signer and sender, using default values on client if not specified + + Will use provided values if given, otherwise will fall back to values defined on client. + If no sender is specified then will attempt to obtain sender from signer + + :raises ValueError: Raised if a signer or sender is not provided. See `get_signer_sender` + for variant with no exception""" + resolved_signer, resolved_sender = self.get_signer_sender(signer, sender) + if not resolved_signer: + raise ValueError("No signer provided") + if not resolved_sender: + raise ValueError("No sender provided") + return resolved_signer, resolved_sender + + # TODO: remove private implementation, kept in the 1.0.2 release to not impact existing beaker 1.0 installs + _resolve_signer_sender = resolve_signer_sender + + +def substitute_template_and_compile( + algod_client: "AlgodClient", + app_spec: au_spec.ApplicationSpecification, + template_values: au_deploy.TemplateValueMapping, +) -> tuple[Program, Program]: + """Substitutes the provided template_values into app_spec and compiles""" + template_values = dict(template_values or {}) + clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values) + + au_deploy.check_template_variables(app_spec.approval_program, template_values) + approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values) + + approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client) + + return approval_app, clear_app + + +def get_next_version(current_version: str) -> str: + """Calculates the next version from `current_version` + + Next version is calculated by finding a semver like + version string and incrementing the lower. This function is used by {py:meth}`ApplicationClient.deploy` when + a version is not specified, and is intended mostly for convenience during local development. + + :params str current_version: An existing version string with a semver like version contained within it, + some valid inputs and incremented outputs: + `1` -> `2` + `1.0` -> `1.1` + `v1.1` -> `v1.2` + `v1.1-beta1` -> `v1.2-beta1` + `v1.2.3.4567` -> `v1.2.3.4568` + `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` + :raises DeploymentFailedError: If `current_version` cannot be parsed""" + pattern = re.compile(r"(?P\w*)(?P(?:\d+\.)*\d+)(?P\w*)") + match = pattern.match(current_version) + if match: + version = match.group("version") + new_version = _increment_version(version) + + def replacement(m: re.Match) -> str: + return f"{m.group('prefix')}{new_version}{m.group('suffix')}" + + return re.sub(pattern, replacement, current_version) + raise au_deploy.DeploymentFailedError( + f"Could not auto increment {current_version}, please specify the next version using the version parameter" + ) + + +def _try_convert_to_logic_error( + source_ex: Exception | str, + approval_program: str, + approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, + simulate_traces: list[SimulationTrace] | None = None, +) -> Exception | None: + source_ex_str = str(source_ex) + logic_error_data = parse_logic_error(source_ex_str) + if logic_error_data: + return LogicError( + logic_error_str=source_ex_str, + logic_error=source_ex if isinstance(source_ex, Exception) else None, + program=approval_program, + source_map=approval_source_map() if callable(approval_source_map) else approval_source_map, + **logic_error_data, + traces=simulate_traces, + ) + + return None + + +def execute_atc_with_logic_error( + atc: AtomicTransactionComposer, + algod_client: "AlgodClient", + approval_program: str, + wait_rounds: int = 4, + approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, +) -> AtomicTransactionResponse: + """Calls {py:meth}`AtomicTransactionComposer.execute` on provided `atc`, but will parse any errors + and raise a {py:class}`LogicError` if possible + + ```{note} + `approval_program` and `approval_source_map` are required to be able to parse any errors into a + {py:class}`LogicError` + ``` + """ + try: + if config.debug and config.project_root and config.trace_all: + simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb) + + return atc.execute(algod_client, wait_rounds=wait_rounds) + except Exception as ex: + if config.debug: + simulate = None + if config.project_root and not config.trace_all: + # if trace_all is enabled, we already have the traces executed above + # hence we only need to simulate if trace_all is disabled and + # project_root is set + simulate = simulate_and_persist_response( + atc, config.project_root, algod_client, config.trace_buffer_size_mb + ) + else: + simulate = simulate_response(atc, algod_client) + traces = _create_simulate_traces(simulate) + else: + traces = None + logger.info("An error occurred while executing the transaction.") + logger.info("To see more details, enable debug mode by setting config.debug = True ") + + logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces) + if logic_error: + raise logic_error from ex + raise ex + + +def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: + traces = [] + if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget_added = txn_group.get("app-budget-added", None) + app_budget_consumed = txn_group.get("app-budget-consumed", None) + failure_message = txn_group.get("failure-message", None) + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + traces.append( + SimulationTrace( + app_budget_added=app_budget_added, + app_budget_consumed=app_budget_consumed, + failure_message=failure_message, + exec_trace=exec_trace, + ) + ) + return traces + + +def _convert_transaction_parameters( + args: TransactionParameters | TransactionParametersDict | None, +) -> CreateCallParameters: + _args = args.__dict__ if isinstance(args, TransactionParameters) else (args or {}) + return CreateCallParameters(**_args) + + +def get_sender_from_signer(signer: TransactionSigner | None) -> str | None: + """Returns the associated address of a signer, return None if no address found""" + + if isinstance(signer, AccountTransactionSigner): + sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] + assert isinstance(sender, str) + return sender + elif isinstance(signer, MultisigTransactionSigner): + sender = signer.msig.address() # type: ignore[no-untyped-call] + assert isinstance(sender, str) + return sender + elif isinstance(signer, LogicSigTransactionSigner): + return signer.lsig.address() + return None + + +# TEMPORARY, use SDK one when available +def _parse_result( + methods: dict[int, Method], + txns: list[dict[str, Any]], + txids: list[str], +) -> list[ABIResult]: + method_results = [] + for i, tx_info in enumerate(txns): + raw_value = b"" + return_value = None + decode_error = None + + if i not in methods: + continue + + # Parse log for ABI method return value + try: + if methods[i].returns.type == Returns.VOID: + method_results.append( + ABIResult( + tx_id=txids[i], + raw_value=raw_value, + return_value=return_value, + decode_error=decode_error, + tx_info=tx_info, + method=methods[i], + ) + ) + continue + + logs = tx_info.get("logs", []) + + # Look for the last returned value in the log + if not logs: + raise Exception("No logs") + + result = logs[-1] + # Check that the first four bytes is the hash of "return" + result_bytes = base64.b64decode(result) + if len(result_bytes) < len(ABI_RETURN_HASH) or result_bytes[: len(ABI_RETURN_HASH)] != ABI_RETURN_HASH: + raise Exception("no logs") + + raw_value = result_bytes[4:] + abi_return_type = methods[i].returns.type + if isinstance(abi_return_type, ABIType): + return_value = abi_return_type.decode(raw_value) + else: + return_value = raw_value + + except Exception as e: + decode_error = e + + method_results.append( + ABIResult( + tx_id=txids[i], + raw_value=raw_value, + return_value=return_value, + decode_error=decode_error, + tx_info=tx_info, + method=methods[i], + ) + ) + + return method_results + + +def _increment_version(version: str) -> str: + split = list(map(int, version.split("."))) + split[-1] = split[-1] + 1 + return ".".join(str(x) for x in split) + + +def _str_or_hex(v: bytes) -> str: + decoded: str + try: + decoded = v.decode("utf-8") + except UnicodeDecodeError: + decoded = v.hex() + + return decoded + + +def _decode_state(state: list[dict[str, Any]], *, raw: bool = False) -> dict[str | bytes, bytes | str | int | None]: + decoded_state: dict[str | bytes, bytes | str | int | None] = {} + + for state_value in state: + raw_key = base64.b64decode(state_value["key"]) + + key: str | bytes = raw_key if raw else _str_or_hex(raw_key) + val: str | bytes | int | None + + action = state_value["value"]["action"] if "action" in state_value["value"] else state_value["value"]["type"] + + match action: + case 1: + raw_val = base64.b64decode(state_value["value"]["bytes"]) + val = raw_val if raw else _str_or_hex(raw_val) + case 2: + val = state_value["value"]["uint"] + case 3: + val = None + case _: + raise NotImplementedError + + decoded_state[key] = val + return decoded_state diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py new file mode 100644 index 00000000..392fce8d --- /dev/null +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -0,0 +1,206 @@ +import base64 +import dataclasses +import json +from enum import IntFlag +from pathlib import Path +from typing import Any, Literal, TypeAlias, TypedDict + +from algosdk.abi import Contract +from algosdk.abi.method import MethodDict +from algosdk.transaction import StateSchema + +__all__ = [ + "CallConfig", + "DefaultArgumentDict", + "DefaultArgumentType", + "MethodConfigDict", + "OnCompleteActionName", + "MethodHints", + "ApplicationSpecification", + "AppSpecStateDict", +] + + +AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] +"""Type defining Application Specification state entries""" + + +class CallConfig(IntFlag): + """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" + + NEVER = 0 + """Never handle the specified on completion type""" + CALL = 1 + """Only handle the specified on completion type for application calls""" + CREATE = 2 + """Only handle the specified on completion type for application create calls""" + ALL = 3 + """Handle the specified on completion type for both create and normal application calls""" + + +class StructArgDict(TypedDict): + name: str + elements: list[list[str]] + + +OnCompleteActionName: TypeAlias = Literal[ + "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" +] +"""String literals representing on completion transaction types""" +MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] +"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" +DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] +"""Literal values describing the types of default argument sources""" + + +class DefaultArgumentDict(TypedDict): + """ + DefaultArgument is a container for any arguments that may + be resolved prior to calling some target method + """ + + source: DefaultArgumentType + data: int | str | bytes | MethodDict + + +StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword + "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} +) + + +@dataclasses.dataclass(kw_only=True) +class MethodHints: + """MethodHints provides hints to the caller about how to call the method""" + + #: hint to indicate this method can be called through Dryrun + read_only: bool = False + #: hint to provide names for tuple argument indices + #: method_name=>param_name=>{name:str, elements:[str,str]} + structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) + #: defaults + default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) + call_config: MethodConfigDict = dataclasses.field(default_factory=dict) + + def empty(self) -> bool: + return not self.dictify() + + def dictify(self) -> dict[str, Any]: + d: dict[str, Any] = {} + if self.read_only: + d["read_only"] = True + if self.default_arguments: + d["default_arguments"] = self.default_arguments + if self.structs: + d["structs"] = self.structs + if any(v for v in self.call_config.values() if v != CallConfig.NEVER): + d["call_config"] = _encode_method_config(self.call_config) + return d + + @staticmethod + def undictify(data: dict[str, Any]) -> "MethodHints": + return MethodHints( + read_only=data.get("read_only", False), + default_arguments=data.get("default_arguments", {}), + structs=data.get("structs", {}), + call_config=_decode_method_config(data.get("call_config", {})), + ) + + +def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: + return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} + + +def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: + return {k: CallConfig[v] for k, v in data.items()} + + +def _encode_source(teal_text: str) -> str: + return base64.b64encode(teal_text.encode()).decode("utf-8") + + +def _decode_source(b64_text: str) -> str: + return base64.b64decode(b64_text).decode("utf-8") + + +def _encode_state_schema(schema: StateSchema) -> dict[str, int]: + return { + "num_byte_slices": schema.num_byte_slices, + "num_uints": schema.num_uints, + } + + +def _decode_state_schema(data: dict[str, int]) -> StateSchema: + return StateSchema( # type: ignore[no-untyped-call] + num_byte_slices=data.get("num_byte_slices", 0), + num_uints=data.get("num_uints", 0), + ) + + +@dataclasses.dataclass(kw_only=True) +class ApplicationSpecification: + """ARC-0032 application specification + + See """ + + approval_program: str + clear_program: str + contract: Contract + hints: dict[str, MethodHints] + schema: StateDict + global_state_schema: StateSchema + local_state_schema: StateSchema + bare_call_config: MethodConfigDict + + def dictify(self) -> dict: + return { + "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, + "source": { + "approval": _encode_source(self.approval_program), + "clear": _encode_source(self.clear_program), + }, + "state": { + "global": _encode_state_schema(self.global_state_schema), + "local": _encode_state_schema(self.local_state_schema), + }, + "schema": self.schema, + "contract": self.contract.dictify(), + "bare_call_config": _encode_method_config(self.bare_call_config), + } + + def to_json(self) -> str: + return json.dumps(self.dictify(), indent=4) + + @staticmethod + def from_json(application_spec: str) -> "ApplicationSpecification": + json_spec = json.loads(application_spec) + return ApplicationSpecification( + approval_program=_decode_source(json_spec["source"]["approval"]), + clear_program=_decode_source(json_spec["source"]["clear"]), + schema=json_spec["schema"], + global_state_schema=_decode_state_schema(json_spec["state"]["global"]), + local_state_schema=_decode_state_schema(json_spec["state"]["local"]), + contract=Contract.undictify(json_spec["contract"]), + hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, + bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), + ) + + def export(self, directory: Path | str | None = None) -> None: + """write out the artifacts generated by the application to disk + + Args: + directory(optional): path to the directory where the artifacts should be written + """ + if directory is None: + output_dir = Path.cwd() + else: + output_dir = Path(directory) + output_dir.mkdir(exist_ok=True, parents=True) + + (output_dir / "approval.teal").write_text(self.approval_program) + (output_dir / "clear.teal").write_text(self.clear_program) + (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) + (output_dir / "application.json").write_text(self.to_json()) + + +def _state_schema(schema: dict[str, int]) -> StateSchema: + return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py new file mode 100644 index 00000000..2ef4860f --- /dev/null +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -0,0 +1,168 @@ +import logging +from typing import TYPE_CHECKING + +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner +from algosdk.constants import TX_GROUP_LIMIT +from algosdk.transaction import AssetTransferTxn + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + +from enum import Enum, auto + +from algokit_utils._legacy_v2.models import Account + +__all__ = ["opt_in", "opt_out"] +logger = logging.getLogger(__name__) + + +class ValidationType(Enum): + OPTIN = auto() + OPTOUT = auto() + + +def _ensure_account_is_valid(algod_client: "AlgodClient", account: Account) -> None: + try: + algod_client.account_info(account.address) + except Exception as err: + error_message = f"Account address{account.address} does not exist" + logger.debug(error_message) + raise err + + +def _ensure_asset_balance_conditions( + algod_client: "AlgodClient", account: Account, asset_ids: list, validation_type: ValidationType +) -> None: + invalid_asset_ids = [] + account_info = algod_client.account_info(account.address) + account_assets = account_info.get("assets", []) # type: ignore # noqa: PGH003 + for asset_id in asset_ids: + asset_exists_in_account_info = any(asset["asset-id"] == asset_id for asset in account_assets) + if validation_type == ValidationType.OPTIN: + if asset_exists_in_account_info: + logger.debug(f"Asset {asset_id} is already opted in for account {account.address}") + invalid_asset_ids.append(asset_id) + + elif validation_type == ValidationType.OPTOUT: + if not account_assets or not asset_exists_in_account_info: + logger.debug(f"Account {account.address} does not have asset {asset_id}") + invalid_asset_ids.append(asset_id) + else: + asset_balance = next((asset["amount"] for asset in account_assets if asset["asset-id"] == asset_id), 0) + if asset_balance != 0: + logger.debug(f"Asset {asset_id} balance is not zero") + invalid_asset_ids.append(asset_id) + + if len(invalid_asset_ids) > 0: + action = "opted out" if validation_type == ValidationType.OPTOUT else "opted in" + condition_message = ( + "their amount is zero and that the account has" + if validation_type == ValidationType.OPTOUT + else "they are valid and that the account has not" + ) + + error_message = ( + f"Assets {invalid_asset_ids} cannot be {action}. Ensure that " + f"{condition_message} previously opted into them." + ) + raise ValueError(error_message) + + +def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: + """ + Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, + it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases + its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). + + Args: + algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. + account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. + asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. + Returns: + dict[int, str]: A dictionary where the keys are the asset IDs and the values + are the transaction IDs for opting-in to each asset. + """ + _ensure_account_is_valid(algod_client, account) + _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN) + suggested_params = algod_client.suggested_params() + result = {} + for i in range(0, len(asset_ids), TX_GROUP_LIMIT): + atc = AtomicTransactionComposer() + chunk = asset_ids[i : i + TX_GROUP_LIMIT] + for asset_id in chunk: + asset = algod_client.asset_info(asset_id) + xfer_txn = AssetTransferTxn( + sp=suggested_params, + sender=account.address, + receiver=account.address, + close_assets_to=None, + revocation_target=None, + amt=0, + note=f"opt in asset id ${asset_id}", + index=asset["index"], # type: ignore # noqa: PGH003 + rekey_to=None, + ) + + transaction_with_signer = TransactionWithSigner( + txn=xfer_txn, + signer=account.signer, + ) + atc.add_transaction(transaction_with_signer) + atc.execute(algod_client, 4) + + for index, asset_id in enumerate(chunk): + result[asset_id] = atc.tx_ids[index] + + return result + + +def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: + """ + Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. + The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) + The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. + + It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero. + + Args: + algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. + account (Account): An instance of the Account class that holds the private key and address for an account. + asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. + Returns: + dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of + the executed transactions. + + """ + _ensure_account_is_valid(algod_client, account) + _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT) + suggested_params = algod_client.suggested_params() + result = {} + for i in range(0, len(asset_ids), TX_GROUP_LIMIT): + atc = AtomicTransactionComposer() + chunk = asset_ids[i : i + TX_GROUP_LIMIT] + for asset_id in chunk: + asset = algod_client.asset_info(asset_id) + asset_creator = asset["params"]["creator"] # type: ignore # noqa: PGH003 + xfer_txn = AssetTransferTxn( + sp=suggested_params, + sender=account.address, + receiver=account.address, + close_assets_to=asset_creator, + revocation_target=None, + amt=0, + note=f"opt out asset id ${asset_id}", + index=asset["index"], # type: ignore # noqa: PGH003 + rekey_to=None, + ) + + transaction_with_signer = TransactionWithSigner( + txn=xfer_txn, + signer=account.signer, + ) + atc.add_transaction(transaction_with_signer) + atc.execute(algod_client, 4) + + for index, asset_id in enumerate(chunk): + result[asset_id] = atc.tx_ids[index] + + return result diff --git a/src/algokit_utils/_legacy_v2/common.py b/src/algokit_utils/_legacy_v2/common.py new file mode 100644 index 00000000..cd412f82 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/common.py @@ -0,0 +1,28 @@ +""" +This module contains common classes and methods that are reused in more than one file. +""" + +import base64 +import typing + +from algosdk.source_map import SourceMap + +from algokit_utils._legacy_v2.deploy import strip_comments + +if typing.TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + + +class Program: + """A compiled TEAL program""" + + def __init__(self, program: str, client: "AlgodClient"): + """ + Fully compile the program source to binary and generate a + source map for matching pc to line number + """ + self.teal = program + result: dict = client.compile(strip_comments(self.teal), source_map=True) + self.raw_binary = base64.b64decode(result["result"]) + self.binary_hash: str = result["hash"] + self.source_map = SourceMap(result["sourcemap"]) diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py new file mode 100644 index 00000000..561ce413 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -0,0 +1,897 @@ +import base64 +import dataclasses +import json +import logging +import re +from collections.abc import Iterable, Mapping, Sequence +from enum import Enum +from typing import TYPE_CHECKING, TypeAlias, TypedDict + +from algosdk import transaction +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner +from algosdk.logic import get_application_address +from algosdk.transaction import StateSchema + +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + CallConfig, + MethodConfigDict, + OnCompleteActionName, +) +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIMethod, + Account, + CreateCallParameters, + TransactionResponse, +) + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + from algokit_utils._legacy_v2.application_client import ApplicationClient + + +__all__ = [ + "UPDATABLE_TEMPLATE_NAME", + "DELETABLE_TEMPLATE_NAME", + "NOTE_PREFIX", + "ABICallArgs", + "ABICreateCallArgs", + "ABICallArgsDict", + "ABICreateCallArgsDict", + "DeploymentFailedError", + "AppReference", + "AppDeployMetaData", + "AppMetaData", + "AppLookup", + "DeployCallArgs", + "DeployCreateCallArgs", + "DeployCallArgsDict", + "DeployCreateCallArgsDict", + "Deployer", + "DeployResponse", + "OnUpdate", + "OnSchemaBreak", + "OperationPerformed", + "TemplateValueDict", + "TemplateValueMapping", + "get_app_id_from_tx_id", + "get_creator_apps", + "replace_template_variables", +] + +logger = logging.getLogger(__name__) + +DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT = 1000 +_UPDATABLE = "UPDATABLE" +_DELETABLE = "DELETABLE" +UPDATABLE_TEMPLATE_NAME = f"TMPL_{_UPDATABLE}" +"""Template variable name used to control if a smart contract is updatable or not at deployment""" +DELETABLE_TEMPLATE_NAME = f"TMPL_{_DELETABLE}" +"""Template variable name used to control if a smart contract is deletable or not at deployment""" +_TOKEN_PATTERN = re.compile(r"TMPL_[A-Z_]+") +TemplateValue: TypeAlias = int | str | bytes +TemplateValueDict: TypeAlias = dict[str, TemplateValue] +"""Dictionary of `dict[str, int | str | bytes]` representing template variable names and values""" +TemplateValueMapping: TypeAlias = Mapping[str, TemplateValue] +"""Mapping of `str` to `int | str | bytes` representing template variable names and values""" + +NOTE_PREFIX = "ALGOKIT_DEPLOYER:j" +"""ARC-0002 compliant note prefix for algokit_utils deployed applications""" +# This prefix is also used to filter for parsable transaction notes in get_creator_apps. +# However, as the note is base64 encoded first we need to consider it's base64 representation. +# When base64 encoding bytes, 3 bytes are stored in every 4 characters. +# So then we don't need to worry about the padding/changing characters of the prefix if it was followed by +# additional characters, assert the NOTE_PREFIX length is a multiple of 3. +assert len(NOTE_PREFIX) % 3 == 0 + + +class DeploymentFailedError(Exception): + pass + + +@dataclasses.dataclass +class AppReference: + """Information about an Algorand app""" + + app_id: int + app_address: str + + +@dataclasses.dataclass +class AppDeployMetaData: + """Metadata about an application stored in a transaction note during creation. + + The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field + as part of {py:meth}`ApplicationClient.deploy` + """ + + name: str + version: str + deletable: bool | None + updatable: bool | None + + @staticmethod + def from_json(value: str) -> "AppDeployMetaData": + json_value: dict = json.loads(value) + json_value.setdefault("deletable", None) + json_value.setdefault("updatable", None) + return AppDeployMetaData(**json_value) + + @classmethod + def from_b64(cls: type["AppDeployMetaData"], b64: str) -> "AppDeployMetaData": + return cls.decode(base64.b64decode(b64)) + + @classmethod + def decode(cls: type["AppDeployMetaData"], value: bytes) -> "AppDeployMetaData": + note = value.decode("utf-8") + assert note.startswith(NOTE_PREFIX) + return cls.from_json(note[len(NOTE_PREFIX) :]) + + def encode(self) -> bytes: + json_str = json.dumps(self.__dict__) + return f"{NOTE_PREFIX}{json_str}".encode() + + +@dataclasses.dataclass +class AppMetaData(AppReference, AppDeployMetaData): + """Metadata about a deployed app""" + + created_round: int + updated_round: int + created_metadata: AppDeployMetaData + deleted: bool + + +@dataclasses.dataclass +class AppLookup: + """Cache of {py:class}`AppMetaData` for a specific `creator` + + Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple + apps or discovering multiple app_ids + """ + + creator: str + apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) + + +def _sort_by_round(txn: dict) -> tuple[int, int]: + confirmed = txn["confirmed-round"] + offset = txn["intra-round-offset"] + return confirmed, offset + + +def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: + if not metadata_b64: + return None + # noinspection PyBroadException + try: + return AppDeployMetaData.from_b64(metadata_b64) + except Exception: + return None + + +def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: + """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified + creator that have a transaction note containing {py:class}`AppDeployMetaData` + """ + apps: dict[str, AppMetaData] = {} + + creator_address = creator_account if isinstance(creator_account, str) else creator_account.address + token = None + # TODO: paginated indexer call instead of N + 1 calls + while True: + response = indexer.lookup_account_application_by_creator( + creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token + ) # type: ignore[no-untyped-call] + if "message" in response: # an error occurred + raise Exception(f"Error querying applications for {creator_address}: {response}") + for app in response["applications"]: + app_id = app["id"] + app_created_at_round = app["created-at-round"] + app_deleted = app.get("deleted", False) + search_transactions_response = indexer.search_transactions( + min_round=app_created_at_round, + txn_type="appl", + application_id=app_id, + address=creator_address, + address_role="sender", + note_prefix=NOTE_PREFIX.encode("utf-8"), + ) # type: ignore[no-untyped-call] + transactions: list[dict] = search_transactions_response["transactions"] + if not transactions: + continue + + created_transaction = next( + t + for t in transactions + if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address + ) + + transactions.sort(key=_sort_by_round, reverse=True) + latest_transaction = transactions[0] + app_updated_at_round = latest_transaction["confirmed-round"] + + create_metadata = _parse_note(created_transaction.get("note")) + update_metadata = _parse_note(latest_transaction.get("note")) + + if create_metadata and create_metadata.name: + apps[create_metadata.name] = AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + created_metadata=create_metadata, + created_round=app_created_at_round, + **(update_metadata or create_metadata).__dict__, + updated_round=app_updated_at_round, + deleted=app_deleted, + ) + + token = response.get("next-token") + if not token: + break + + return AppLookup(creator_address, apps) + + +def _state_schema(schema: dict[str, int]) -> StateSchema: + return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] + + +def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: + if to_schema.num_uints > from_schema.num_uints: + yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}" + if to_schema.num_byte_slices > from_schema.num_byte_slices: + yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}" + + +@dataclasses.dataclass(kw_only=True) +class AppChanges: + app_updated: bool + schema_breaking_change: bool + schema_change_description: str | None + + +def check_for_app_changes( # noqa: PLR0913 + algod_client: "AlgodClient", + *, + new_approval: bytes, + new_clear: bytes, + new_global_schema: StateSchema, + new_local_schema: StateSchema, + app_id: int, +) -> AppChanges: + application_info = algod_client.application_info(app_id) + assert isinstance(application_info, dict) + application_create_params = application_info["params"] + + current_approval = base64.b64decode(application_create_params["approval-program"]) + current_clear = base64.b64decode(application_create_params["clear-state-program"]) + current_global_schema = _state_schema(application_create_params["global-state-schema"]) + current_local_schema = _state_schema(application_create_params["local-state-schema"]) + + app_updated = current_approval != new_approval or current_clear != new_clear + + schema_changes: list[str] = [] + schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema)) + schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema)) + + return AppChanges( + app_updated=app_updated, + schema_breaking_change=bool(schema_changes), + schema_change_description=", ".join(schema_changes), + ) + + +def _is_valid_token_character(char: str) -> bool: + return char.isalnum() or char == "_" + + +def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: + result: list[str] = [] + match_count = 0 + token = f"TMPL_{template_variable}" + token_idx_offset = len(value) - len(token) + for line in program_lines: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + comment_idx = len(line) + code = line[:comment_idx] + comment = line[comment_idx:] + trailing_idx = 0 + while True: + token_idx = _find_template_token(code, token, trailing_idx) + if token_idx is None: + break + + trailing_idx = token_idx + len(token) + prefix = code[:token_idx] + suffix = code[trailing_idx:] + code = f"{prefix}{value}{suffix}" + match_count += 1 + trailing_idx += token_idx_offset + result.append(code + comment) + return result, match_count + + +def add_deploy_template_variables( + template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None +) -> None: + if allow_update is not None: + template_values[_UPDATABLE] = int(allow_update) + if allow_delete is not None: + template_values[_DELETABLE] = int(allow_delete) + + +def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. + Returns None if not found""" + + if end < 0: + end = len(line) + idx = start + in_quotes = in_base64 = False + while idx < end: + current_char = line[idx] + match current_char: + # enter base64 + case " " | "(" if not in_quotes and _last_token_base64(line, idx): + in_base64 = True + # exit base64 + case " " | ")" if not in_quotes and in_base64: + in_base64 = False + # escaped char + case "\\" if in_quotes: + # skip next character + idx += 1 + # quote boundary + case '"': + in_quotes = not in_quotes + # can test for match + case _ if not in_quotes and not in_base64 and line.startswith(token, idx): + # only match if not in quotes and string matches + return idx + idx += 1 + return None + + +def _last_token_base64(line: str, idx: int) -> bool: + try: + *_, last = line[:idx].split() + except ValueError: + return False + return last in ("base64", "b64") + + +def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. + Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING + Returns None if not found""" + if end < 0: + end = len(line) + + idx = start + while idx < end: + token_idx = _find_unquoted_string(line, token, idx, end) + if token_idx is None: + break + trailing_idx = token_idx + len(token) + if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start + trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end + ): + return token_idx + idx = trailing_idx + return None + + +def _strip_comment(line: str) -> str: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + return line + return line[:comment_idx].rstrip() + + +def strip_comments(program: str) -> str: + return "\n".join(_strip_comment(line) for line in program.splitlines()) + + +def _has_token(program_without_comments: str, token: str) -> bool: + for line in program_without_comments.splitlines(): + token_idx = _find_template_token(line, token) + if token_idx is not None: + return True + return False + + +def _find_tokens(stripped_approval_program: str) -> list[str]: + return _TOKEN_PATTERN.findall(stripped_approval_program) + + +def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: + approval_program = strip_comments(approval_program) + if _has_token(approval_program, UPDATABLE_TEMPLATE_NAME) and _UPDATABLE not in template_values: + raise DeploymentFailedError( + "allow_update must be specified if deploy time configuration of update is being used" + ) + if _has_token(approval_program, DELETABLE_TEMPLATE_NAME) and _DELETABLE not in template_values: + raise DeploymentFailedError( + "allow_delete must be specified if deploy time configuration of delete is being used" + ) + all_tokens = _find_tokens(approval_program) + missing_values = [token for token in all_tokens if token[len("TMPL_") :] not in template_values] + if missing_values: + raise DeploymentFailedError(f"The following template values were not provided: {', '.join(missing_values)}") + + for template_variable_name in template_values: + tmpl_variable = f"TMPL_{template_variable_name}" + if not _has_token(approval_program, tmpl_variable): + if template_variable_name == _UPDATABLE: + raise DeploymentFailedError( + "allow_update must only be specified if deploy time configuration of update is being used" + ) + if template_variable_name == _DELETABLE: + raise DeploymentFailedError( + "allow_delete must only be specified if deploy time configuration of delete is being used" + ) + logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") + + +def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: + """Replaces `TMPL_*` variables in `program` with `template_values` + + ```{note} + `template_values` keys should *NOT* be prefixed with `TMPL_` + ``` + """ + program_lines = program.splitlines() + for template_variable_name, template_value in template_values.items(): + match template_value: + case int(): + value = str(template_value) + case str(): + value = "0x" + template_value.encode("utf-8").hex() + case bytes(): + value = "0x" + template_value.hex() + case _: + raise DeploymentFailedError( + f"Unexpected template value type {template_variable_name}: {template_value.__class__}" + ) + + program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value) + + return "\n".join(program_lines) + + +def has_template_vars(app_spec: ApplicationSpecification) -> bool: + return "TMPL_" in strip_comments(app_spec.approval_program) or "TMPL_" in strip_comments(app_spec.clear_program) + + +def get_deploy_control( + app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete +) -> bool | None: + if template_var not in strip_comments(app_spec.approval_program): + return None + return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( + h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER + ) + + +def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: + def get(key: OnCompleteActionName) -> CallConfig: + return method_config.get(key, CallConfig.NEVER) + + match on_complete: + case transaction.OnComplete.NoOpOC: + return get("no_op") + case transaction.OnComplete.UpdateApplicationOC: + return get("update_application") + case transaction.OnComplete.DeleteApplicationOC: + return get("delete_application") + case transaction.OnComplete.OptInOC: + return get("opt_in") + case transaction.OnComplete.CloseOutOC: + return get("close_out") + case transaction.OnComplete.ClearStateOC: + return get("clear_state") + + +class OnUpdate(Enum): + """Action to take if an Application has been updated""" + + Fail = 0 + """Fail the deployment""" + UpdateApp = 1 + """Update the Application with the new approval and clear programs""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new application""" + + +class OnSchemaBreak(Enum): + """Action to take if an Application's schema has breaking changes""" + + Fail = 0 + """Fail the deployment""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new Application""" + + +class OperationPerformed(Enum): + """Describes the actions taken during deployment""" + + Nothing = 0 + """An existing Application was found""" + Create = 1 + """No existing Application was found, created a new Application""" + Update = 2 + """An existing Application was found, but was out of date, updated to latest version""" + Replace = 3 + """An existing Application was found, but was out of date, created a new Application and deleted the original""" + + +@dataclasses.dataclass(kw_only=True) +class DeployResponse: + """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`""" + + app: AppMetaData + create_response: TransactionResponse | None = None + delete_response: TransactionResponse | None = None + update_response: TransactionResponse | None = None + action_taken: OperationPerformed = OperationPerformed.Nothing + + +@dataclasses.dataclass(kw_only=True) +class DeployCallArgs: + """Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + suggested_params: transaction.SuggestedParams | None = None + lease: bytes | str | None = None + accounts: list[str] | None = None + foreign_apps: list[int] | None = None + foreign_assets: list[int] | None = None + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None + rekey_to: str | None = None + + +@dataclasses.dataclass(kw_only=True) +class ABICall: + method: ABIMethod | bool | None = None + args: ABIArgsDict = dataclasses.field(default_factory=dict) + + +@dataclasses.dataclass(kw_only=True) +class DeployCreateCallArgs(DeployCallArgs): + """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + extra_pages: int | None = None + on_complete: transaction.OnComplete | None = None + + +@dataclasses.dataclass(kw_only=True) +class ABICallArgs(DeployCallArgs, ABICall): + """ABI Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + +@dataclasses.dataclass(kw_only=True) +class ABICreateCallArgs(DeployCreateCallArgs, ABICall): + """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + +class DeployCallArgsDict(TypedDict, total=False): + """Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + suggested_params: transaction.SuggestedParams + lease: bytes | str + accounts: list[str] + foreign_apps: list[int] + foreign_assets: list[int] + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] + rekey_to: str + + +class ABICallArgsDict(DeployCallArgsDict, TypedDict, total=False): + """ABI Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + method: ABIMethod | bool + args: ABIArgsDict + + +class DeployCreateCallArgsDict(DeployCallArgsDict, TypedDict, total=False): + """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + extra_pages: int | None + on_complete: transaction.OnComplete + + +class ABICreateCallArgsDict(DeployCreateCallArgsDict, TypedDict, total=False): + """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + method: ABIMethod | bool + args: ABIArgsDict + + +@dataclasses.dataclass(kw_only=True) +class Deployer: + app_client: "ApplicationClient" + creator: str + signer: TransactionSigner + sender: str + existing_app_metadata_or_reference: AppReference | AppMetaData + new_app_metadata: AppDeployMetaData + on_update: OnUpdate + on_schema_break: OnSchemaBreak + create_args: ABICreateCallArgs | ABICreateCallArgsDict | DeployCreateCallArgs | None + update_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None + delete_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None + + def deploy(self) -> DeployResponse: + """Ensures app associated with app client's creator is present and up to date""" + assert self.app_client.approval + assert self.app_client.clear + + if self.existing_app_metadata_or_reference.app_id == 0: + logger.info(f"{self.new_app_metadata.name} not found in {self.creator} account, deploying app.") + return self._create_app() + + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + logger.debug( + f"{self.existing_app_metadata_or_reference.name} found in {self.creator} account, " + f"with app id {self.existing_app_metadata_or_reference.app_id}, " + f"version={self.existing_app_metadata_or_reference.version}." + ) + + app_changes = check_for_app_changes( + self.app_client.algod_client, + new_approval=self.app_client.approval.raw_binary, + new_clear=self.app_client.clear.raw_binary, + new_global_schema=self.app_client.app_spec.global_state_schema, + new_local_schema=self.app_client.app_spec.local_state_schema, + app_id=self.existing_app_metadata_or_reference.app_id, + ) + + if app_changes.schema_breaking_change: + logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") + return self._deploy_breaking_change() + + if app_changes.app_updated: + logger.info(f"Detected a TEAL update in app id {self.existing_app_metadata_or_reference.app_id}") + return self._deploy_update() + + logger.info("No detected changes in app, nothing to do.") + return DeployResponse(app=self.existing_app_metadata_or_reference) + + def _deploy_breaking_change(self) -> DeployResponse: + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + if self.on_schema_break == OnSchemaBreak.Fail: + raise DeploymentFailedError( + "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" + ) + if self.on_schema_break == OnSchemaBreak.AppendApp: + logger.info("Schema break detected and on_schema_break=AppendApp, will attempt to create new app") + return self._create_app() + + if self.existing_app_metadata_or_reference.deletable: + logger.info( + "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" + ) + elif self.existing_app_metadata_or_reference.deletable is False: + logger.warning( + "App is not deletable but on_schema_break=ReplaceApp, " + "will attempt to delete app, delete will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is deletable but on_schema_break=ReplaceApp, will attempt to delete app" + ) + return self._create_and_delete_app() + + def _deploy_update(self) -> DeployResponse: + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + if self.on_update == OnUpdate.Fail: + raise DeploymentFailedError( + "Update detected and on_update=Fail, stopping deployment. " + "If you want to try updating the app then re-run with on_update=UpdateApp" + ) + if self.on_update == OnUpdate.AppendApp: + logger.info("Update detected and on_update=AppendApp, will attempt to create new app") + return self._create_app() + elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.UpdateApp: + logger.info("App is updatable and on_update=UpdateApp, will update app") + return self._update_app() + elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.ReplaceApp: + logger.warning( + "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" + ) + return self._create_and_delete_app() + elif self.on_update == OnUpdate.ReplaceApp: + if self.existing_app_metadata_or_reference.updatable is False: + logger.warning( + "App is not updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + return self._create_and_delete_app() + else: + if self.existing_app_metadata_or_reference.updatable is False: + logger.warning( + "App is not updatable but on_update=UpdateApp, " + "will attempt to update app, update will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=UpdateApp, will attempt to update app" + ) + return self._update_app() + + def _create_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + + method, abi_args, parameters = _convert_deploy_args( + self.create_args, self.new_app_metadata, self.signer, self.sender + ) + create_response = self.app_client.create( + method, + parameters, + **abi_args, + ) + logger.info( + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " + f"with app id {self.app_client.app_id}." + ) + assert create_response.confirmed_round is not None + app_metadata = _create_metadata(self.new_app_metadata, self.app_client.app_id, create_response.confirmed_round) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + return DeployResponse(app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create) + + def _create_and_delete_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + + logger.info( + f"Replacing {self.existing_app_metadata_or_reference.name} " + f"({self.existing_app_metadata_or_reference.version}) with " + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) in {self.creator} account." + ) + atc = AtomicTransactionComposer() + create_method, create_abi_args, create_parameters = _convert_deploy_args( + self.create_args, self.new_app_metadata, self.signer, self.sender + ) + self.app_client.compose_create( + atc, + create_method, + create_parameters, + **create_abi_args, + ) + create_txn_index = len(atc.txn_list) - 1 + delete_method, delete_abi_args, delete_parameters = _convert_deploy_args( + self.delete_args, self.new_app_metadata, self.signer, self.sender + ) + self.app_client.compose_delete( + atc, + delete_method, + delete_parameters, + **delete_abi_args, + ) + delete_txn_index = len(atc.txn_list) - 1 + create_delete_response = self.app_client.execute_atc(atc) + create_response = TransactionResponse.from_atr(create_delete_response, create_txn_index) + delete_response = TransactionResponse.from_atr(create_delete_response, delete_txn_index) + self.app_client.app_id = get_app_id_from_tx_id(self.app_client.algod_client, create_response.tx_id) + logger.info( + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " + f"with app id {self.app_client.app_id}." + ) + logger.info( + f"{self.existing_app_metadata_or_reference.name} " + f"({self.existing_app_metadata_or_reference.version}) with app id " + f"{self.existing_app_metadata_or_reference.app_id}, deleted successfully." + ) + + app_metadata = _create_metadata( + self.new_app_metadata, self.app_client.app_id, create_delete_response.confirmed_round + ) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + + return DeployResponse( + app=app_metadata, + create_response=create_response, + delete_response=delete_response, + action_taken=OperationPerformed.Replace, + ) + + def _update_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + + logger.info( + f"Updating {self.existing_app_metadata_or_reference.name} to {self.new_app_metadata.version} in " + f"{self.creator} account, with app id {self.existing_app_metadata_or_reference.app_id}" + ) + method, abi_args, parameters = _convert_deploy_args( + self.update_args, self.new_app_metadata, self.signer, self.sender + ) + update_response = self.app_client.update( + method, + parameters, + **abi_args, + ) + app_metadata = _create_metadata( + self.new_app_metadata, + self.app_client.app_id, + self.existing_app_metadata_or_reference.created_round, + updated_round=update_response.confirmed_round, + original_metadata=self.existing_app_metadata_or_reference.created_metadata, + ) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + return DeployResponse(app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update) + + +def _create_metadata( + app_spec_note: AppDeployMetaData, + app_id: int, + created_round: int, + updated_round: int | None = None, + original_metadata: AppDeployMetaData | None = None, +) -> AppMetaData: + return AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + created_metadata=original_metadata or app_spec_note, + created_round=created_round, + updated_round=updated_round or created_round, + name=app_spec_note.name, + version=app_spec_note.version, + deletable=app_spec_note.deletable, + updatable=app_spec_note.updatable, + deleted=False, + ) + + +def _convert_deploy_args( + _args: DeployCallArgs | DeployCallArgsDict | None, + note: AppDeployMetaData, + signer: TransactionSigner | None, + sender: str | None, +) -> tuple[ABIMethod | bool | None, ABIArgsDict, CreateCallParameters]: + args = _args.__dict__ if isinstance(_args, DeployCallArgs) else dict(_args or {}) + + # return most derived type, unused parameters are ignored + parameters = CreateCallParameters( + note=note.encode(), + signer=signer, + sender=sender, + suggested_params=args.get("suggested_params"), + lease=args.get("lease"), + accounts=args.get("accounts"), + foreign_assets=args.get("foreign_assets"), + foreign_apps=args.get("foreign_apps"), + boxes=args.get("boxes"), + rekey_to=args.get("rekey_to"), + extra_pages=args.get("extra_pages"), + on_complete=args.get("on_complete"), + ) + + return args.get("method"), args.get("args") or {}, parameters + + +def get_app_id_from_tx_id(algod_client: "AlgodClient", tx_id: str) -> int: + """Finds the app_id for provided transaction id""" + result = algod_client.pending_transaction_info(tx_id) + assert isinstance(result, dict) + app_id = result["application-index"] + assert isinstance(app_id, int) + return app_id diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py new file mode 100644 index 00000000..a365a3c1 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -0,0 +1,85 @@ +import re +from copy import copy +from typing import TYPE_CHECKING, TypedDict + +from algokit_utils._legacy_v2.models import SimulationTrace + +if TYPE_CHECKING: + from algosdk.source_map import SourceMap as AlgoSourceMap + +__all__ = [ + "LogicError", + "parse_logic_error", +] + +LOGIC_ERROR = ( + ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" +) + + +class LogicErrorData(TypedDict): + transaction_id: str + message: str + pc: int + + +def parse_logic_error( + error_str: str, +) -> LogicErrorData | None: + match = re.match(LOGIC_ERROR, error_str) + if match is None: + return None + + return { + "transaction_id": match.group("transaction_id"), + "message": match.group("message"), + "pc": int(match.group("pc")), + } + + +class LogicError(Exception): + def __init__( # noqa: PLR0913 + self, + *, + logic_error_str: str, + program: str, + source_map: "AlgoSourceMap | None", + transaction_id: str, + message: str, + pc: int, + logic_error: Exception | None = None, + traces: list[SimulationTrace] | None = None, + ): + self.logic_error = logic_error + self.logic_error_str = logic_error_str + self.program = program + self.source_map = source_map + self.lines = program.split("\n") + self.transaction_id = transaction_id + self.message = message + self.pc = pc + self.traces = traces + + self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None + + def __str__(self) -> str: + return ( + f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" + + (":" if self.line_no is None else f" and Source Line {self.line_no}:") + + f"\n{self.trace()}" + ) + + def trace(self, lines: int = 5) -> str: + if self.line_no is None: + return """ +Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the +error please provide an approval SourceMap. Either by: + 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2.) Set approval_source_map from a previously compiled approval program OR + 3.) Import a previously exported source map using import_source_map""" + + program_lines = copy(self.lines) + program_lines[self.line_no] += "\t\t<-- Error" + lines_before = max(0, self.line_no - lines) + lines_after = min(len(program_lines), self.line_no + lines) + return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) diff --git a/src/algokit_utils/models.py b/src/algokit_utils/_legacy_v2/models.py similarity index 97% rename from src/algokit_utils/models.py rename to src/algokit_utils/_legacy_v2/models.py index e1030088..cc5d34d2 100644 --- a/src/algokit_utils/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -202,14 +202,14 @@ class TransactionParametersDict(TypedDict, total=False): """Address to rekey to""" -class OnCompleteCallParametersDict(TypedDict, TransactionParametersDict, total=False): +class OnCompleteCallParametersDict(TransactionParametersDict, total=False): """Additional parameters that can be included in a transaction when using the ApplicationClient.call/compose_call methods""" on_complete: transaction.OnComplete -class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=False): +class CreateCallParametersDict(OnCompleteCallParametersDict, total=False): """Additional parameters that can be included in a transaction when using the ApplicationClient.create/compose_create methods""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py new file mode 100644 index 00000000..2de270da --- /dev/null +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -0,0 +1,130 @@ +import dataclasses +import os +from typing import Literal +from urllib import parse + +from algosdk.kmd import KMDClient +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +__all__ = [ + "AlgoClientConfig", + "get_algod_client", + "get_algonode_config", + "get_default_localnet_config", + "get_indexer_client", + "get_kmd_client_from_algod_client", + "is_localnet", + "is_mainnet", + "is_testnet", + "AlgoClientConfigs", + "get_kmd_client", +] + + +@dataclasses.dataclass +class AlgoClientConfig: + """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or + {py:class}`algosdk.v2client.indexer.IndexerClient`""" + + server: str + """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + token: str + """API Token to authenticate with the service""" + + +@dataclasses.dataclass +class AlgoClientConfigs: + algod_config: AlgoClientConfig + indexer_config: AlgoClientConfig + kmd_config: AlgoClientConfig | None + + +def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: + """Returns the client configuration to point to the default LocalNet""" + port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] + return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) + + +def get_algonode_config( + network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str +) -> AlgoClientConfig: + client = "api" if config == "algod" else "idx" + return AlgoClientConfig( + server=f"https://{network}-{client}.algonode.cloud", + token=token, + ) + + +def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: + """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment + + If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" + config = config or _get_config_from_environment("ALGOD") + headers = {"X-Algo-API-Token": config.token} + return AlgodClient(config.token, config.server, headers) + + +def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment + + If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" + config = config or _get_config_from_environment("KMD") + return KMDClient(config.token, config.server) # type: ignore[no-untyped-call] + + +def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: + """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. + + If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`""" + config = config or _get_config_from_environment("INDEXER") + headers = {"X-Indexer-API-Token": config.token} + return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call] + + +def is_localnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" + params = client.suggested_params() + return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + + +def is_mainnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `mainnet-v1`""" + params = client.suggested_params() + return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] + + +def is_testnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `testnet-v1`""" + params = client.suggested_params() + return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] + + +def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` + + Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, + or 4002 by default""" + # We can only use Kmd on the LocalNet otherwise it's not exposed so this makes some assumptions + # (e.g. same token and server as algod and port 4002 by default) + port = os.getenv("KMD_PORT", "4002") + server = _replace_kmd_port(client.algod_address, port) + return KMDClient(client.algod_token, server) # type: ignore[no-untyped-call] + + +def _replace_kmd_port(address: str, port: str) -> str: + parsed_algod = parse.urlparse(address) + kmd_host = parsed_algod.netloc.split(":", maxsplit=1)[0] + f":{port}" + kmd_parsed = parsed_algod._replace(netloc=kmd_host) + return parse.urlunparse(kmd_parsed) + + +def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: + server = os.getenv(f"{environment_prefix}_SERVER") + if server is None: + raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") + port = os.getenv(f"{environment_prefix}_PORT") + if port: + parsed = parse.urlparse(server) + server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() + return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index a0eb7d53..cb51b335 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -1,183 +1 @@ -import logging -import os -from typing import TYPE_CHECKING, Any - -from algosdk.account import address_from_private_key -from algosdk.mnemonic import from_private_key, to_private_key -from algosdk.util import algos_to_microalgos - -from algokit_utils._transfer import TransferParameters, transfer -from algokit_utils.models import Account -from algokit_utils.network_clients import get_kmd_client_from_algod_client, is_localnet - -if TYPE_CHECKING: - from collections.abc import Callable - - from algosdk.kmd import KMDClient - from algosdk.v2client.algod import AlgodClient - -__all__ = [ - "create_kmd_wallet_account", - "get_account", - "get_account_from_mnemonic", - "get_dispenser_account", - "get_kmd_wallet_account", - "get_localnet_default_account", - "get_or_create_kmd_wallet_account", -] - -logger = logging.getLogger(__name__) -_DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 - - -def get_account_from_mnemonic(mnemonic: str) -> Account: - """Convert a mnemonic (25 word passphrase) into an Account""" - private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call] - address = address_from_private_key(private_key) # type: ignore[no-untyped-call] - return Account(private_key=private_key, address=address) - - -def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: - """Creates a wallet with specified name""" - wallet_id = kmd_client.create_wallet(name, "")["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - kmd_client.generate_key(wallet_handle) - - key_ids: list[str] = kmd_client.list_keys(wallet_handle) - account_key = key_ids[0] - - private_account_key = kmd_client.export_key(wallet_handle, "", account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] - - -def get_or_create_kmd_wallet_account( - client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None -) -> Account: - """Returns a wallet with specified name, or creates one if not found""" - kmd_client = kmd_client or get_kmd_client_from_algod_client(client) - account = get_kmd_wallet_account(client, kmd_client, name) - - if account: - account_info = client.account_info(account.address) - assert isinstance(account_info, dict) - if account_info["amount"] > 0: - return account - logger.debug(f"Found existing account in LocalNet with name '{name}', but no funds in the account.") - else: - account = create_kmd_wallet_account(kmd_client, name) - - logger.debug( - f"Couldn't find existing account in LocalNet with name '{name}'. " - f"So created account {account.address} with keys stored in KMD." - ) - - logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs") - - if fund_with_algos: - transfer( - client, - TransferParameters( - from_account=get_dispenser_account(client), - to_address=account.address, - micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call] - ), - ) - - return account - - -def _is_default_account(account: dict[str, Any]) -> bool: - return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) - - -def get_localnet_default_account(client: "AlgodClient") -> Account: - """Returns the default Account in a LocalNet instance""" - if not is_localnet(client): - raise Exception("Can't get a default account from non LocalNet network") - - account = get_kmd_wallet_account( - client, get_kmd_client_from_algod_client(client), "unencrypted-default-wallet", _is_default_account - ) - assert account - return account - - -def get_dispenser_account(client: "AlgodClient") -> Account: - """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" - if is_localnet(client): - return get_localnet_default_account(client) - return get_account(client, "DISPENSER") - - -def get_kmd_wallet_account( - client: "AlgodClient", - kmd_client: "KMDClient", - name: str, - predicate: "Callable[[dict[str, Any]], bool] | None" = None, -) -> Account | None: - """Returns wallet matching specified name and predicate or None if not found""" - wallets: list[dict] = kmd_client.list_wallets() - - wallet = next((w for w in wallets if w["name"] == name), None) - if wallet is None: - return None - - wallet_id = wallet["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - key_ids: list[str] = kmd_client.list_keys(wallet_handle) - matched_account_key = None - if predicate: - for key in key_ids: - account = client.account_info(key) - assert isinstance(account, dict) - if predicate(account): - matched_account_key = key - else: - matched_account_key = next(key_ids.__iter__(), None) - - if not matched_account_key: - return None - - private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] - - -def get_account( - client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None -) -> Account: - """Returns an Algorand account with private key loaded by convention based on the given name identifier. - - # Convention - - **Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret - Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a - secret storage service rather than the file system. - - **LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will - create it and fund the account for you - - This allows you to write code that will work seamlessly in production and local development (LocalNet) without - manual config locally (including when you reset the LocalNet). - - # Example - If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get - that private key loaded into an account object: - ```python - account = get_account('ACCOUNT', algod) - ``` - - If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account - that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. - """ - - mnemonic_key = f"{name.upper()}_MNEMONIC" - mnemonic = os.getenv(mnemonic_key) - if mnemonic: - return get_account_from_mnemonic(mnemonic) - - if is_localnet(client): - account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) - os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call] - return account - - raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'") +from algokit_utils._legacy_v2.account import * # noqa: F403 diff --git a/src/algokit_utils/accounts/__init__.py b/src/algokit_utils/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/beta/account_manager.py b/src/algokit_utils/accounts/account_manager.py similarity index 63% rename from src/algokit_utils/beta/account_manager.py rename to src/algokit_utils/accounts/account_manager.py index 7eddff75..a2527c6c 100644 --- a/src/algokit_utils/beta/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -2,12 +2,12 @@ from dataclasses import dataclass from typing import Any -from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account from algosdk.account import generate_account from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner from typing_extensions import Self -from .client_manager import ClientManager +from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account +from algokit_utils.clients.client_manager import ClientManager @dataclass @@ -86,40 +86,11 @@ def get_asset_information(self, sender: str, asset_id: int) -> dict[str, Any]: assert isinstance(info, dict) return info - # TODO - # def from_mnemonic(self, mnemonic_secret: str, sender: Optional[str] = None) -> AddrAndSigner: - # """ - # Tracks and returns an Algorand account with secret key loaded (i.e. that can sign transactions) by taking the mnemonic secret. - - # Example: - # account = account.from_mnemonic("mnemonic secret ...") - # rekeyed_account = account.from_mnemonic("mnemonic secret ...", "SENDERADDRESS...") - - # :param mnemonic_secret: The mnemonic secret representing the private key of an account; **Note: Be careful how the mnemonic is handled**, - # never commit it into source control and ideally load it from the environment (ideally via a secret storage service) rather than the file system. - # :param sender: The optional sender address to use this signer for (aka a rekeyed account) - # :return: The account - # """ - # account = mnemonic_account(mnemonic_secret) - # return self.signer_account(rekeyed_account(account, sender) if sender else account) - def from_kmd( self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, ) -> AddressAndSigner: - """ - Tracks and returns an Algorand account with private key loaded from the given KMD wallet (identified by name). - - Example (Get default funded account in a LocalNet): - default_dispenser_account = account.from_kmd('unencrypted-default-wallet', - lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000 - ) - - :param name: The name of the wallet to retrieve an account from - :param predicate: An optional filter to use to find the account (otherwise it will return a random account from the wallet) - :return: The account - """ account = get_kmd_wallet_account( name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd ) @@ -129,29 +100,6 @@ def from_kmd( self.set_signer(account.address, account.signer) return AddressAndSigner(address=account.address, signer=account.signer) - # TODO - # def multisig( - # self, multisig_params: algosdk.MultisigMetadata, signing_accounts: Union[algosdk.Account, SigningAccount] - # ) -> TransactionSignerAccount: - # """ - # Tracks and returns an account that supports partial or full multisig signing. - - # Example: - # account = account.multisig( - # { - # "version": 1, - # "threshold": 1, - # "addrs": ["ADDRESS1...", "ADDRESS2..."] - # }, - # account.from_environment('ACCOUNT1') - # ) - - # :param multisig_params: The parameters that define the multisig account - # :param signing_accounts: The signers that are currently present - # :return: A multisig account wrapper - # """ - # return self.signer_account(multisig_account(multisig_params, signing_accounts)) - def random(self) -> AddressAndSigner: """ Tracks and returns a new, random Algorand account with secret key loaded. @@ -187,14 +135,6 @@ def dispenser(self) -> AddressAndSigner: return AddressAndSigner(address=acct.address, signer=acct.signer) def localnet_dispenser(self) -> AddressAndSigner: - """ - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts). - - Example: - account = account.localnet_dispenser() - - :return: The account - """ acct = get_localnet_default_account(self._client_manager.algod) self.set_signer(acct.address, acct.signer) return AddressAndSigner(address=acct.address, signer=acct.signer) diff --git a/src/algokit_utils/accounts/models.py b/src/algokit_utils/accounts/models.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 008ac32f..2859c5d0 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -1,1449 +1 @@ -import base64 -import copy -import json -import logging -import re -import typing -from math import ceil -from pathlib import Path -from typing import Any, Literal, cast, overload - -import algosdk -from algosdk import transaction -from algosdk.abi import ABIType, Method, Returns -from algosdk.account import address_from_private_key -from algosdk.atomic_transaction_composer import ( - ABI_RETURN_HASH, - ABIResult, - AccountTransactionSigner, - AtomicTransactionComposer, - AtomicTransactionResponse, - LogicSigTransactionSigner, - MultisigTransactionSigner, - SimulateAtomicTransactionResponse, - TransactionSigner, - TransactionWithSigner, -) -from algosdk.constants import APP_PAGE_MAX_SIZE -from algosdk.logic import get_application_address -from algosdk.source_map import SourceMap - -import algokit_utils.application_specification as au_spec -import algokit_utils.deploy as au_deploy -from algokit_utils._debugging import ( - PersistSourceMapInput, - persist_sourcemaps, - simulate_and_persist_response, - simulate_response, -) -from algokit_utils.common import Program -from algokit_utils.config import config -from algokit_utils.logic_error import LogicError, parse_logic_error -from algokit_utils.models import ( - ABIArgsDict, - ABIArgType, - ABIMethod, - ABITransactionResponse, - Account, - CreateCallParameters, - CreateCallParametersDict, - OnCompleteCallParameters, - OnCompleteCallParametersDict, - SimulationTrace, - TransactionParameters, - TransactionParametersDict, - TransactionResponse, -) - -if typing.TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient - - -logger = logging.getLogger(__name__) - - -"""A dictionary `dict[str, Any]` representing ABI argument names and values""" - -__all__ = [ - "ApplicationClient", - "execute_atc_with_logic_error", - "get_next_version", - "get_sender_from_signer", - "num_extra_program_pages", -] - -"""Alias for {py:class}`pyteal.ABIReturnSubroutine`, {py:class}`algosdk.abi.method.Method` or a {py:class}`str` -representing an ABI method name or signature""" - - -def num_extra_program_pages(approval: bytes, clear: bytes) -> int: - """Calculate minimum number of extra_pages required for provided approval and clear programs""" - - return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) - - -class ApplicationClient: - """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" - - @overload - def __init__( - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - app_id: int = 0, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - ): ... - - @overload - def __init__( - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - creator: str | Account, - indexer_client: "IndexerClient | None" = None, - existing_deployments: au_deploy.AppLookup | None = None, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - app_name: str | None = None, - ): ... - - def __init__( # noqa: PLR0913 - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - app_id: int = 0, - creator: str | Account | None = None, - indexer_client: "IndexerClient | None" = None, - existing_deployments: au_deploy.AppLookup | None = None, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - app_name: str | None = None, - ): - """ApplicationClient can be created with an app_id to interact with an existing application, alternatively - it can be created with a creator and indexer_client specified to find existing applications by name and creator. - - :param AlgodClient algod_client: AlgoSDK algod client - :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one - :param int app_id: The app_id of an existing application, to instead find the application by creator and name - use the creator and indexer_client parameters - :param str | Account creator: The address or Account of the app creator to resolve the app_id - :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by - creator and app name - :param AppLookup existing_deployments: - :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and - creator was passed as an Account will use that. - :param str sender: Address to use as the sender for all transactions, will use the address associated with the - signer if not specified. - :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should - *NOT* include the TMPL_ prefix - :param str | None app_name: Name of application to use when deploying, defaults to name defined on the - Application Specification - """ - self.algod_client = algod_client - self.app_spec = ( - au_spec.ApplicationSpecification.from_json(app_spec.read_text()) if isinstance(app_spec, Path) else app_spec - ) - self._app_name = app_name - self._approval_program: Program | None = None - self._approval_source_map: SourceMap | None = None - self._clear_program: Program | None = None - - self.template_values: au_deploy.TemplateValueMapping = template_values or {} - self.existing_deployments = existing_deployments - self._indexer_client = indexer_client - if creator is not None: - if not self.existing_deployments and not self._indexer_client: - raise Exception( - "If using the creator parameter either existing_deployments or indexer_client must also be provided" - ) - self._creator: str | None = creator.address if isinstance(creator, Account) else creator - if self.existing_deployments and self.existing_deployments.creator != self._creator: - raise Exception( - "Attempt to create application client with invalid existing_deployments against" - f"a different creator ({self.existing_deployments.creator} instead of " - f"expected creator {self._creator}" - ) - self.app_id = 0 - else: - self.app_id = app_id - self._creator = None - - self.signer: TransactionSigner | None - if signer: - self.signer = ( - signer if isinstance(signer, TransactionSigner) else AccountTransactionSigner(signer.private_key) - ) - elif isinstance(creator, Account): - self.signer = AccountTransactionSigner(creator.private_key) - else: - self.signer = None - - self.sender = sender - self.suggested_params = suggested_params - - @property - def app_name(self) -> str: - return self._app_name or self.app_spec.contract.name - - @app_name.setter - def app_name(self, value: str) -> None: - self._app_name = value - - @property - def app_address(self) -> str: - return get_application_address(self.app_id) - - @property - def approval(self) -> Program | None: - return self._approval_program - - @property - def approval_source_map(self) -> SourceMap | None: - if self._approval_source_map: - return self._approval_source_map - if self._approval_program: - return self._approval_program.source_map - return None - - @approval_source_map.setter - def approval_source_map(self, value: SourceMap) -> None: - self._approval_source_map = value - - @property - def clear(self) -> Program | None: - return self._clear_program - - def prepare( - self, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - app_id: int | None = None, - template_values: au_deploy.TemplateValueDict | None = None, - ) -> "ApplicationClient": - """Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. - Will also substitute provided template_values into the associated app_spec in the copy""" - new_client: ApplicationClient = copy.copy(self) - new_client._prepare( # noqa: SLF001 - new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values - ) - return new_client - - def _prepare( # noqa: PLR0913 - self, - target: "ApplicationClient", - *, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - app_id: int | None = None, - template_values: au_deploy.TemplateValueDict | None = None, - ) -> None: - target.app_id = self.app_id if app_id is None else app_id - target.signer, target.sender = target.get_signer_sender( - AccountTransactionSigner(signer.private_key) if isinstance(signer, Account) else signer, sender - ) - target.template_values = {**self.template_values, **(template_values or {})} - - def deploy( # noqa: PLR0913 - self, - version: str | None = None, - *, - signer: TransactionSigner | None = None, - sender: str | None = None, - allow_update: bool | None = None, - allow_delete: bool | None = None, - on_update: au_deploy.OnUpdate = au_deploy.OnUpdate.Fail, - on_schema_break: au_deploy.OnSchemaBreak = au_deploy.OnSchemaBreak.Fail, - template_values: au_deploy.TemplateValueMapping | None = None, - create_args: au_deploy.ABICreateCallArgs - | au_deploy.ABICreateCallArgsDict - | au_deploy.DeployCreateCallArgs - | None = None, - update_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, - delete_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, - ) -> au_deploy.DeployResponse: - """Deploy an application and update client to reference it. - - Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator - account, including deploy-time template placeholder substitutions. - To understand the architecture decisions behind this functionality please see - - - ```{note} - If there is a breaking state schema change to an existing app (and `on_schema_break` is set to - 'ReplaceApp' the existing app will be deleted and re-created. - ``` - - ```{note} - If there is an update (different TEAL code) to an existing app (and `on_update` is set to 'ReplaceApp') - the existing app will be deleted and re-created. - ``` - - :param str version: version to use when creating or updating app, if None version will be auto incremented - :param algosdk.atomic_transaction_composer.TransactionSigner signer: signer to use when deploying app - , if None uses self.signer - :param str sender: sender address to use when deploying app, if None uses self.sender - :param bool allow_delete: Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app - can be deleted - :param bool allow_update: Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app - can be updated - :param OnUpdate on_update: Determines what action to take if an application update is required - :param OnSchemaBreak on_schema_break: Determines what action to take if an application schema requirements - has increased beyond the current allocation - :param dict[str, int|str|bytes] template_values: Values to use for `TMPL_*` template variables, dictionary keys - should *NOT* include the TMPL_ prefix - :param ABICreateCallArgs create_args: Arguments used when creating an application - :param ABICallArgs | ABICallArgsDict update_args: Arguments used when updating an application - :param ABICallArgs | ABICallArgsDict delete_args: Arguments used when deleting an application - :return DeployResponse: details action taken and relevant transactions - :raises DeploymentError: If the deployment failed - """ - # check inputs - if self.app_id: - raise au_deploy.DeploymentFailedError( - f"Attempt to deploy app which already has an app index of {self.app_id}" - ) - try: - resolved_signer, resolved_sender = self.resolve_signer_sender(signer, sender) - except ValueError as ex: - raise au_deploy.DeploymentFailedError(f"{ex}, unable to deploy app") from None - if not self._creator: - raise au_deploy.DeploymentFailedError("No creator provided, unable to deploy app") - if self._creator != resolved_sender: - raise au_deploy.DeploymentFailedError( - f"Attempt to deploy contract with a sender address {resolved_sender} that differs " - f"from the given creator address for this application client: {self._creator}" - ) - - # make a copy and prepare variables - template_values = {**self.template_values, **(template_values or {})} - au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) - - existing_app_metadata_or_reference = self._load_app_reference() - - self._approval_program, self._clear_program = substitute_template_and_compile( - self.algod_client, self.app_spec, template_values - ) - - if config.debug and config.project_root: - persist_sourcemaps( - sources=[ - PersistSourceMapInput( - compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" - ), - PersistSourceMapInput( - compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" - ), - ], - project_root=config.project_root, - client=self.algod_client, - with_sources=True, - ) - - deployer = au_deploy.Deployer( - app_client=self, - creator=self._creator, - signer=resolved_signer, - sender=resolved_sender, - new_app_metadata=self._get_app_deploy_metadata(version, allow_update, allow_delete), - existing_app_metadata_or_reference=existing_app_metadata_or_reference, - on_update=on_update, - on_schema_break=on_schema_break, - create_args=create_args, - update_args=update_args, - delete_args=delete_args, - ) - - return deployer.deploy() - - def compose_create( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" - approval_program, clear_program = self._check_is_compiled() - transaction_parameters = _convert_transaction_parameters(transaction_parameters) - - extra_pages = transaction_parameters.extra_pages or num_extra_program_pages( - approval_program.raw_binary, clear_program.raw_binary - ) - - self.add_method_call( - atc, - app_id=0, - abi_method=call_abi_method, - abi_args=abi_kwargs, - on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC, - call_config=au_spec.CallConfig.CREATE, - parameters=transaction_parameters, - approval_program=approval_program.raw_binary, - clear_program=clear_program.raw_binary, - global_schema=self.app_spec.global_state_schema, - local_schema=self.app_spec.local_state_schema, - extra_pages=extra_pages, - ) - - @overload - def create( - self, - call_abi_method: Literal[False], - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def create( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def create( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def create( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" - - atc = AtomicTransactionComposer() - - self.compose_create( - atc, - call_abi_method, - transaction_parameters, - **abi_kwargs, - ) - create_result = self._execute_atc_tr(atc) - self.app_id = au_deploy.get_app_id_from_tx_id(self.algod_client, create_result.tx_id) - return create_result - - def compose_update( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=UpdateApplication to atc""" - approval_program, clear_program = self._check_is_compiled() - - self.add_method_call( - atc=atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.UpdateApplicationOC, - approval_program=approval_program.raw_binary, - clear_program=clear_program.raw_binary, - ) - - @overload - def update( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def update( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def update( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def update( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=UpdateApplication""" - - atc = AtomicTransactionComposer() - self.compose_update( - atc, - call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_delete( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=DeleteApplication to atc""" - - self.add_method_call( - atc, - call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.DeleteApplicationOC, - ) - - @overload - def delete( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def delete( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def delete( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def delete( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=DeleteApplication""" - - atc = AtomicTransactionComposer() - self.compose_delete( - atc, - call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_call( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with specified parameters to atc""" - _parameters = _convert_transaction_parameters(transaction_parameters) - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=_parameters, - on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC, - ) - - @overload - def call( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def call( - self, - call_abi_method: Literal[False], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def call( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def call( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with specified parameters""" - atc = AtomicTransactionComposer() - _parameters = _convert_transaction_parameters(transaction_parameters) - self.compose_call( - atc, - call_abi_method=call_abi_method, - transaction_parameters=_parameters, - **abi_kwargs, - ) - - method = self._resolve_method( - call_abi_method, abi_kwargs, _parameters.on_complete or transaction.OnComplete.NoOpOC - ) - if method: - hints = self._method_hints(method) - if hints and hints.read_only: - if config.debug and config.project_root and config.trace_all: - simulate_and_persist_response( - atc, config.project_root, self.algod_client, config.trace_buffer_size_mb - ) - - return self._simulate_readonly_call(method, atc) - - return self._execute_atc_tr(atc) - - def compose_opt_in( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=OptIn to atc""" - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.OptInOC, - ) - - @overload - def opt_in( - self, - call_abi_method: ABIMethod | Literal[True] = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def opt_in( - self, - call_abi_method: Literal[False] = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - ) -> TransactionResponse: ... - - @overload - def opt_in( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def opt_in( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=OptIn""" - atc = AtomicTransactionComposer() - self.compose_opt_in( - atc, - call_abi_method=call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_close_out( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=CloseOut to ac""" - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.CloseOutOC, - ) - - @overload - def close_out( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def close_out( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def close_out( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def close_out( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=CloseOut""" - atc = AtomicTransactionComposer() - self.compose_close_out( - atc, - call_abi_method=call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_clear_state( - self, - atc: AtomicTransactionComposer, - /, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - app_args: list[bytes] | None = None, - ) -> None: - """Adds a signed transaction with on_complete=ClearState to atc""" - return self.add_method_call( - atc, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.ClearStateOC, - app_args=app_args, - ) - - def clear_state( - self, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - app_args: list[bytes] | None = None, - ) -> TransactionResponse: - """Submits a signed transaction with on_complete=ClearState""" - atc = AtomicTransactionComposer() - self.compose_clear_state( - atc, - transaction_parameters=transaction_parameters, - app_args=app_args, - ) - return self._execute_atc_tr(atc) - - def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: - """Gets the global state info associated with app_id""" - global_state = self.algod_client.application_info(self.app_id) - assert isinstance(global_state, dict) - return cast( - dict[bytes | str, bytes | str | int], - _decode_state(global_state.get("params", {}).get("global-state", {}), raw=raw), - ) - - def get_local_state(self, account: str | None = None, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: - """Gets the local state info for associated app_id and account/sender""" - - if account is None: - _, account = self.resolve_signer_sender(self.signer, self.sender) - - acct_state = self.algod_client.account_application_info(account, self.app_id) - assert isinstance(acct_state, dict) - return cast( - dict[bytes | str, bytes | str | int], - _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), - ) - - def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes: - """Resolves the default value for an ABI method, based on app_spec""" - - def _data_check(value: object) -> int | str | bytes: - if isinstance(value, int | str | bytes): - return value - raise ValueError(f"Unexpected type for constant data: {value}") - - match to_resolve: - case {"source": "constant", "data": data}: - return _data_check(data) - case {"source": "global-state", "data": str() as key}: - global_state = self.get_global_state(raw=True) - return global_state[key.encode()] - case {"source": "local-state", "data": str() as key}: - _, sender = self.resolve_signer_sender(self.signer, self.sender) - acct_state = self.get_local_state(sender, raw=True) - return acct_state[key.encode()] - case {"source": "abi-method", "data": dict() as method_dict}: - method = Method.undictify(method_dict) - response = self.call(method) - assert isinstance(response, ABITransactionResponse) - return _data_check(response.return_value) - - case {"source": source}: - raise ValueError(f"Unrecognized default argument source: {source}") - case _: - raise TypeError("Unable to interpret default argument specification") - - def _get_app_deploy_metadata( - self, version: str | None, allow_update: bool | None, allow_delete: bool | None - ) -> au_deploy.AppDeployMetaData: - updatable = ( - allow_update - if allow_update is not None - else au_deploy.get_deploy_control( - self.app_spec, au_deploy.UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC - ) - ) - deletable = ( - allow_delete - if allow_delete is not None - else au_deploy.get_deploy_control( - self.app_spec, au_deploy.DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC - ) - ) - - app = self._load_app_reference() - - if version is None: - if app.app_id == 0: - version = "v1.0" - else: - assert isinstance(app, au_deploy.AppDeployMetaData) - version = get_next_version(app.version) - return au_deploy.AppDeployMetaData(self.app_name, version, updatable=updatable, deletable=deletable) - - def _check_is_compiled(self) -> tuple[Program, Program]: - if self._approval_program is None or self._clear_program is None: - self._approval_program, self._clear_program = substitute_template_and_compile( - self.algod_client, self.app_spec, self.template_values - ) - - if config.debug and config.project_root: - persist_sourcemaps( - sources=[ - PersistSourceMapInput( - compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" - ), - PersistSourceMapInput( - compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" - ), - ], - project_root=config.project_root, - client=self.algod_client, - with_sources=True, - ) - - return self._approval_program, self._clear_program - - def _simulate_readonly_call( - self, method: Method, atc: AtomicTransactionComposer - ) -> ABITransactionResponse | TransactionResponse: - response = simulate_response(atc, self.algod_client) - traces = None - if config.debug: - traces = _create_simulate_traces(response) - if response.failure_message: - raise _try_convert_to_logic_error( - response.failure_message, - self.app_spec.approval_program, - self._get_approval_source_map, - traces, - ) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}") - - return TransactionResponse.from_atr(response) - - def _load_reference_and_check_app_id(self) -> None: - self._load_app_reference() - self._check_app_id() - - def _load_app_reference(self) -> au_deploy.AppReference | au_deploy.AppMetaData: - if not self.existing_deployments and self._creator: - assert self._indexer_client - self.existing_deployments = au_deploy.get_creator_apps(self._indexer_client, self._creator) - - if self.existing_deployments: - app = self.existing_deployments.apps.get(self.app_name) - if app: - if self.app_id == 0: - self.app_id = app.app_id - return app - - return au_deploy.AppReference(self.app_id, self.app_address) - - def _check_app_id(self) -> None: - if self.app_id == 0: - raise Exception( - "ApplicationClient is not associated with an app instance, to resolve either:\n" - "1.) provide an app_id on construction OR\n" - "2.) provide a creator address so an app can be searched for OR\n" - "3.) create an app first using create or deploy methods" - ) - - def _resolve_method( - self, - abi_method: ABIMethod | bool | None, - args: ABIArgsDict | None, - on_complete: transaction.OnComplete, - call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, - ) -> Method | None: - matches: list[Method | None] = [] - match abi_method: - case str() | Method(): # abi method specified - return self._resolve_abi_method(abi_method) - case bool() | None: # find abi method - has_bare_config = ( - call_config in au_deploy.get_call_config(self.app_spec.bare_call_config, on_complete) - or on_complete == transaction.OnComplete.ClearStateOC - ) - abi_methods = self._find_abi_methods(args, on_complete, call_config) - if abi_method is not False: - matches += abi_methods - if has_bare_config and abi_method is not True: - matches += [None] - case _: - return abi_method.method_spec() - - if len(matches) == 1: # exact match - return matches[0] - elif len(matches) > 1: # ambiguous match - signatures = ", ".join((m.get_signature() if isinstance(m, Method) else "bare") for m in matches) - raise Exception( - f"Could not find an exact method to use for {on_complete.name} with call_config of {call_config.name}, " - f"specify the exact method using abi_method and args parameters, considered: {signatures}" - ) - else: # no match - raise Exception( - f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" - ) - - def _get_approval_source_map(self) -> SourceMap | None: - if self.approval_source_map: - return self.approval_source_map - - try: - approval, _ = self._check_is_compiled() - except au_deploy.DeploymentFailedError: - return None - return approval.source_map - - def export_source_map(self) -> str | None: - """Export approval source map to JSON, can be later re-imported with `import_source_map`""" - source_map = self._get_approval_source_map() - if source_map: - return json.dumps( - { - "version": source_map.version, - "sources": source_map.sources, - "mappings": source_map.mappings, - } - ) - return None - - def import_source_map(self, source_map_json: str) -> None: - """Import approval source from JSON exported by `export_source_map`""" - source_map = json.loads(source_map_json) - self._approval_source_map = SourceMap(source_map) - - def add_method_call( # noqa: PLR0913 - self, - atc: AtomicTransactionComposer, - abi_method: ABIMethod | bool | None = None, - *, - abi_args: ABIArgsDict | None = None, - app_id: int | None = None, - parameters: TransactionParameters | TransactionParametersDict | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - local_schema: transaction.StateSchema | None = None, - global_schema: transaction.StateSchema | None = None, - approval_program: bytes | None = None, - clear_program: bytes | None = None, - extra_pages: int | None = None, - app_args: list[bytes] | None = None, - call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, - ) -> None: - """Adds a transaction to the AtomicTransactionComposer passed""" - if app_id is None: - self._load_reference_and_check_app_id() - app_id = self.app_id - parameters = _convert_transaction_parameters(parameters) - method = self._resolve_method(abi_method, abi_args, on_complete, call_config) - sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() - signer, sender = self.resolve_signer_sender(parameters.signer, parameters.sender) - if parameters.boxes is not None: - # TODO: algosdk actually does this, but it's type hints say otherwise... - encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in parameters.boxes] - else: - encoded_boxes = None - - encoded_lease = parameters.lease.encode("utf-8") if isinstance(parameters.lease, str) else parameters.lease - - if not method: # not an abi method, treat as a regular call - if abi_args: - raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}") - atc.add_transaction( - TransactionWithSigner( - txn=transaction.ApplicationCallTxn( # type: ignore[no-untyped-call] - sender=sender, - sp=sp, - index=app_id, - on_complete=on_complete, - approval_program=approval_program, - clear_program=clear_program, - global_schema=global_schema, - local_schema=local_schema, - extra_pages=extra_pages, - accounts=parameters.accounts, - foreign_apps=parameters.foreign_apps, - foreign_assets=parameters.foreign_assets, - boxes=encoded_boxes, - note=parameters.note, - lease=encoded_lease, - rekey_to=parameters.rekey_to, - app_args=app_args, - ), - signer=signer, - ) - ) - return - # resolve ABI method args - args = self._get_abi_method_args(abi_args, method) - atc.add_method_call( - app_id, - method, - sender, - sp, - signer, - method_args=args, - on_complete=on_complete, - local_schema=local_schema, - global_schema=global_schema, - approval_program=approval_program, - clear_program=clear_program, - extra_pages=extra_pages or 0, - accounts=parameters.accounts, - foreign_apps=parameters.foreign_apps, - foreign_assets=parameters.foreign_assets, - boxes=encoded_boxes, - note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, - lease=encoded_lease, - rekey_to=parameters.rekey_to, - ) - - def _get_abi_method_args(self, abi_args: ABIArgsDict | None, method: Method) -> list: - args: list = [] - hints = self._method_hints(method) - # copy args so we don't mutate original - abi_args = dict(abi_args or {}) - for method_arg in method.args: - name = method_arg.name - if name in abi_args: - argument = abi_args.pop(name) - if isinstance(argument, dict): - if hints.structs is None or name not in hints.structs: - raise Exception(f"Argument missing struct hint: {name}. Check argument name and type") - - elements = hints.structs[name]["elements"] - - argument_tuple = tuple(argument[field_name] for field_name, field_type in elements) - args.append(argument_tuple) - else: - args.append(argument) - - elif hints.default_arguments is not None and name in hints.default_arguments: - default_arg = hints.default_arguments[name] - if default_arg is not None: - args.append(self.resolve(default_arg)) - else: - raise Exception(f"Unspecified argument: {name}") - if abi_args: - raise Exception(f"Unused arguments specified: {', '.join(abi_args)}") - return args - - def _method_matches( - self, - method: Method, - args: ABIArgsDict | None, - on_complete: transaction.OnComplete, - call_config: au_spec.CallConfig, - ) -> bool: - hints = self._method_hints(method) - if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): - return False - method_args = {m.name for m in method.args} - provided_args = set(args or {}) | set(hints.default_arguments) - - # TODO: also match on types? - return method_args == provided_args - - def _find_abi_methods( - self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: au_spec.CallConfig - ) -> list[Method]: - return [ - method - for method in self.app_spec.contract.methods - if self._method_matches(method, args, on_complete, call_config) - ] - - def _resolve_abi_method(self, method: ABIMethod) -> Method: - if isinstance(method, str): - try: - return next(iter(m for m in self.app_spec.contract.methods if m.get_signature() == method)) - except StopIteration: - pass - return self.app_spec.contract.get_method_by_name(method) - elif hasattr(method, "method_spec"): - return method.method_spec() - else: - return method - - def _method_hints(self, method: Method) -> au_spec.MethodHints: - sig = method.get_signature() - if sig not in self.app_spec.hints: - return au_spec.MethodHints() - return self.app_spec.hints[sig] - - def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: - result = self.execute_atc(atc) - return TransactionResponse.from_atr(result) - - def execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: - return execute_atc_with_logic_error( - atc, - self.algod_client, - approval_program=self.app_spec.approval_program, - approval_source_map=self._get_approval_source_map, - ) - - def get_signer_sender( - self, signer: TransactionSigner | None = None, sender: str | None = None - ) -> tuple[TransactionSigner | None, str | None]: - """Return signer and sender, using default values on client if not specified - - Will use provided values if given, otherwise will fall back to values defined on client. - If no sender is specified then will attempt to obtain sender from signer""" - resolved_signer = signer or self.signer - resolved_sender = sender or get_sender_from_signer(signer) or self.sender or get_sender_from_signer(self.signer) - return resolved_signer, resolved_sender - - def resolve_signer_sender( - self, signer: TransactionSigner | None = None, sender: str | None = None - ) -> tuple[TransactionSigner, str]: - """Return signer and sender, using default values on client if not specified - - Will use provided values if given, otherwise will fall back to values defined on client. - If no sender is specified then will attempt to obtain sender from signer - - :raises ValueError: Raised if a signer or sender is not provided. See `get_signer_sender` - for variant with no exception""" - resolved_signer, resolved_sender = self.get_signer_sender(signer, sender) - if not resolved_signer: - raise ValueError("No signer provided") - if not resolved_sender: - raise ValueError("No sender provided") - return resolved_signer, resolved_sender - - # TODO: remove private implementation, kept in the 1.0.2 release to not impact existing beaker 1.0 installs - _resolve_signer_sender = resolve_signer_sender - - -def substitute_template_and_compile( - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification, - template_values: au_deploy.TemplateValueMapping, -) -> tuple[Program, Program]: - """Substitutes the provided template_values into app_spec and compiles""" - template_values = dict(template_values or {}) - clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values) - - au_deploy.check_template_variables(app_spec.approval_program, template_values) - approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values) - - approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client) - - return approval_app, clear_app - - -def get_next_version(current_version: str) -> str: - """Calculates the next version from `current_version` - - Next version is calculated by finding a semver like - version string and incrementing the lower. This function is used by {py:meth}`ApplicationClient.deploy` when - a version is not specified, and is intended mostly for convenience during local development. - - :params str current_version: An existing version string with a semver like version contained within it, - some valid inputs and incremented outputs: - `1` -> `2` - `1.0` -> `1.1` - `v1.1` -> `v1.2` - `v1.1-beta1` -> `v1.2-beta1` - `v1.2.3.4567` -> `v1.2.3.4568` - `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` - :raises DeploymentFailedError: If `current_version` cannot be parsed""" - pattern = re.compile(r"(?P\w*)(?P(?:\d+\.)*\d+)(?P\w*)") - match = pattern.match(current_version) - if match: - version = match.group("version") - new_version = _increment_version(version) - - def replacement(m: re.Match) -> str: - return f"{m.group('prefix')}{new_version}{m.group('suffix')}" - - return re.sub(pattern, replacement, current_version) - raise au_deploy.DeploymentFailedError( - f"Could not auto increment {current_version}, please specify the next version using the version parameter" - ) - - -def _try_convert_to_logic_error( - source_ex: Exception | str, - approval_program: str, - approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, - simulate_traces: list[SimulationTrace] | None = None, -) -> Exception | None: - source_ex_str = str(source_ex) - logic_error_data = parse_logic_error(source_ex_str) - if logic_error_data: - return LogicError( - logic_error_str=source_ex_str, - logic_error=source_ex if isinstance(source_ex, Exception) else None, - program=approval_program, - source_map=approval_source_map() if callable(approval_source_map) else approval_source_map, - **logic_error_data, - traces=simulate_traces, - ) - - return None - - -def execute_atc_with_logic_error( - atc: AtomicTransactionComposer, - algod_client: "AlgodClient", - approval_program: str, - wait_rounds: int = 4, - approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, -) -> AtomicTransactionResponse: - """Calls {py:meth}`AtomicTransactionComposer.execute` on provided `atc`, but will parse any errors - and raise a {py:class}`LogicError` if possible - - ```{note} - `approval_program` and `approval_source_map` are required to be able to parse any errors into a - {py:class}`LogicError` - ``` - """ - try: - if config.debug and config.project_root and config.trace_all: - simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb) - - return atc.execute(algod_client, wait_rounds=wait_rounds) - except Exception as ex: - if config.debug: - simulate = None - if config.project_root and not config.trace_all: - # if trace_all is enabled, we already have the traces executed above - # hence we only need to simulate if trace_all is disabled and - # project_root is set - simulate = simulate_and_persist_response( - atc, config.project_root, algod_client, config.trace_buffer_size_mb - ) - else: - simulate = simulate_response(atc, algod_client) - traces = _create_simulate_traces(simulate) - else: - traces = None - logger.info("An error occurred while executing the transaction.") - logger.info("To see more details, enable debug mode by setting config.debug = True ") - - logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces) - if logic_error: - raise logic_error from ex - raise ex - - -def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: - traces = [] - if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: - for txn_group in simulate.simulate_response["txn-groups"]: - app_budget_added = txn_group.get("app-budget-added", None) - app_budget_consumed = txn_group.get("app-budget-consumed", None) - failure_message = txn_group.get("failure-message", None) - txn_result = txn_group.get("txn-results", [{}])[0] - exec_trace = txn_result.get("exec-trace", {}) - traces.append( - SimulationTrace( - app_budget_added=app_budget_added, - app_budget_consumed=app_budget_consumed, - failure_message=failure_message, - exec_trace=exec_trace, - ) - ) - return traces - - -def _convert_transaction_parameters( - args: TransactionParameters | TransactionParametersDict | None, -) -> CreateCallParameters: - _args = args.__dict__ if isinstance(args, TransactionParameters) else (args or {}) - return CreateCallParameters(**_args) - - -def get_sender_from_signer(signer: TransactionSigner | None) -> str | None: - """Returns the associated address of a signer, return None if no address found""" - - if isinstance(signer, AccountTransactionSigner): - sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] - assert isinstance(sender, str) - return sender - elif isinstance(signer, MultisigTransactionSigner): - sender = signer.msig.address() # type: ignore[no-untyped-call] - assert isinstance(sender, str) - return sender - elif isinstance(signer, LogicSigTransactionSigner): - return signer.lsig.address() - return None - - -# TEMPORARY, use SDK one when available -def _parse_result( - methods: dict[int, Method], - txns: list[dict[str, Any]], - txids: list[str], -) -> list[ABIResult]: - method_results = [] - for i, tx_info in enumerate(txns): - raw_value = b"" - return_value = None - decode_error = None - - if i not in methods: - continue - - # Parse log for ABI method return value - try: - if methods[i].returns.type == Returns.VOID: - method_results.append( - ABIResult( - tx_id=txids[i], - raw_value=raw_value, - return_value=return_value, - decode_error=decode_error, - tx_info=tx_info, - method=methods[i], - ) - ) - continue - - logs = tx_info.get("logs", []) - - # Look for the last returned value in the log - if not logs: - raise Exception("No logs") - - result = logs[-1] - # Check that the first four bytes is the hash of "return" - result_bytes = base64.b64decode(result) - if len(result_bytes) < len(ABI_RETURN_HASH) or result_bytes[: len(ABI_RETURN_HASH)] != ABI_RETURN_HASH: - raise Exception("no logs") - - raw_value = result_bytes[4:] - abi_return_type = methods[i].returns.type - if isinstance(abi_return_type, ABIType): - return_value = abi_return_type.decode(raw_value) - else: - return_value = raw_value - - except Exception as e: - decode_error = e - - method_results.append( - ABIResult( - tx_id=txids[i], - raw_value=raw_value, - return_value=return_value, - decode_error=decode_error, - tx_info=tx_info, - method=methods[i], - ) - ) - - return method_results - - -def _increment_version(version: str) -> str: - split = list(map(int, version.split("."))) - split[-1] = split[-1] + 1 - return ".".join(str(x) for x in split) - - -def _str_or_hex(v: bytes) -> str: - decoded: str - try: - decoded = v.decode("utf-8") - except UnicodeDecodeError: - decoded = v.hex() - - return decoded - - -def _decode_state(state: list[dict[str, Any]], *, raw: bool = False) -> dict[str | bytes, bytes | str | int | None]: - decoded_state: dict[str | bytes, bytes | str | int | None] = {} - - for state_value in state: - raw_key = base64.b64decode(state_value["key"]) - - key: str | bytes = raw_key if raw else _str_or_hex(raw_key) - val: str | bytes | int | None - - action = state_value["value"]["action"] if "action" in state_value["value"] else state_value["value"]["type"] - - match action: - case 1: - raw_val = base64.b64decode(state_value["value"]["bytes"]) - val = raw_val if raw else _str_or_hex(raw_val) - case 2: - val = state_value["value"]["uint"] - case 3: - val = None - case _: - raise NotImplementedError - - decoded_state[key] = val - return decoded_state +from algokit_utils._legacy_v2.application_client import * # noqa: F403 diff --git a/src/algokit_utils/application_specification.py b/src/algokit_utils/application_specification.py index 392fce8d..56c286ee 100644 --- a/src/algokit_utils/application_specification.py +++ b/src/algokit_utils/application_specification.py @@ -1,206 +1 @@ -import base64 -import dataclasses -import json -from enum import IntFlag -from pathlib import Path -from typing import Any, Literal, TypeAlias, TypedDict - -from algosdk.abi import Contract -from algosdk.abi.method import MethodDict -from algosdk.transaction import StateSchema - -__all__ = [ - "CallConfig", - "DefaultArgumentDict", - "DefaultArgumentType", - "MethodConfigDict", - "OnCompleteActionName", - "MethodHints", - "ApplicationSpecification", - "AppSpecStateDict", -] - - -AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] -"""Type defining Application Specification state entries""" - - -class CallConfig(IntFlag): - """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" - - NEVER = 0 - """Never handle the specified on completion type""" - CALL = 1 - """Only handle the specified on completion type for application calls""" - CREATE = 2 - """Only handle the specified on completion type for application create calls""" - ALL = 3 - """Handle the specified on completion type for both create and normal application calls""" - - -class StructArgDict(TypedDict): - name: str - elements: list[list[str]] - - -OnCompleteActionName: TypeAlias = Literal[ - "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" -] -"""String literals representing on completion transaction types""" -MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] -"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" -DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] -"""Literal values describing the types of default argument sources""" - - -class DefaultArgumentDict(TypedDict): - """ - DefaultArgument is a container for any arguments that may - be resolved prior to calling some target method - """ - - source: DefaultArgumentType - data: int | str | bytes | MethodDict - - -StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword - "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} -) - - -@dataclasses.dataclass(kw_only=True) -class MethodHints: - """MethodHints provides hints to the caller about how to call the method""" - - #: hint to indicate this method can be called through Dryrun - read_only: bool = False - #: hint to provide names for tuple argument indices - #: method_name=>param_name=>{name:str, elements:[str,str]} - structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) - #: defaults - default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) - call_config: MethodConfigDict = dataclasses.field(default_factory=dict) - - def empty(self) -> bool: - return not self.dictify() - - def dictify(self) -> dict[str, Any]: - d: dict[str, Any] = {} - if self.read_only: - d["read_only"] = True - if self.default_arguments: - d["default_arguments"] = self.default_arguments - if self.structs: - d["structs"] = self.structs - if any(v for v in self.call_config.values() if v != CallConfig.NEVER): - d["call_config"] = _encode_method_config(self.call_config) - return d - - @staticmethod - def undictify(data: dict[str, Any]) -> "MethodHints": - return MethodHints( - read_only=data.get("read_only", False), - default_arguments=data.get("default_arguments", {}), - structs=data.get("structs", {}), - call_config=_decode_method_config(data.get("call_config", {})), - ) - - -def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: - return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} - - -def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: - return {k: CallConfig[v] for k, v in data.items()} - - -def _encode_source(teal_text: str) -> str: - return base64.b64encode(teal_text.encode()).decode("utf-8") - - -def _decode_source(b64_text: str) -> str: - return base64.b64decode(b64_text).decode("utf-8") - - -def _encode_state_schema(schema: StateSchema) -> dict[str, int]: - return { - "num_byte_slices": schema.num_byte_slices, - "num_uints": schema.num_uints, - } - - -def _decode_state_schema(data: dict[str, int]) -> StateSchema: - return StateSchema( # type: ignore[no-untyped-call] - num_byte_slices=data.get("num_byte_slices", 0), - num_uints=data.get("num_uints", 0), - ) - - -@dataclasses.dataclass(kw_only=True) -class ApplicationSpecification: - """ARC-0032 application specification - - See """ - - approval_program: str - clear_program: str - contract: Contract - hints: dict[str, MethodHints] - schema: StateDict - global_state_schema: StateSchema - local_state_schema: StateSchema - bare_call_config: MethodConfigDict - - def dictify(self) -> dict: - return { - "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, - "source": { - "approval": _encode_source(self.approval_program), - "clear": _encode_source(self.clear_program), - }, - "state": { - "global": _encode_state_schema(self.global_state_schema), - "local": _encode_state_schema(self.local_state_schema), - }, - "schema": self.schema, - "contract": self.contract.dictify(), - "bare_call_config": _encode_method_config(self.bare_call_config), - } - - def to_json(self) -> str: - return json.dumps(self.dictify(), indent=4) - - @staticmethod - def from_json(application_spec: str) -> "ApplicationSpecification": - json_spec = json.loads(application_spec) - return ApplicationSpecification( - approval_program=_decode_source(json_spec["source"]["approval"]), - clear_program=_decode_source(json_spec["source"]["clear"]), - schema=json_spec["schema"], - global_state_schema=_decode_state_schema(json_spec["state"]["global"]), - local_state_schema=_decode_state_schema(json_spec["state"]["local"]), - contract=Contract.undictify(json_spec["contract"]), - hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, - bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), - ) - - def export(self, directory: Path | str | None = None) -> None: - """write out the artifacts generated by the application to disk - - Args: - directory(optional): path to the directory where the artifacts should be written - """ - if directory is None: - output_dir = Path.cwd() - else: - output_dir = Path(directory) - output_dir.mkdir(exist_ok=True, parents=True) - - (output_dir / "approval.teal").write_text(self.approval_program) - (output_dir / "clear.teal").write_text(self.clear_program) - (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) - (output_dir / "application.json").write_text(self.to_json()) - - -def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] +from algokit_utils._legacy_v2.application_specification import * # noqa: F403 diff --git a/src/algokit_utils/applications/__init__.py b/src/algokit_utils/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/applications/models.py b/src/algokit_utils/applications/models.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/asset.py b/src/algokit_utils/asset.py index 085ea8c5..4d9c8522 100644 --- a/src/algokit_utils/asset.py +++ b/src/algokit_utils/asset.py @@ -1,168 +1 @@ -import logging -from typing import TYPE_CHECKING - -from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner -from algosdk.constants import TX_GROUP_LIMIT -from algosdk.transaction import AssetTransferTxn - -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - -from enum import Enum, auto - -from algokit_utils.models import Account - -__all__ = ["opt_in", "opt_out"] -logger = logging.getLogger(__name__) - - -class ValidationType(Enum): - OPTIN = auto() - OPTOUT = auto() - - -def _ensure_account_is_valid(algod_client: "AlgodClient", account: Account) -> None: - try: - algod_client.account_info(account.address) - except Exception as err: - error_message = f"Account address{account.address} does not exist" - logger.debug(error_message) - raise err - - -def _ensure_asset_balance_conditions( - algod_client: "AlgodClient", account: Account, asset_ids: list, validation_type: ValidationType -) -> None: - invalid_asset_ids = [] - account_info = algod_client.account_info(account.address) - account_assets = account_info.get("assets", []) # type: ignore # noqa: PGH003 - for asset_id in asset_ids: - asset_exists_in_account_info = any(asset["asset-id"] == asset_id for asset in account_assets) - if validation_type == ValidationType.OPTIN: - if asset_exists_in_account_info: - logger.debug(f"Asset {asset_id} is already opted in for account {account.address}") - invalid_asset_ids.append(asset_id) - - elif validation_type == ValidationType.OPTOUT: - if not account_assets or not asset_exists_in_account_info: - logger.debug(f"Account {account.address} does not have asset {asset_id}") - invalid_asset_ids.append(asset_id) - else: - asset_balance = next((asset["amount"] for asset in account_assets if asset["asset-id"] == asset_id), 0) - if asset_balance != 0: - logger.debug(f"Asset {asset_id} balance is not zero") - invalid_asset_ids.append(asset_id) - - if len(invalid_asset_ids) > 0: - action = "opted out" if validation_type == ValidationType.OPTOUT else "opted in" - condition_message = ( - "their amount is zero and that the account has" - if validation_type == ValidationType.OPTOUT - else "they are valid and that the account has not" - ) - - error_message = ( - f"Assets {invalid_asset_ids} cannot be {action}. Ensure that " - f"{condition_message} previously opted into them." - ) - raise ValueError(error_message) - - -def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: - """ - Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, - it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases - its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). - - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. - account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. - asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values - are the transaction IDs for opting-in to each asset. - """ - _ensure_account_is_valid(algod_client, account) - _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN) - suggested_params = algod_client.suggested_params() - result = {} - for i in range(0, len(asset_ids), TX_GROUP_LIMIT): - atc = AtomicTransactionComposer() - chunk = asset_ids[i : i + TX_GROUP_LIMIT] - for asset_id in chunk: - asset = algod_client.asset_info(asset_id) - xfer_txn = AssetTransferTxn( - sp=suggested_params, - sender=account.address, - receiver=account.address, - close_assets_to=None, - revocation_target=None, - amt=0, - note=f"opt in asset id ${asset_id}", - index=asset["index"], # type: ignore # noqa: PGH003 - rekey_to=None, - ) - - transaction_with_signer = TransactionWithSigner( - txn=xfer_txn, - signer=account.signer, - ) - atc.add_transaction(transaction_with_signer) - atc.execute(algod_client, 4) - - for index, asset_id in enumerate(chunk): - result[asset_id] = atc.tx_ids[index] - - return result - - -def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: - """ - Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. - The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) - The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. - - It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero. - - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. - account (Account): An instance of the Account class that holds the private key and address for an account. - asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of - the executed transactions. - - """ - _ensure_account_is_valid(algod_client, account) - _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT) - suggested_params = algod_client.suggested_params() - result = {} - for i in range(0, len(asset_ids), TX_GROUP_LIMIT): - atc = AtomicTransactionComposer() - chunk = asset_ids[i : i + TX_GROUP_LIMIT] - for asset_id in chunk: - asset = algod_client.asset_info(asset_id) - asset_creator = asset["params"]["creator"] # type: ignore # noqa: PGH003 - xfer_txn = AssetTransferTxn( - sp=suggested_params, - sender=account.address, - receiver=account.address, - close_assets_to=asset_creator, - revocation_target=None, - amt=0, - note=f"opt out asset id ${asset_id}", - index=asset["index"], # type: ignore # noqa: PGH003 - rekey_to=None, - ) - - transaction_with_signer = TransactionWithSigner( - txn=xfer_txn, - signer=account.signer, - ) - atc.add_transaction(transaction_with_signer) - atc.execute(algod_client, 4) - - for index, asset_id in enumerate(chunk): - result[asset_id] = atc.tx_ids[index] - - return result +from algokit_utils._legacy_v2.asset import * # noqa: F403 diff --git a/src/algokit_utils/assets/__init__.py b/src/algokit_utils/assets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/assets/models.py b/src/algokit_utils/assets/models.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/clients/__init__.py b/src/algokit_utils/clients/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/beta/algorand_client.py b/src/algokit_utils/clients/algorand_client.py similarity index 96% rename from src/algokit_utils/beta/algorand_client.py rename to src/algokit_utils/clients/algorand_client.py index e80dadaf..7e02e20a 100644 --- a/src/algokit_utils/beta/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -4,10 +4,21 @@ from dataclasses import dataclass from typing import Any -from algokit_utils.beta.account_manager import AccountManager -from algokit_utils.beta.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.beta.composer import ( - AlgokitComposer, +from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner +from algosdk.transaction import SuggestedParams, Transaction, wait_for_confirmation +from typing_extensions import Self + +from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager +from algokit_utils.network_clients import ( + AlgoClientConfigs, + get_algod_client, + get_algonode_config, + get_default_localnet_config, + get_indexer_client, + get_kmd_client, +) +from algokit_utils.transactions.transaction_composer import ( AppCallParams, AssetConfigParams, AssetCreateParams, @@ -18,18 +29,8 @@ MethodCallParams, OnlineKeyRegParams, PayParams, + TransactionComposer, ) -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client, -) -from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner -from algosdk.transaction import SuggestedParams, Transaction, wait_for_confirmation -from typing_extensions import Self __all__ = [ "AlgorandClient", @@ -176,9 +177,9 @@ def account(self) -> AccountManager: """Get or create accounts that can sign transactions.""" return self._account_manager - def new_group(self) -> AlgokitComposer: - """Start a new `AlgokitComposer` transaction group""" - return AlgokitComposer( + def new_group(self) -> TransactionComposer: + """Start a new `TransactionComposer` transaction group""" + return TransactionComposer( algod=self.client.algod, get_signer=lambda addr: self.account.get_signer(addr), get_suggested_params=self.get_suggested_params, diff --git a/src/algokit_utils/beta/client_manager.py b/src/algokit_utils/clients/client_manager.py similarity index 73% rename from src/algokit_utils/beta/client_manager.py rename to src/algokit_utils/clients/client_manager.py index 1069eacf..16108520 100644 --- a/src/algokit_utils/beta/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,21 +1,18 @@ import algosdk -from algokit_utils.dispenser_api import TestNetDispenserApiClient -from algokit_utils.network_clients import AlgoClientConfigs, get_algod_client, get_indexer_client, get_kmd_client from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient +from algokit_utils.network_clients import ( + AlgoClientConfigs, + get_algod_client, + get_indexer_client, + get_kmd_client, +) -class AlgoSdkClients: - """ - Clients from algosdk that interact with the official Algorand APIs. - - Attributes: - algod (AlgodClient): Algod client, see https://developer.algorand.org/docs/rest-apis/algod/ - indexer (Optional[IndexerClient]): Optional indexer client, see https://developer.algorand.org/docs/rest-apis/indexer/ - kmd (Optional[KMDClient]): Optional KMD client, see https://developer.algorand.org/docs/rest-apis/kmd/ - """ +class AlgoSdkClients: def __init__( self, algod: algosdk.v2client.algod.AlgodClient, @@ -28,13 +25,6 @@ def __init__( class ClientManager: - """ - Exposes access to various API clients. - - Args: - clients_or_config (Union[AlgoConfig, AlgoSdkClients]): algosdk clients or config for interacting with the official Algorand APIs. - """ - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs diff --git a/src/algokit_utils/clients/dispenser_api_client.py b/src/algokit_utils/clients/dispenser_api_client.py new file mode 100644 index 00000000..66593e80 --- /dev/null +++ b/src/algokit_utils/clients/dispenser_api_client.py @@ -0,0 +1,178 @@ +import contextlib +import enum +import logging +import os +from dataclasses import dataclass + +import httpx + +logger = logging.getLogger(__name__) + + +class DispenserApiConfig: + BASE_URL = "https://api.dispenser.algorandfoundation.tools" + + +class DispenserAssetName(enum.IntEnum): + ALGO = 0 + + +@dataclass +class DispenserAsset: + asset_id: int + decimals: int + description: str + + +@dataclass +class DispenserFundResponse: + tx_id: str + amount: int + + +@dataclass +class DispenserLimitResponse: + amount: int + + +DISPENSER_ASSETS = { + DispenserAssetName.ALGO: DispenserAsset( + asset_id=0, + decimals=6, + description="Algo", + ), +} +DISPENSER_REQUEST_TIMEOUT = 15 +DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN" + + +class TestNetDispenserApiClient: + """ + Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). + To get started create a new access token via `algokit dispenser login --ci` + and pass it to the client constructor as `auth_token`. + Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, + and it will be auto loaded. If both are set, the constructor argument takes precedence. + + Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. + """ + + auth_token: str + request_timeout = DISPENSER_REQUEST_TIMEOUT + + def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT): + auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY) + + if auth_token: + self.auth_token = auth_token + elif auth_token_from_env: + self.auth_token = auth_token_from_env + else: + raise Exception( + f"Can't init AlgoKit TestNet Dispenser API client " + f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or " + "the auth_token were provided." + ) + + self.request_timeout = request_timeout + + def _process_dispenser_request( + self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST" + ) -> httpx.Response: + """ + Generalized method to process http requests to dispenser API + """ + + headers = {"Authorization": f"Bearer {(auth_token)}"} + + # Set request arguments + request_args = { + "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}", + "headers": headers, + "timeout": self.request_timeout, + } + + if method.upper() != "GET" and data is not None: + request_args["json"] = data + + try: + response: httpx.Response = getattr(httpx, method.lower())(**request_args) + response.raise_for_status() + return response + + except httpx.HTTPStatusError as err: + error_message = f"Error processing dispenser API request: {err.response.status_code}" + error_response = None + with contextlib.suppress(Exception): + error_response = err.response.json() + + if error_response and error_response.get("code"): + error_message = error_response.get("code") + + elif err.response.status_code == httpx.codes.BAD_REQUEST: + error_message = err.response.json()["message"] + + raise Exception(error_message) from err + + except Exception as err: + error_message = "Error processing dispenser API request" + logger.debug(f"{error_message}: {err}", exc_info=True) + raise err + + def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse: + """ + Fund an account with Algos from the dispenser API + """ + + try: + response = self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix=f"fund/{asset_id}", + data={"receiver": address, "amount": amount, "assetID": asset_id}, + method="POST", + ) + + content = response.json() + return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"]) + + except Exception as err: + logger.exception(f"Error funding account {address}: {err}") + raise err + + def refund(self, refund_txn_id: str) -> None: + """ + Register a refund for a transaction with the dispenser API + """ + + try: + self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix="refund", + data={"refundTransactionID": refund_txn_id}, + method="POST", + ) + + except Exception as err: + logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}") + raise err + + def get_limit( + self, + address: str, + ) -> DispenserLimitResponse: + """ + Get current limit for an account with Algos from the dispenser API + """ + + try: + response = self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit", + method="GET", + ) + content = response.json() + + return DispenserLimitResponse(amount=content["amount"]) + except Exception as err: + logger.exception(f"Error setting limit for account {address}: {err}") + raise err diff --git a/src/algokit_utils/clients/models.py b/src/algokit_utils/clients/models.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/common.py b/src/algokit_utils/common.py index 8071c98f..45c54a87 100644 --- a/src/algokit_utils/common.py +++ b/src/algokit_utils/common.py @@ -1,28 +1 @@ -""" -This module contains common classes and methods that are reused in more than one file. -""" - -import base64 -import typing - -from algosdk.source_map import SourceMap - -from algokit_utils import deploy - -if typing.TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - - -class Program: - """A compiled TEAL program""" - - def __init__(self, program: str, client: "AlgodClient"): - """ - Fully compile the program source to binary and generate a - source map for matching pc to line number - """ - self.teal = program - result: dict = client.compile(deploy.strip_comments(self.teal), source_map=True) - self.raw_binary = base64.b64decode(result["result"]) - self.binary_hash: str = result["hash"] - self.source_map = SourceMap(result["sourcemap"]) +from algokit_utils._legacy_v2.common import * # noqa: F403 diff --git a/src/algokit_utils/deploy.py b/src/algokit_utils/deploy.py index bb01c4f2..7543c6c1 100644 --- a/src/algokit_utils/deploy.py +++ b/src/algokit_utils/deploy.py @@ -1,897 +1 @@ -import base64 -import dataclasses -import json -import logging -import re -from collections.abc import Iterable, Mapping, Sequence -from enum import Enum -from typing import TYPE_CHECKING, TypeAlias, TypedDict - -from algosdk import transaction -from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner -from algosdk.logic import get_application_address -from algosdk.transaction import StateSchema - -from algokit_utils.application_specification import ( - ApplicationSpecification, - CallConfig, - MethodConfigDict, - OnCompleteActionName, -) -from algokit_utils.models import ( - ABIArgsDict, - ABIMethod, - Account, - CreateCallParameters, - TransactionResponse, -) - -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient - - from algokit_utils.application_client import ApplicationClient - - -__all__ = [ - "UPDATABLE_TEMPLATE_NAME", - "DELETABLE_TEMPLATE_NAME", - "NOTE_PREFIX", - "ABICallArgs", - "ABICreateCallArgs", - "ABICallArgsDict", - "ABICreateCallArgsDict", - "DeploymentFailedError", - "AppReference", - "AppDeployMetaData", - "AppMetaData", - "AppLookup", - "DeployCallArgs", - "DeployCreateCallArgs", - "DeployCallArgsDict", - "DeployCreateCallArgsDict", - "Deployer", - "DeployResponse", - "OnUpdate", - "OnSchemaBreak", - "OperationPerformed", - "TemplateValueDict", - "TemplateValueMapping", - "get_app_id_from_tx_id", - "get_creator_apps", - "replace_template_variables", -] - -logger = logging.getLogger(__name__) - -DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT = 1000 -_UPDATABLE = "UPDATABLE" -_DELETABLE = "DELETABLE" -UPDATABLE_TEMPLATE_NAME = f"TMPL_{_UPDATABLE}" -"""Template variable name used to control if a smart contract is updatable or not at deployment""" -DELETABLE_TEMPLATE_NAME = f"TMPL_{_DELETABLE}" -"""Template variable name used to control if a smart contract is deletable or not at deployment""" -_TOKEN_PATTERN = re.compile(r"TMPL_[A-Z_]+") -TemplateValue: TypeAlias = int | str | bytes -TemplateValueDict: TypeAlias = dict[str, TemplateValue] -"""Dictionary of `dict[str, int | str | bytes]` representing template variable names and values""" -TemplateValueMapping: TypeAlias = Mapping[str, TemplateValue] -"""Mapping of `str` to `int | str | bytes` representing template variable names and values""" - -NOTE_PREFIX = "ALGOKIT_DEPLOYER:j" -"""ARC-0002 compliant note prefix for algokit_utils deployed applications""" -# This prefix is also used to filter for parsable transaction notes in get_creator_apps. -# However, as the note is base64 encoded first we need to consider it's base64 representation. -# When base64 encoding bytes, 3 bytes are stored in every 4 characters. -# So then we don't need to worry about the padding/changing characters of the prefix if it was followed by -# additional characters, assert the NOTE_PREFIX length is a multiple of 3. -assert len(NOTE_PREFIX) % 3 == 0 - - -class DeploymentFailedError(Exception): - pass - - -@dataclasses.dataclass -class AppReference: - """Information about an Algorand app""" - - app_id: int - app_address: str - - -@dataclasses.dataclass -class AppDeployMetaData: - """Metadata about an application stored in a transaction note during creation. - - The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field - as part of {py:meth}`ApplicationClient.deploy` - """ - - name: str - version: str - deletable: bool | None - updatable: bool | None - - @staticmethod - def from_json(value: str) -> "AppDeployMetaData": - json_value: dict = json.loads(value) - json_value.setdefault("deletable", None) - json_value.setdefault("updatable", None) - return AppDeployMetaData(**json_value) - - @classmethod - def from_b64(cls: type["AppDeployMetaData"], b64: str) -> "AppDeployMetaData": - return cls.decode(base64.b64decode(b64)) - - @classmethod - def decode(cls: type["AppDeployMetaData"], value: bytes) -> "AppDeployMetaData": - note = value.decode("utf-8") - assert note.startswith(NOTE_PREFIX) - return cls.from_json(note[len(NOTE_PREFIX) :]) - - def encode(self) -> bytes: - json_str = json.dumps(self.__dict__) - return f"{NOTE_PREFIX}{json_str}".encode() - - -@dataclasses.dataclass -class AppMetaData(AppReference, AppDeployMetaData): - """Metadata about a deployed app""" - - created_round: int - updated_round: int - created_metadata: AppDeployMetaData - deleted: bool - - -@dataclasses.dataclass -class AppLookup: - """Cache of {py:class}`AppMetaData` for a specific `creator` - - Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple - apps or discovering multiple app_ids - """ - - creator: str - apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) - - -def _sort_by_round(txn: dict) -> tuple[int, int]: - confirmed = txn["confirmed-round"] - offset = txn["intra-round-offset"] - return confirmed, offset - - -def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: - if not metadata_b64: - return None - # noinspection PyBroadException - try: - return AppDeployMetaData.from_b64(metadata_b64) - except Exception: - return None - - -def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: - """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified - creator that have a transaction note containing {py:class}`AppDeployMetaData` - """ - apps: dict[str, AppMetaData] = {} - - creator_address = creator_account if isinstance(creator_account, str) else creator_account.address - token = None - # TODO: paginated indexer call instead of N + 1 calls - while True: - response = indexer.lookup_account_application_by_creator( - creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token - ) # type: ignore[no-untyped-call] - if "message" in response: # an error occurred - raise Exception(f"Error querying applications for {creator_address}: {response}") - for app in response["applications"]: - app_id = app["id"] - app_created_at_round = app["created-at-round"] - app_deleted = app.get("deleted", False) - search_transactions_response = indexer.search_transactions( - min_round=app_created_at_round, - txn_type="appl", - application_id=app_id, - address=creator_address, - address_role="sender", - note_prefix=NOTE_PREFIX.encode("utf-8"), - ) # type: ignore[no-untyped-call] - transactions: list[dict] = search_transactions_response["transactions"] - if not transactions: - continue - - created_transaction = next( - t - for t in transactions - if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address - ) - - transactions.sort(key=_sort_by_round, reverse=True) - latest_transaction = transactions[0] - app_updated_at_round = latest_transaction["confirmed-round"] - - create_metadata = _parse_note(created_transaction.get("note")) - update_metadata = _parse_note(latest_transaction.get("note")) - - if create_metadata and create_metadata.name: - apps[create_metadata.name] = AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - created_metadata=create_metadata, - created_round=app_created_at_round, - **(update_metadata or create_metadata).__dict__, - updated_round=app_updated_at_round, - deleted=app_deleted, - ) - - token = response.get("next-token") - if not token: - break - - return AppLookup(creator_address, apps) - - -def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] - - -def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: - if to_schema.num_uints > from_schema.num_uints: - yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}" - if to_schema.num_byte_slices > from_schema.num_byte_slices: - yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}" - - -@dataclasses.dataclass(kw_only=True) -class AppChanges: - app_updated: bool - schema_breaking_change: bool - schema_change_description: str | None - - -def check_for_app_changes( # noqa: PLR0913 - algod_client: "AlgodClient", - *, - new_approval: bytes, - new_clear: bytes, - new_global_schema: StateSchema, - new_local_schema: StateSchema, - app_id: int, -) -> AppChanges: - application_info = algod_client.application_info(app_id) - assert isinstance(application_info, dict) - application_create_params = application_info["params"] - - current_approval = base64.b64decode(application_create_params["approval-program"]) - current_clear = base64.b64decode(application_create_params["clear-state-program"]) - current_global_schema = _state_schema(application_create_params["global-state-schema"]) - current_local_schema = _state_schema(application_create_params["local-state-schema"]) - - app_updated = current_approval != new_approval or current_clear != new_clear - - schema_changes: list[str] = [] - schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema)) - schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema)) - - return AppChanges( - app_updated=app_updated, - schema_breaking_change=bool(schema_changes), - schema_change_description=", ".join(schema_changes), - ) - - -def _is_valid_token_character(char: str) -> bool: - return char.isalnum() or char == "_" - - -def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: - result: list[str] = [] - match_count = 0 - token = f"TMPL_{template_variable}" - token_idx_offset = len(value) - len(token) - for line in program_lines: - comment_idx = _find_unquoted_string(line, "//") - if comment_idx is None: - comment_idx = len(line) - code = line[:comment_idx] - comment = line[comment_idx:] - trailing_idx = 0 - while True: - token_idx = _find_template_token(code, token, trailing_idx) - if token_idx is None: - break - - trailing_idx = token_idx + len(token) - prefix = code[:token_idx] - suffix = code[trailing_idx:] - code = f"{prefix}{value}{suffix}" - match_count += 1 - trailing_idx += token_idx_offset - result.append(code + comment) - return result, match_count - - -def add_deploy_template_variables( - template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None -) -> None: - if allow_update is not None: - template_values[_UPDATABLE] = int(allow_update) - if allow_delete is not None: - template_values[_DELETABLE] = int(allow_delete) - - -def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. - Returns None if not found""" - - if end < 0: - end = len(line) - idx = start - in_quotes = in_base64 = False - while idx < end: - current_char = line[idx] - match current_char: - # enter base64 - case " " | "(" if not in_quotes and _last_token_base64(line, idx): - in_base64 = True - # exit base64 - case " " | ")" if not in_quotes and in_base64: - in_base64 = False - # escaped char - case "\\" if in_quotes: - # skip next character - idx += 1 - # quote boundary - case '"': - in_quotes = not in_quotes - # can test for match - case _ if not in_quotes and not in_base64 and line.startswith(token, idx): - # only match if not in quotes and string matches - return idx - idx += 1 - return None - - -def _last_token_base64(line: str, idx: int) -> bool: - try: - *_, last = line[:idx].split() - except ValueError: - return False - return last in ("base64", "b64") - - -def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. - Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING - Returns None if not found""" - if end < 0: - end = len(line) - - idx = start - while idx < end: - token_idx = _find_unquoted_string(line, token, idx, end) - if token_idx is None: - break - trailing_idx = token_idx + len(token) - if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start - trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end - ): - return token_idx - idx = trailing_idx - return None - - -def _strip_comment(line: str) -> str: - comment_idx = _find_unquoted_string(line, "//") - if comment_idx is None: - return line - return line[:comment_idx].rstrip() - - -def strip_comments(program: str) -> str: - return "\n".join(_strip_comment(line) for line in program.splitlines()) - - -def _has_token(program_without_comments: str, token: str) -> bool: - for line in program_without_comments.splitlines(): - token_idx = _find_template_token(line, token) - if token_idx is not None: - return True - return False - - -def _find_tokens(stripped_approval_program: str) -> list[str]: - return _TOKEN_PATTERN.findall(stripped_approval_program) - - -def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: - approval_program = strip_comments(approval_program) - if _has_token(approval_program, UPDATABLE_TEMPLATE_NAME) and _UPDATABLE not in template_values: - raise DeploymentFailedError( - "allow_update must be specified if deploy time configuration of update is being used" - ) - if _has_token(approval_program, DELETABLE_TEMPLATE_NAME) and _DELETABLE not in template_values: - raise DeploymentFailedError( - "allow_delete must be specified if deploy time configuration of delete is being used" - ) - all_tokens = _find_tokens(approval_program) - missing_values = [token for token in all_tokens if token[len("TMPL_") :] not in template_values] - if missing_values: - raise DeploymentFailedError(f"The following template values were not provided: {', '.join(missing_values)}") - - for template_variable_name in template_values: - tmpl_variable = f"TMPL_{template_variable_name}" - if not _has_token(approval_program, tmpl_variable): - if template_variable_name == _UPDATABLE: - raise DeploymentFailedError( - "allow_update must only be specified if deploy time configuration of update is being used" - ) - if template_variable_name == _DELETABLE: - raise DeploymentFailedError( - "allow_delete must only be specified if deploy time configuration of delete is being used" - ) - logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") - - -def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: - """Replaces `TMPL_*` variables in `program` with `template_values` - - ```{note} - `template_values` keys should *NOT* be prefixed with `TMPL_` - ``` - """ - program_lines = program.splitlines() - for template_variable_name, template_value in template_values.items(): - match template_value: - case int(): - value = str(template_value) - case str(): - value = "0x" + template_value.encode("utf-8").hex() - case bytes(): - value = "0x" + template_value.hex() - case _: - raise DeploymentFailedError( - f"Unexpected template value type {template_variable_name}: {template_value.__class__}" - ) - - program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value) - - return "\n".join(program_lines) - - -def has_template_vars(app_spec: ApplicationSpecification) -> bool: - return "TMPL_" in strip_comments(app_spec.approval_program) or "TMPL_" in strip_comments(app_spec.clear_program) - - -def get_deploy_control( - app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete -) -> bool | None: - if template_var not in strip_comments(app_spec.approval_program): - return None - return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( - h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER - ) - - -def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: - def get(key: OnCompleteActionName) -> CallConfig: - return method_config.get(key, CallConfig.NEVER) - - match on_complete: - case transaction.OnComplete.NoOpOC: - return get("no_op") - case transaction.OnComplete.UpdateApplicationOC: - return get("update_application") - case transaction.OnComplete.DeleteApplicationOC: - return get("delete_application") - case transaction.OnComplete.OptInOC: - return get("opt_in") - case transaction.OnComplete.CloseOutOC: - return get("close_out") - case transaction.OnComplete.ClearStateOC: - return get("clear_state") - - -class OnUpdate(Enum): - """Action to take if an Application has been updated""" - - Fail = 0 - """Fail the deployment""" - UpdateApp = 1 - """Update the Application with the new approval and clear programs""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new application""" - - -class OnSchemaBreak(Enum): - """Action to take if an Application's schema has breaking changes""" - - Fail = 0 - """Fail the deployment""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new Application""" - - -class OperationPerformed(Enum): - """Describes the actions taken during deployment""" - - Nothing = 0 - """An existing Application was found""" - Create = 1 - """No existing Application was found, created a new Application""" - Update = 2 - """An existing Application was found, but was out of date, updated to latest version""" - Replace = 3 - """An existing Application was found, but was out of date, created a new Application and deleted the original""" - - -@dataclasses.dataclass(kw_only=True) -class DeployResponse: - """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`""" - - app: AppMetaData - create_response: TransactionResponse | None = None - delete_response: TransactionResponse | None = None - update_response: TransactionResponse | None = None - action_taken: OperationPerformed = OperationPerformed.Nothing - - -@dataclasses.dataclass(kw_only=True) -class DeployCallArgs: - """Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - suggested_params: transaction.SuggestedParams | None = None - lease: bytes | str | None = None - accounts: list[str] | None = None - foreign_apps: list[int] | None = None - foreign_assets: list[int] | None = None - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None - rekey_to: str | None = None - - -@dataclasses.dataclass(kw_only=True) -class ABICall: - method: ABIMethod | bool | None = None - args: ABIArgsDict = dataclasses.field(default_factory=dict) - - -@dataclasses.dataclass(kw_only=True) -class DeployCreateCallArgs(DeployCallArgs): - """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - extra_pages: int | None = None - on_complete: transaction.OnComplete | None = None - - -@dataclasses.dataclass(kw_only=True) -class ABICallArgs(DeployCallArgs, ABICall): - """ABI Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - -@dataclasses.dataclass(kw_only=True) -class ABICreateCallArgs(DeployCreateCallArgs, ABICall): - """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - -class DeployCallArgsDict(TypedDict, total=False): - """Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - suggested_params: transaction.SuggestedParams - lease: bytes | str - accounts: list[str] - foreign_apps: list[int] - foreign_assets: list[int] - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] - rekey_to: str - - -class ABICallArgsDict(DeployCallArgsDict, TypedDict, total=False): - """ABI Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - method: ABIMethod | bool - args: ABIArgsDict - - -class DeployCreateCallArgsDict(DeployCallArgsDict, TypedDict, total=False): - """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - extra_pages: int | None - on_complete: transaction.OnComplete - - -class ABICreateCallArgsDict(DeployCreateCallArgsDict, TypedDict, total=False): - """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - method: ABIMethod | bool - args: ABIArgsDict - - -@dataclasses.dataclass(kw_only=True) -class Deployer: - app_client: "ApplicationClient" - creator: str - signer: TransactionSigner - sender: str - existing_app_metadata_or_reference: AppReference | AppMetaData - new_app_metadata: AppDeployMetaData - on_update: OnUpdate - on_schema_break: OnSchemaBreak - create_args: ABICreateCallArgs | ABICreateCallArgsDict | DeployCreateCallArgs | None - update_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None - delete_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None - - def deploy(self) -> DeployResponse: - """Ensures app associated with app client's creator is present and up to date""" - assert self.app_client.approval - assert self.app_client.clear - - if self.existing_app_metadata_or_reference.app_id == 0: - logger.info(f"{self.new_app_metadata.name} not found in {self.creator} account, deploying app.") - return self._create_app() - - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - logger.debug( - f"{self.existing_app_metadata_or_reference.name} found in {self.creator} account, " - f"with app id {self.existing_app_metadata_or_reference.app_id}, " - f"version={self.existing_app_metadata_or_reference.version}." - ) - - app_changes = check_for_app_changes( - self.app_client.algod_client, - new_approval=self.app_client.approval.raw_binary, - new_clear=self.app_client.clear.raw_binary, - new_global_schema=self.app_client.app_spec.global_state_schema, - new_local_schema=self.app_client.app_spec.local_state_schema, - app_id=self.existing_app_metadata_or_reference.app_id, - ) - - if app_changes.schema_breaking_change: - logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") - return self._deploy_breaking_change() - - if app_changes.app_updated: - logger.info(f"Detected a TEAL update in app id {self.existing_app_metadata_or_reference.app_id}") - return self._deploy_update() - - logger.info("No detected changes in app, nothing to do.") - return DeployResponse(app=self.existing_app_metadata_or_reference) - - def _deploy_breaking_change(self) -> DeployResponse: - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - if self.on_schema_break == OnSchemaBreak.Fail: - raise DeploymentFailedError( - "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " - "If you want to try deleting and recreating the app then " - "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" - ) - if self.on_schema_break == OnSchemaBreak.AppendApp: - logger.info("Schema break detected and on_schema_break=AppendApp, will attempt to create new app") - return self._create_app() - - if self.existing_app_metadata_or_reference.deletable: - logger.info( - "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" - ) - elif self.existing_app_metadata_or_reference.deletable is False: - logger.warning( - "App is not deletable but on_schema_break=ReplaceApp, " - "will attempt to delete app, delete will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is deletable but on_schema_break=ReplaceApp, will attempt to delete app" - ) - return self._create_and_delete_app() - - def _deploy_update(self) -> DeployResponse: - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - if self.on_update == OnUpdate.Fail: - raise DeploymentFailedError( - "Update detected and on_update=Fail, stopping deployment. " - "If you want to try updating the app then re-run with on_update=UpdateApp" - ) - if self.on_update == OnUpdate.AppendApp: - logger.info("Update detected and on_update=AppendApp, will attempt to create new app") - return self._create_app() - elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.UpdateApp: - logger.info("App is updatable and on_update=UpdateApp, will update app") - return self._update_app() - elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.ReplaceApp: - logger.warning( - "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" - ) - return self._create_and_delete_app() - elif self.on_update == OnUpdate.ReplaceApp: - if self.existing_app_metadata_or_reference.updatable is False: - logger.warning( - "App is not updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - return self._create_and_delete_app() - else: - if self.existing_app_metadata_or_reference.updatable is False: - logger.warning( - "App is not updatable but on_update=UpdateApp, " - "will attempt to update app, update will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=UpdateApp, will attempt to update app" - ) - return self._update_app() - - def _create_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - - method, abi_args, parameters = _convert_deploy_args( - self.create_args, self.new_app_metadata, self.signer, self.sender - ) - create_response = self.app_client.create( - method, - parameters, - **abi_args, - ) - logger.info( - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " - f"with app id {self.app_client.app_id}." - ) - assert create_response.confirmed_round is not None - app_metadata = _create_metadata(self.new_app_metadata, self.app_client.app_id, create_response.confirmed_round) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - return DeployResponse(app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create) - - def _create_and_delete_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - - logger.info( - f"Replacing {self.existing_app_metadata_or_reference.name} " - f"({self.existing_app_metadata_or_reference.version}) with " - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) in {self.creator} account." - ) - atc = AtomicTransactionComposer() - create_method, create_abi_args, create_parameters = _convert_deploy_args( - self.create_args, self.new_app_metadata, self.signer, self.sender - ) - self.app_client.compose_create( - atc, - create_method, - create_parameters, - **create_abi_args, - ) - create_txn_index = len(atc.txn_list) - 1 - delete_method, delete_abi_args, delete_parameters = _convert_deploy_args( - self.delete_args, self.new_app_metadata, self.signer, self.sender - ) - self.app_client.compose_delete( - atc, - delete_method, - delete_parameters, - **delete_abi_args, - ) - delete_txn_index = len(atc.txn_list) - 1 - create_delete_response = self.app_client.execute_atc(atc) - create_response = TransactionResponse.from_atr(create_delete_response, create_txn_index) - delete_response = TransactionResponse.from_atr(create_delete_response, delete_txn_index) - self.app_client.app_id = get_app_id_from_tx_id(self.app_client.algod_client, create_response.tx_id) - logger.info( - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " - f"with app id {self.app_client.app_id}." - ) - logger.info( - f"{self.existing_app_metadata_or_reference.name} " - f"({self.existing_app_metadata_or_reference.version}) with app id " - f"{self.existing_app_metadata_or_reference.app_id}, deleted successfully." - ) - - app_metadata = _create_metadata( - self.new_app_metadata, self.app_client.app_id, create_delete_response.confirmed_round - ) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - - return DeployResponse( - app=app_metadata, - create_response=create_response, - delete_response=delete_response, - action_taken=OperationPerformed.Replace, - ) - - def _update_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - - logger.info( - f"Updating {self.existing_app_metadata_or_reference.name} to {self.new_app_metadata.version} in " - f"{self.creator} account, with app id {self.existing_app_metadata_or_reference.app_id}" - ) - method, abi_args, parameters = _convert_deploy_args( - self.update_args, self.new_app_metadata, self.signer, self.sender - ) - update_response = self.app_client.update( - method, - parameters, - **abi_args, - ) - app_metadata = _create_metadata( - self.new_app_metadata, - self.app_client.app_id, - self.existing_app_metadata_or_reference.created_round, - updated_round=update_response.confirmed_round, - original_metadata=self.existing_app_metadata_or_reference.created_metadata, - ) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - return DeployResponse(app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update) - - -def _create_metadata( - app_spec_note: AppDeployMetaData, - app_id: int, - created_round: int, - updated_round: int | None = None, - original_metadata: AppDeployMetaData | None = None, -) -> AppMetaData: - return AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - created_metadata=original_metadata or app_spec_note, - created_round=created_round, - updated_round=updated_round or created_round, - name=app_spec_note.name, - version=app_spec_note.version, - deletable=app_spec_note.deletable, - updatable=app_spec_note.updatable, - deleted=False, - ) - - -def _convert_deploy_args( - _args: DeployCallArgs | DeployCallArgsDict | None, - note: AppDeployMetaData, - signer: TransactionSigner | None, - sender: str | None, -) -> tuple[ABIMethod | bool | None, ABIArgsDict, CreateCallParameters]: - args = _args.__dict__ if isinstance(_args, DeployCallArgs) else dict(_args or {}) - - # return most derived type, unused parameters are ignored - parameters = CreateCallParameters( - note=note.encode(), - signer=signer, - sender=sender, - suggested_params=args.get("suggested_params"), - lease=args.get("lease"), - accounts=args.get("accounts"), - foreign_assets=args.get("foreign_assets"), - foreign_apps=args.get("foreign_apps"), - boxes=args.get("boxes"), - rekey_to=args.get("rekey_to"), - extra_pages=args.get("extra_pages"), - on_complete=args.get("on_complete"), - ) - - return args.get("method"), args.get("args") or {}, parameters - - -def get_app_id_from_tx_id(algod_client: "AlgodClient", tx_id: str) -> int: - """Finds the app_id for provided transaction id""" - result = algod_client.pending_transaction_info(tx_id) - assert isinstance(result, dict) - app_id = result["application-index"] - assert isinstance(app_id, int) - return app_id +from algokit_utils._legacy_v2.deploy import * # noqa: F403 diff --git a/src/algokit_utils/dispenser_api.py b/src/algokit_utils/dispenser_api.py index 66593e80..1dc9e175 100644 --- a/src/algokit_utils/dispenser_api.py +++ b/src/algokit_utils/dispenser_api.py @@ -1,178 +1 @@ -import contextlib -import enum -import logging -import os -from dataclasses import dataclass - -import httpx - -logger = logging.getLogger(__name__) - - -class DispenserApiConfig: - BASE_URL = "https://api.dispenser.algorandfoundation.tools" - - -class DispenserAssetName(enum.IntEnum): - ALGO = 0 - - -@dataclass -class DispenserAsset: - asset_id: int - decimals: int - description: str - - -@dataclass -class DispenserFundResponse: - tx_id: str - amount: int - - -@dataclass -class DispenserLimitResponse: - amount: int - - -DISPENSER_ASSETS = { - DispenserAssetName.ALGO: DispenserAsset( - asset_id=0, - decimals=6, - description="Algo", - ), -} -DISPENSER_REQUEST_TIMEOUT = 15 -DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN" - - -class TestNetDispenserApiClient: - """ - Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). - To get started create a new access token via `algokit dispenser login --ci` - and pass it to the client constructor as `auth_token`. - Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, - and it will be auto loaded. If both are set, the constructor argument takes precedence. - - Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. - """ - - auth_token: str - request_timeout = DISPENSER_REQUEST_TIMEOUT - - def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT): - auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY) - - if auth_token: - self.auth_token = auth_token - elif auth_token_from_env: - self.auth_token = auth_token_from_env - else: - raise Exception( - f"Can't init AlgoKit TestNet Dispenser API client " - f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or " - "the auth_token were provided." - ) - - self.request_timeout = request_timeout - - def _process_dispenser_request( - self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST" - ) -> httpx.Response: - """ - Generalized method to process http requests to dispenser API - """ - - headers = {"Authorization": f"Bearer {(auth_token)}"} - - # Set request arguments - request_args = { - "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}", - "headers": headers, - "timeout": self.request_timeout, - } - - if method.upper() != "GET" and data is not None: - request_args["json"] = data - - try: - response: httpx.Response = getattr(httpx, method.lower())(**request_args) - response.raise_for_status() - return response - - except httpx.HTTPStatusError as err: - error_message = f"Error processing dispenser API request: {err.response.status_code}" - error_response = None - with contextlib.suppress(Exception): - error_response = err.response.json() - - if error_response and error_response.get("code"): - error_message = error_response.get("code") - - elif err.response.status_code == httpx.codes.BAD_REQUEST: - error_message = err.response.json()["message"] - - raise Exception(error_message) from err - - except Exception as err: - error_message = "Error processing dispenser API request" - logger.debug(f"{error_message}: {err}", exc_info=True) - raise err - - def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse: - """ - Fund an account with Algos from the dispenser API - """ - - try: - response = self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix=f"fund/{asset_id}", - data={"receiver": address, "amount": amount, "assetID": asset_id}, - method="POST", - ) - - content = response.json() - return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"]) - - except Exception as err: - logger.exception(f"Error funding account {address}: {err}") - raise err - - def refund(self, refund_txn_id: str) -> None: - """ - Register a refund for a transaction with the dispenser API - """ - - try: - self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix="refund", - data={"refundTransactionID": refund_txn_id}, - method="POST", - ) - - except Exception as err: - logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}") - raise err - - def get_limit( - self, - address: str, - ) -> DispenserLimitResponse: - """ - Get current limit for an account with Algos from the dispenser API - """ - - try: - response = self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit", - method="GET", - ) - content = response.json() - - return DispenserLimitResponse(amount=content["amount"]) - except Exception as err: - logger.exception(f"Error setting limit for account {address}: {err}") - raise err +from algokit_utils.clients.dispenser_api_client import * # noqa: F403 diff --git a/src/algokit_utils/errors/__init__.py b/src/algokit_utils/errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/logic_error.py b/src/algokit_utils/logic_error.py index 56d22f9f..2b750b56 100644 --- a/src/algokit_utils/logic_error.py +++ b/src/algokit_utils/logic_error.py @@ -1,85 +1 @@ -import re -from copy import copy -from typing import TYPE_CHECKING, TypedDict - -from algokit_utils.models import SimulationTrace - -if TYPE_CHECKING: - from algosdk.source_map import SourceMap as AlgoSourceMap - -__all__ = [ - "LogicError", - "parse_logic_error", -] - -LOGIC_ERROR = ( - ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" -) - - -class LogicErrorData(TypedDict): - transaction_id: str - message: str - pc: int - - -def parse_logic_error( - error_str: str, -) -> LogicErrorData | None: - match = re.match(LOGIC_ERROR, error_str) - if match is None: - return None - - return { - "transaction_id": match.group("transaction_id"), - "message": match.group("message"), - "pc": int(match.group("pc")), - } - - -class LogicError(Exception): - def __init__( # noqa: PLR0913 - self, - *, - logic_error_str: str, - program: str, - source_map: "AlgoSourceMap | None", - transaction_id: str, - message: str, - pc: int, - logic_error: Exception | None = None, - traces: list[SimulationTrace] | None = None, - ): - self.logic_error = logic_error - self.logic_error_str = logic_error_str - self.program = program - self.source_map = source_map - self.lines = program.split("\n") - self.transaction_id = transaction_id - self.message = message - self.pc = pc - self.traces = traces - - self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None - - def __str__(self) -> str: - return ( - f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" - + (":" if self.line_no is None else f" and Source Line {self.line_no}:") - + f"\n{self.trace()}" - ) - - def trace(self, lines: int = 5) -> str: - if self.line_no is None: - return """ -Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the -error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map""" - - program_lines = copy(self.lines) - program_lines[self.line_no] += "\t\t<-- Error" - lines_before = max(0, self.line_no - lines) - lines_after = min(len(program_lines), self.line_no + lines) - return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) +from algokit_utils._legacy_v2.logic_error import * # noqa: F403 diff --git a/src/algokit_utils/models/__init__.py b/src/algokit_utils/models/__init__.py new file mode 100644 index 00000000..bcffc093 --- /dev/null +++ b/src/algokit_utils/models/__init__.py @@ -0,0 +1 @@ +from algokit_utils._legacy_v2.models import * # noqa: F403 diff --git a/src/algokit_utils/models/common.py b/src/algokit_utils/models/common.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/network_clients.py b/src/algokit_utils/network_clients.py index 2de270da..a9dc5de2 100644 --- a/src/algokit_utils/network_clients.py +++ b/src/algokit_utils/network_clients.py @@ -1,130 +1 @@ -import dataclasses -import os -from typing import Literal -from urllib import parse - -from algosdk.kmd import KMDClient -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient - -__all__ = [ - "AlgoClientConfig", - "get_algod_client", - "get_algonode_config", - "get_default_localnet_config", - "get_indexer_client", - "get_kmd_client_from_algod_client", - "is_localnet", - "is_mainnet", - "is_testnet", - "AlgoClientConfigs", - "get_kmd_client", -] - - -@dataclasses.dataclass -class AlgoClientConfig: - """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or - {py:class}`algosdk.v2client.indexer.IndexerClient`""" - - server: str - """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" - token: str - """API Token to authenticate with the service""" - - -@dataclasses.dataclass -class AlgoClientConfigs: - algod_config: AlgoClientConfig - indexer_config: AlgoClientConfig - kmd_config: AlgoClientConfig | None - - -def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: - """Returns the client configuration to point to the default LocalNet""" - port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] - return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) - - -def get_algonode_config( - network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str -) -> AlgoClientConfig: - client = "api" if config == "algod" else "idx" - return AlgoClientConfig( - server=f"https://{network}-{client}.algonode.cloud", - token=token, - ) - - -def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: - """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment - - If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" - config = config or _get_config_from_environment("ALGOD") - headers = {"X-Algo-API-Token": config.token} - return AlgodClient(config.token, config.server, headers) - - -def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: - """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment - - If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" - config = config or _get_config_from_environment("KMD") - return KMDClient(config.token, config.server) # type: ignore[no-untyped-call] - - -def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: - """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. - - If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`""" - config = config or _get_config_from_environment("INDEXER") - headers = {"X-Indexer-API-Token": config.token} - return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call] - - -def is_localnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" - params = client.suggested_params() - return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] - - -def is_mainnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `mainnet-v1`""" - params = client.suggested_params() - return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] - - -def is_testnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `testnet-v1`""" - params = client.suggested_params() - return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] - - -def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: - """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` - - Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, - or 4002 by default""" - # We can only use Kmd on the LocalNet otherwise it's not exposed so this makes some assumptions - # (e.g. same token and server as algod and port 4002 by default) - port = os.getenv("KMD_PORT", "4002") - server = _replace_kmd_port(client.algod_address, port) - return KMDClient(client.algod_token, server) # type: ignore[no-untyped-call] - - -def _replace_kmd_port(address: str, port: str) -> str: - parsed_algod = parse.urlparse(address) - kmd_host = parsed_algod.netloc.split(":", maxsplit=1)[0] + f":{port}" - kmd_parsed = parsed_algod._replace(netloc=kmd_host) - return parse.urlunparse(kmd_parsed) - - -def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: - server = os.getenv(f"{environment_prefix}_SERVER") - if server is None: - raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") - port = os.getenv(f"{environment_prefix}_PORT") - if port: - parsed = parse.urlparse(server) - server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() - return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) +from algokit_utils._legacy_v2.network_clients import * # noqa: F403 diff --git a/src/algokit_utils/transactions/__init__.py b/src/algokit_utils/transactions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/beta/composer.py b/src/algokit_utils/transactions/transaction_composer.py similarity index 77% rename from src/algokit_utils/beta/composer.py rename to src/algokit_utils/transactions/transaction_composer.py index a8aaa4b8..254b2a30 100644 --- a/src/algokit_utils/beta/composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -22,21 +22,6 @@ class SenderParam: @dataclass(frozen=True) class CommonTxnParams: - """ - Common transaction parameters. - - :param signer: The function used to sign transactions. - :param rekey_to: Change the signing key of the sender to the given address. - :param note: Note to attach to the transaction. - :param lease: Prevent multiple transactions with the same lease being included within the validity window. - :param static_fee: The transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. - :param extra_fee: The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. - :param max_fee: Throw an error if the fee for the transaction is more than this amount. - :param validity_window: How many rounds the transaction should be valid for. - :param first_valid_round: Set the first round this transaction is valid. If left undefined, the value from algod will be used. Only set this when you intentionally want this to be some time in the future. - :param last_valid_round: The last round this transaction is valid. It is recommended to use validity_window instead. - """ - signer: TransactionSigner | None = None rekey_to: str | None = None note: bytes | None = None @@ -75,22 +60,6 @@ class _RequiredAssetCreateParams(SenderParam): @dataclass(frozen=True) class AssetCreateParams(CommonTxnParams, _RequiredAssetCreateParams): - """ - Asset creation parameters. - - :param total: The total amount of the smallest divisible unit to create. - :param decimals: The amount of decimal places the asset should have. - :param default_frozen: Whether the asset is frozen by default in the creator address. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. Clawback will be permanently disabled if undefined or an empty string. - :param unit_name: The short ticker name for the asset. - :param asset_name: The full name of the asset. - :param url: The metadata URL for the asset. - :param metadata_hash: Hash of the metadata contained in the metadata URL. - """ - decimals: int | None = None default_frozen: bool | None = None manager: str | None = None @@ -110,16 +79,6 @@ class _RequiredAssetConfigParams(SenderParam): @dataclass(frozen=True) class AssetConfigParams(CommonTxnParams, _RequiredAssetConfigParams): - """ - Asset configuration parameters. - - :param asset_id: ID of the asset. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. Clawback will be permanently disabled if undefined or an empty string. - """ - manager: str | None = None reserve: str | None = None freeze: str | None = None @@ -169,17 +128,6 @@ class _RequiredOnlineKeyRegParams(SenderParam): @dataclass(frozen=True) class OnlineKeyRegParams(CommonTxnParams, _RequiredOnlineKeyRegParams): - """ - Online key registration parameters. - - :param vote_key: The root participation public key. - :param selection_key: The VRF public key. - :param vote_first: The first round that the participation key is valid. Not to be confused with the `first_valid` round of the keyreg transaction. - :param vote_last: The last round that the participation key is valid. Not to be confused with the `last_valid` round of the keyreg transaction. - :param vote_key_dilution: This is the dilution for the 2-level participation key. It determines the interval (number of rounds) for generating new ephemeral keys. - :param state_proof_key: The 64 byte state proof public key commitment. - """ - state_proof_key: bytes | None = None @@ -192,16 +140,6 @@ class _RequiredAssetTransferParams(SenderParam): @dataclass(frozen=True) class AssetTransferParams(CommonTxnParams, _RequiredAssetTransferParams): - """ - Asset transfer parameters. - - :param asset_id: ID of the asset. - :param amount: Amount of the asset to transfer (smallest divisible unit). - :param receiver: The account to send the asset to. - :param clawback_target: The account to take the asset from. - :param close_asset_to: The account to close the asset to. - """ - clawback_target: str | None = None close_asset_to: str | None = None @@ -284,20 +222,7 @@ class MethodCallParams(CommonTxnParams, _RequiredMethodCallParams): ] -class AlgokitComposer: - """ - A class for composing and managing Algorand transactions using the Algosdk library. - - Attributes: - txn_method_map (dict[str, algosdk.abi.Method]): A dictionary that maps transaction IDs to their corresponding ABI methods. - txns (List[Union[TransactionWithSigner, TxnParams, AtomicTransactionComposer]]): A list of transactions that have not yet been composed. - atc (AtomicTransactionComposer): An instance of AtomicTransactionComposer used to compose transactions. - algod (AlgodClient): The AlgodClient instance used by the composer for suggested params. - get_suggested_params (Callable[[], algosdk.future.transaction.SuggestedParams]): A function that returns suggested parameters for transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a TransactionSigner for that address. - default_validity_window (int): The default validity window for transactions. - """ - +class TransactionComposer: def __init__( self, algod: AlgodClient, @@ -305,15 +230,6 @@ def __init__( get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None, default_validity_window: int | None = None, ): - """ - Initialize an instance of the AlgokitComposer class. - - Args: - algod (AlgodClient): An instance of AlgodClient used to get suggested params and send transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a TransactionSigner for that address. - get_suggested_params (Optional[Callable[[], algosdk.future.transaction.SuggestedParams]], optional): A function that returns suggested parameters for transactions. If not provided, it defaults to using algod.suggested_params(). Defaults to None. - default_validity_window (Optional[int], optional): The default validity window for transactions. If not provided, it defaults to 10. Defaults to None. - """ self.txn_method_map: dict[str, algosdk.abi.Method] = {} self.txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] self.atc: AtomicTransactionComposer = AtomicTransactionComposer() @@ -323,47 +239,47 @@ def __init__( self.get_signer: Callable[[str], TransactionSigner] = get_signer self.default_validity_window: int = default_validity_window or 10 - def add_payment(self, params: PayParams) -> "AlgokitComposer": + def add_payment(self, params: PayParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_create(self, params: AssetCreateParams) -> "AlgokitComposer": + def add_asset_create(self, params: AssetCreateParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_config(self, params: AssetConfigParams) -> "AlgokitComposer": + def add_asset_config(self, params: AssetConfigParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_freeze(self, params: AssetFreezeParams) -> "AlgokitComposer": + def add_asset_freeze(self, params: AssetFreezeParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_destroy(self, params: AssetDestroyParams) -> "AlgokitComposer": + def add_asset_destroy(self, params: AssetDestroyParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_transfer(self, params: AssetTransferParams) -> "AlgokitComposer": + def add_asset_transfer(self, params: AssetTransferParams) -> "TransactionComposer": self.txns.append(params) return self - def add_asset_opt_in(self, params: AssetOptInParams) -> "AlgokitComposer": + def add_asset_opt_in(self, params: AssetOptInParams) -> "TransactionComposer": self.txns.append(params) return self - def add_app_call(self, params: AppCallParams) -> "AlgokitComposer": + def add_app_call(self, params: AppCallParams) -> "TransactionComposer": self.txns.append(params) return self - def add_online_key_reg(self, params: OnlineKeyRegParams) -> "AlgokitComposer": + def add_online_key_reg(self, params: OnlineKeyRegParams) -> "TransactionComposer": self.txns.append(params) return self - def add_atc(self, atc: AtomicTransactionComposer) -> "AlgokitComposer": + def add_atc(self, atc: AtomicTransactionComposer) -> "TransactionComposer": self.txns.append(atc) return self - def add_method_call(self, params: MethodCallParams) -> "AlgokitComposer": + def add_method_call(self, params: MethodCallParams) -> "TransactionComposer": self.txns.append(params) return self @@ -633,7 +549,7 @@ def _build_method_call( # noqa: C901, PLR0912 return self._build_atc(method_atc) - def _build_txn( # noqa: C901, PLR0912 + def _build_txn( # noqa: C901, PLR0912, PLR0911 self, txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, suggested_params: algosdk.transaction.SuggestedParams, diff --git a/tests/conftest.py b/tests/conftest.py index be23305b..e3997a2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ ) from dotenv import load_dotenv -from tests import app_client_test +from legacy_v2_tests import app_client_test if TYPE_CHECKING: from algosdk.kmd import KMDClient diff --git a/tests/test_algorand_client.py b/tests/test_algorand_client.py index 5f258640..8b7c448d 100644 --- a/tests/test_algorand_client.py +++ b/tests/test_algorand_client.py @@ -3,8 +3,8 @@ import pytest from algokit_utils import Account, ApplicationClient -from algokit_utils.beta.account_manager import AddressAndSigner -from algokit_utils.beta.algorand_client import ( +from algokit_utils.accounts.account_manager import AddressAndSigner +from algokit_utils.clients.algorand_client import ( AlgorandClient, AssetCreateParams, AssetOptInParams, From 1c77ad87c137e38707c6d71510594417c82057f6 Mon Sep 17 00:00:00 2001 From: Al Date: Mon, 4 Nov 2024 17:16:53 +0100 Subject: [PATCH 02/31] feat: TransactionComposer & AppManager implementation; various ongoing refactoring efforts (#120) * feat: Initial AppManager implementation; wip on Composer; mypy tweaks; initial tests - Add AppManager class with methods for compiling TEAL, managing app state, and handling template variables - Update TransactionComposer to use AppManager - Move Account model to a separate file and update imports - Add new models for ABI values and application constants - Improve type annotations and remove unnecessary type ignores - Add initial tests for AppManager template substitution and comment stripping - Update mypy configuration to *globally* exclude untyped calls in algosdk -> removing ~50 individual mypy type ignore for algosdk * chore: removing models.py folders in favour of granular modules in root models namespace * chore: wip * feat: initial implementation of TransactionComposer --- legacy_v2_tests/conftest.py | 6 +- pyproject.toml | 9 + src/algokit_utils/__init__.py | 5 +- src/algokit_utils/_debugging.py | 4 +- .../_legacy_v2/_ensure_funded.py | 6 +- src/algokit_utils/_legacy_v2/_transfer.py | 8 +- src/algokit_utils/_legacy_v2/account.py | 14 +- .../_legacy_v2/application_client.py | 8 +- .../_legacy_v2/application_specification.py | 4 +- src/algokit_utils/_legacy_v2/asset.py | 2 +- src/algokit_utils/_legacy_v2/deploy.py | 56 +- src/algokit_utils/_legacy_v2/models.py | 38 +- .../_legacy_v2/network_clients.py | 6 +- src/algokit_utils/accounts/account_manager.py | 2 +- src/algokit_utils/applications/app_manager.py | 355 +++++++ src/algokit_utils/assets/asset_manager.py | 2 + src/algokit_utils/clients/algorand_client.py | 84 +- src/algokit_utils/clients/models.py | 0 src/algokit_utils/models/__init__.py | 3 + src/algokit_utils/models/abi.py | 4 + src/algokit_utils/models/account.py | 35 + src/algokit_utils/models/amount.py | 123 +++ src/algokit_utils/models/application.py | 5 + src/algokit_utils/models/common.py | 0 src/algokit_utils/transactions/models.py | 30 + .../transactions/transaction_composer.py | 905 ++++++++++++++---- .../transactions/transaction_creator.py | 2 + .../transactions/transaction_sender.py | 88 ++ .../applications/__init__.py | 0 .../test_comment_stripping.approved.txt | 30 + .../test_template_substitution.approved.txt | 21 + tests/applications/test_app_manager.py | 77 ++ .../models.py => tests/clients/__init__.py | 0 tests/clients/test_algorand_client.py | 223 +++++ tests/conftest.py | 8 +- tests/test_algorand_client.py | 222 ----- tests/test_transaction_composer.py | 212 ++++ .../transactions/__init__.py | 0 .../artifacts/hello_world/approval.teal | 62 ++ .../artifacts/hello_world/clear.teal | 5 + .../transactions/test_transaction_composer.py | 256 +++++ 41 files changed, 2328 insertions(+), 592 deletions(-) create mode 100644 src/algokit_utils/applications/app_manager.py create mode 100644 src/algokit_utils/assets/asset_manager.py delete mode 100644 src/algokit_utils/clients/models.py create mode 100644 src/algokit_utils/models/abi.py create mode 100644 src/algokit_utils/models/account.py create mode 100644 src/algokit_utils/models/amount.py create mode 100644 src/algokit_utils/models/application.py delete mode 100644 src/algokit_utils/models/common.py create mode 100644 src/algokit_utils/transactions/transaction_creator.py create mode 100644 src/algokit_utils/transactions/transaction_sender.py rename src/algokit_utils/accounts/models.py => tests/applications/__init__.py (100%) create mode 100644 tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt create mode 100644 tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt create mode 100644 tests/applications/test_app_manager.py rename src/algokit_utils/applications/models.py => tests/clients/__init__.py (100%) create mode 100644 tests/clients/test_algorand_client.py delete mode 100644 tests/test_algorand_client.py create mode 100644 tests/test_transaction_composer.py rename src/algokit_utils/assets/models.py => tests/transactions/__init__.py (100%) create mode 100644 tests/transactions/artifacts/hello_world/approval.teal create mode 100644 tests/transactions/artifacts/hello_world/clear.teal create mode 100644 tests/transactions/test_transaction_composer.py diff --git a/legacy_v2_tests/conftest.py b/legacy_v2_tests/conftest.py index e3997a2c..dbe4be46 100644 --- a/legacy_v2_tests/conftest.py +++ b/legacy_v2_tests/conftest.py @@ -188,11 +188,11 @@ def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int note=None, lease=None, rekey_to=None, - ) # type: ignore[no-untyped-call] + ) - signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call] + signed_transaction = txn.sign(sender.private_key) algod_client.send_transaction(signed_transaction) - ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call] + ptx = algod_client.pending_transaction_info(txn.get_txid()) if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): return ptx["asset-index"] diff --git a/pyproject.toml b/pyproject.toml index 4e3a99a9..bd391ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,7 @@ suppress-none-returning = true [tool.ruff.lint.per-file-ignores] "src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"] "path/to/file.py" = ["E402"] +"tests/clients/test_algorand_client.py" = ["ERA001"] [tool.poe.tasks] docs = ["docs-html-only", "docs-md-only"] @@ -149,6 +150,14 @@ disallow_any_generics = false implicit_reexport = false show_error_codes = true +untyped_calls_exclude = [ + "algosdk", +] + +[[tool.mypy.overrides]] +module = ["algosdk", "algosdk.*"] +disallow_untyped_calls = false + [tool.semantic_release] version_toml = "pyproject.toml:tool.poetry.version" remove_dist = false diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 77959758..d89bad9b 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -30,9 +30,7 @@ from algokit_utils._legacy_v2.asset import opt_in, opt_out from algokit_utils._legacy_v2.common import Program from algokit_utils._legacy_v2.deploy import ( - DELETABLE_TEMPLATE_NAME, NOTE_PREFIX, - UPDATABLE_TEMPLATE_NAME, ABICallArgs, ABICallArgsDict, ABICreateCallArgs, @@ -61,7 +59,6 @@ ABIArgsDict, ABIMethod, ABITransactionResponse, - Account, CommonCallParameters, CommonCallParametersDict, CreateCallParameters, @@ -91,6 +88,8 @@ DispenserLimitResponse, TestNetDispenserApiClient, ) +from algokit_utils.models.account import Account +from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME __all__ = [ # ==== LEGACY V2 EXPORTS BEGIN ==== diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index e8c0ef52..de5ed182 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -157,9 +157,7 @@ def _build_avm_sourcemap( # noqa: PLR0913 raise ValueError("Either raw teal or compiled teal must be provided") result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client) - program_hash = base64.b64encode( - checksum(result.raw_binary) # type: ignore[no-untyped-call] - ).decode() + program_hash = base64.b64encode(checksum(result.raw_binary)).decode() source_map = result.source_map.__dict__ source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else [] diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index 23c87860..2db90f36 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -7,12 +7,12 @@ from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.account import get_dispenser_account -from algokit_utils._legacy_v2.models import Account from algokit_utils._legacy_v2.network_clients import is_testnet from algokit_utils.clients.dispenser_api_client import ( DispenserAssetName, TestNetDispenserApiClient, ) +from algokit_utils.models.account import Account @dataclass(kw_only=True) @@ -63,7 +63,7 @@ def _get_address_to_fund(parameters: EnsureBalanceParameters) -> str: if isinstance(parameters.account_to_fund, str): return parameters.account_to_fund else: - return str(address_from_private_key(parameters.account_to_fund.private_key)) # type: ignore[no-untyped-call] + return str(address_from_private_key(parameters.account_to_fund.private_key)) def _get_account_info(client: AlgodClient, address_to_fund: str) -> dict: @@ -111,7 +111,7 @@ def _fund_using_transfer( fee_micro_algos=parameters.fee_micro_algos, ), ) - transaction_id = response.get_txid() # type: ignore[no-untyped-call] + transaction_id = response.get_txid() return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt) diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index baca5b2b..6b59cd4c 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -7,7 +7,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams -from algokit_utils._legacy_v2.models import Account +from algokit_utils.models.account import Account if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -93,7 +93,7 @@ def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTx amt=params.micro_algos, note=params.note.encode("utf-8") if isinstance(params.note, str) else params.note, sp=params.suggested_params, - ) # type: ignore[no-untyped-call] + ) result = _send_transaction(client=client, transaction=transaction, parameters=params) assert isinstance(result, PaymentTxn) @@ -117,7 +117,7 @@ def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) - note=params.note, index=params.asset_id, rekey_to=None, - ) # type: ignore[no-untyped-call] + ) result = _send_transaction(client=client, transaction=xfer_txn, parameters=params) assert isinstance(result, AssetTransferTxn) @@ -148,5 +148,5 @@ def _get_address(account: Account | AccountTransactionSigner) -> str: if type(account) is Account: return account.address else: - address = address_from_private_key(account.private_key) # type: ignore[no-untyped-call] + address = address_from_private_key(account.private_key) return str(address) diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index 819a448f..d98a875a 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -7,8 +7,8 @@ from algosdk.util import algos_to_microalgos from algokit_utils._legacy_v2._transfer import TransferParameters, transfer -from algokit_utils._legacy_v2.models import Account from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet +from algokit_utils.models.account import Account if TYPE_CHECKING: from collections.abc import Callable @@ -32,8 +32,8 @@ def get_account_from_mnemonic(mnemonic: str) -> Account: """Convert a mnemonic (25 word passphrase) into an Account""" - private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call] - address = address_from_private_key(private_key) # type: ignore[no-untyped-call] + private_key = to_private_key(mnemonic) + address = address_from_private_key(private_key) return Account(private_key=private_key, address=address) @@ -47,7 +47,7 @@ def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: account_key = key_ids[0] private_account_key = kmd_client.export_key(wallet_handle, "", account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] + return get_account_from_mnemonic(from_private_key(private_account_key)) def get_or_create_kmd_wallet_account( @@ -79,7 +79,7 @@ def get_or_create_kmd_wallet_account( TransferParameters( from_account=get_dispenser_account(client), to_address=account.address, - micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call] + micro_algos=algos_to_microalgos(fund_with_algos), ), ) @@ -139,7 +139,7 @@ def get_kmd_wallet_account( return None private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] + return get_account_from_mnemonic(from_private_key(private_account_key)) def get_account( @@ -177,7 +177,7 @@ def get_account( if is_localnet(client): account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) - os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call] + os.environ[mnemonic_key] = from_private_key(account.private_key) return account raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'") diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index 32851fa4..a52639d1 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -43,7 +43,6 @@ ABIArgType, ABIMethod, ABITransactionResponse, - Account, CreateCallParameters, CreateCallParametersDict, OnCompleteCallParameters, @@ -54,6 +53,7 @@ TransactionResponse, ) from algokit_utils.config import config +from algokit_utils.models.account import Account if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -1021,7 +1021,7 @@ def add_method_call( # noqa: PLR0913 raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}") atc.add_transaction( TransactionWithSigner( - txn=transaction.ApplicationCallTxn( # type: ignore[no-untyped-call] + txn=transaction.ApplicationCallTxn( sender=sender, sp=sp, index=app_id, @@ -1329,11 +1329,11 @@ def get_sender_from_signer(signer: TransactionSigner | None) -> str | None: """Returns the associated address of a signer, return None if no address found""" if isinstance(signer, AccountTransactionSigner): - sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] + sender = address_from_private_key(signer.private_key) assert isinstance(sender, str) return sender elif isinstance(signer, MultisigTransactionSigner): - sender = signer.msig.address() # type: ignore[no-untyped-call] + sender = signer.msig.address() assert isinstance(sender, str) return sender elif isinstance(signer, LogicSigTransactionSigner): diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py index 392fce8d..865dece5 100644 --- a/src/algokit_utils/_legacy_v2/application_specification.py +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -130,7 +130,7 @@ def _encode_state_schema(schema: StateSchema) -> dict[str, int]: def _decode_state_schema(data: dict[str, int]) -> StateSchema: - return StateSchema( # type: ignore[no-untyped-call] + return StateSchema( num_byte_slices=data.get("num_byte_slices", 0), num_uints=data.get("num_uints", 0), ) @@ -203,4 +203,4 @@ def export(self, directory: Path | str | None = None) -> None: def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] + return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 2ef4860f..2f71cbf8 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -10,7 +10,7 @@ from enum import Enum, auto -from algokit_utils._legacy_v2.models import Account +from algokit_utils.models.account import Account __all__ = ["opt_in", "opt_out"] logger = logging.getLogger(__name__) diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index 561ce413..ed0bd0e5 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -11,6 +11,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner from algosdk.logic import get_application_address from algosdk.transaction import StateSchema +from deprecated import deprecated from algokit_utils._legacy_v2.application_specification import ( ApplicationSpecification, @@ -21,10 +22,11 @@ from algokit_utils._legacy_v2.models import ( ABIArgsDict, ABIMethod, - Account, CreateCallParameters, TransactionResponse, ) +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.models.account import Account if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -185,7 +187,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - while True: response = indexer.lookup_account_application_by_creator( creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token - ) # type: ignore[no-untyped-call] + ) if "message" in response: # an error occurred raise Exception(f"Error querying applications for {creator_address}: {response}") for app in response["applications"]: @@ -199,7 +201,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - address=creator_address, address_role="sender", note_prefix=NOTE_PREFIX.encode("utf-8"), - ) # type: ignore[no-untyped-call] + ) transactions: list[dict] = search_transactions_response["transactions"] if not transactions: continue @@ -236,7 +238,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] + return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: @@ -288,33 +290,6 @@ def _is_valid_token_character(char: str) -> bool: return char.isalnum() or char == "_" -def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: - result: list[str] = [] - match_count = 0 - token = f"TMPL_{template_variable}" - token_idx_offset = len(value) - len(token) - for line in program_lines: - comment_idx = _find_unquoted_string(line, "//") - if comment_idx is None: - comment_idx = len(line) - code = line[:comment_idx] - comment = line[comment_idx:] - trailing_idx = 0 - while True: - token_idx = _find_template_token(code, token, trailing_idx) - if token_idx is None: - break - - trailing_idx = token_idx + len(token) - prefix = code[:token_idx] - suffix = code[trailing_idx:] - code = f"{prefix}{value}{suffix}" - match_count += 1 - trailing_idx += token_idx_offset - result.append(code + comment) - return result, match_count - - def add_deploy_template_variables( template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None ) -> None: @@ -437,6 +412,7 @@ def check_template_variables(approval_program: str, template_values: TemplateVal logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") +@deprecated(reason="Use `AppManager.replace_template_variables` instead", version="3.0.0") def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: """Replaces `TMPL_*` variables in `program` with `template_values` @@ -444,23 +420,7 @@ def replace_template_variables(program: str, template_values: TemplateValueMappi `template_values` keys should *NOT* be prefixed with `TMPL_` ``` """ - program_lines = program.splitlines() - for template_variable_name, template_value in template_values.items(): - match template_value: - case int(): - value = str(template_value) - case str(): - value = "0x" + template_value.encode("utf-8").hex() - case bytes(): - value = "0x" + template_value.hex() - case _: - raise DeploymentFailedError( - f"Unexpected template value type {template_variable_name}: {template_value.__class__}" - ) - - program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value) - - return "\n".join(program_lines) + return AppManager.replace_template_variables(program, template_values) def has_template_vars(app_spec: ApplicationSpecification) -> bool: diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index cc5d34d2..d20bed83 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -2,23 +2,22 @@ from collections.abc import Sequence from typing import Any, Generic, Protocol, TypeAlias, TypedDict, TypeVar -import algosdk.account from algosdk import transaction from algosdk.abi import Method from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, AtomicTransactionResponse, SimulateAtomicTransactionResponse, TransactionSigner, ) -from algosdk.encoding import decode_address from deprecated import deprecated +# Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) + + __all__ = [ "ABIArgsDict", "ABIMethod", "ABITransactionResponse", - "Account", "CreateCallParameters", "CreateCallParametersDict", "CreateTransactionParameters", @@ -31,37 +30,6 @@ ReturnType = TypeVar("ReturnType") -@dataclasses.dataclass(kw_only=True) -class Account: - """Holds the private_key and address for an account""" - - private_key: str - """Base64 encoded private key""" - address: str = dataclasses.field(default="") - """Address for this account""" - - def __post_init__(self) -> None: - if not self.address: - self.address = algosdk.account.address_from_private_key(self.private_key) # type: ignore[no-untyped-call] - - @property - def public_key(self) -> bytes: - """The public key for this account""" - public_key = decode_address(self.address) # type: ignore[no-untyped-call] - assert isinstance(public_key, bytes) - return public_key - - @property - def signer(self) -> AccountTransactionSigner: - """An AccountTransactionSigner for this account""" - return AccountTransactionSigner(self.private_key) - - @staticmethod - def new_account() -> "Account": - private_key, address = algosdk.account.generate_account() # type: ignore[no-untyped-call] - return Account(private_key=private_key) - - @dataclasses.dataclass(kw_only=True) class TransactionResponse: """Response for a non ABI call""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index 2de270da..b1bcc2cb 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -70,7 +70,7 @@ def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" config = config or _get_config_from_environment("KMD") - return KMDClient(config.token, config.server) # type: ignore[no-untyped-call] + return KMDClient(config.token, config.server) def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: @@ -79,7 +79,7 @@ def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`""" config = config or _get_config_from_environment("INDEXER") headers = {"X-Indexer-API-Token": config.token} - return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call] + return IndexerClient(config.token, config.server, headers) def is_localnet(client: AlgodClient) -> bool: @@ -109,7 +109,7 @@ def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: # (e.g. same token and server as algod and port 4002 by default) port = os.getenv("KMD_PORT", "4002") server = _replace_kmd_port(client.algod_address, port) - return KMDClient(client.algod_token, server) # type: ignore[no-untyped-call] + return KMDClient(client.algod_token, server) def _replace_kmd_port(address: str, port: str) -> str: diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index a2527c6c..d4d95d19 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -109,7 +109,7 @@ def random(self) -> AddressAndSigner: :return: The account """ - (sk, addr) = generate_account() # type: ignore[no-untyped-call] + (sk, addr) = generate_account() signer = AccountTransactionSigner(sk) self.set_signer(addr, signer) diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py new file mode 100644 index 00000000..91b0a407 --- /dev/null +++ b/src/algokit_utils/applications/app_manager.py @@ -0,0 +1,355 @@ +import base64 +from collections.abc import Mapping +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, TypeAlias + +import algosdk +import algosdk.atomic_transaction_composer +import algosdk.box_reference +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.logic import get_application_address +from algosdk.v2client import algod + +from algokit_utils.models.abi import ABIValue +from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME + + +@dataclass(frozen=True) +class BoxName: + name: str + name_raw: bytes + name_base64: str + + +@dataclass(frozen=True) +class AppState: + key_raw: bytes + key_base64: str + value_raw: bytes | None + value_base64: str | None + value: str | int + + +class DataTypeFlag(IntEnum): + BYTES = 1 + UINT = 2 + + +TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] + + +@dataclass(frozen=True) +class AppInformation: + app_id: int + app_address: str + approval_program: bytes + clear_state_program: bytes + creator: str + global_state: dict[str, AppState] + local_ints: int + local_byte_slices: int + global_ints: int + global_byte_slices: int + extra_program_pages: int | None + + +@dataclass(frozen=True) +class CompiledTeal: + teal: str + compiled: bytes + compiled_hash: str + compiled_base64_to_bytes: bytes + source_map: dict | None + + +BoxIdentifier = str | bytes | AccountTransactionSigner + + +def _is_valid_token_character(char: str) -> bool: + return char.isalnum() or char == "_" + + +def _last_token_base64(line: str, idx: int) -> bool: + try: + *_, last = line[:idx].split() + except ValueError: + return False + return last in ("base64", "b64") + + +def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. + Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING + Returns None if not found""" + if end < 0: + end = len(line) + + idx = start + while idx < end: + token_idx = _find_unquoted_string(line, token, idx, end) + if token_idx is None: + break + trailing_idx = token_idx + len(token) + if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start + trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end + ): + return token_idx + idx = trailing_idx + return None + + +def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. + Returns None if not found""" + + if end < 0: + end = len(line) + idx = start + in_quotes = in_base64 = False + while idx < end: + current_char = line[idx] + match current_char: + # enter base64 + case " " | "(" if not in_quotes and _last_token_base64(line, idx): + in_base64 = True + # exit base64 + case " " | ")" if not in_quotes and in_base64: + in_base64 = False + # escaped char + case "\\" if in_quotes: + # skip next character + idx += 1 + # quote boundary + case '"': + in_quotes = not in_quotes + # can test for match + case _ if not in_quotes and not in_base64 and line.startswith(token, idx): + # only match if not in quotes and string matches + return idx + idx += 1 + return None + + +def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: + result: list[str] = [] + match_count = 0 + token = f"TMPL_{template_variable}" + token_idx_offset = len(value) - len(token) + for line in program_lines: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + comment_idx = len(line) + code = line[:comment_idx] + comment = line[comment_idx:] + trailing_idx = 0 + while True: + token_idx = _find_template_token(code, token, trailing_idx) + if token_idx is None: + break + + trailing_idx = token_idx + len(token) + prefix = code[:token_idx] + suffix = code[trailing_idx:] + code = f"{prefix}{value}{suffix}" + match_count += 1 + trailing_idx += token_idx_offset + result.append(code + comment) + return result, match_count + + +class AppManager: + def __init__(self, algod_client: algod.AlgodClient): + self._algod = algod_client + self._compilation_results: dict[str, CompiledTeal] = {} + + def compile_teal(self, teal_code: str) -> CompiledTeal: + if teal_code in self._compilation_results: + return self._compilation_results[teal_code] + + compiled = self._algod.compile(teal_code, source_map=True) + result = CompiledTeal( + teal=teal_code, + compiled=compiled["result"], + compiled_hash=compiled["hash"], + compiled_base64_to_bytes=base64.b64decode(compiled["result"]), + source_map=compiled.get("sourcemap"), + ) + self._compilation_results[teal_code] = result + return result + + def compile_teal_template( + self, + teal_template_code: str, + template_params: TealTemplateParams | None = None, + deployment_metadata: dict[str, bool] | None = None, + ) -> CompiledTeal: + teal_code = AppManager.strip_teal_comments(teal_template_code) + teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) + + if deployment_metadata: + teal_code = AppManager.replace_teal_template_deploy_time_control_params(teal_code, deployment_metadata) + + return self.compile_teal(teal_code) + + def get_compilation_result(self, teal_code: str) -> CompiledTeal | None: + return self._compilation_results.get(teal_code) + + def get_by_id(self, app_id: int) -> AppInformation: + app = self._algod.application_info(app_id) + assert isinstance(app, dict) + app_params = app["params"] + + return AppInformation( + app_id=app_id, + app_address=get_application_address(app_id), + approval_program=base64.b64decode(app_params["approval-program"]), + clear_state_program=base64.b64decode(app_params["clear-state-program"]), + creator=app_params["creator"], + local_ints=app_params["local-state-schema"]["num-uint"], + local_byte_slices=app_params["local-state-schema"]["num-byte-slice"], + global_ints=app_params["global-state-schema"]["num-uint"], + global_byte_slices=app_params["global-state-schema"]["num-byte-slice"], + extra_program_pages=app_params.get("extra-program-pages", 0), + global_state=self.decode_app_state(app_params.get("global-state", [])), + ) + + def get_global_state(self, app_id: int) -> dict[str, AppState]: + return self.get_by_id(app_id).global_state + + def get_local_state(self, app_id: int, address: str) -> dict[str, AppState]: + app_info = self._algod.account_application_info(address, app_id) + assert isinstance(app_info, dict) + if not app_info.get("app-local-state", {}).get("key-value"): + raise ValueError("Couldn't find local state") + return self.decode_app_state(app_info["app-local-state"]["key-value"]) + + def get_box_names(self, app_id: int) -> list[BoxName]: + box_result = self._algod.application_boxes(app_id) + assert isinstance(box_result, dict) + return [ + BoxName( + name_raw=base64.b64decode(b["name"]), + name_base64=b["name"], + name=base64.b64decode(b["name"]).decode("utf-8"), + ) + for b in box_result["boxes"] + ] + + def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: + name = b"" + if isinstance(box_name, str): + name = box_name.encode("utf-8") + elif isinstance(box_name, bytes): + name = box_name + elif isinstance(box_name, AccountTransactionSigner): + name = algosdk.encoding.decode_address(algosdk.account.address_from_private_key(box_name.private_key)) + else: + raise ValueError(f"Invalid box identifier type: {type(box_name)}") + + box_result = self._algod.application_box_by_name(app_id, name) + assert isinstance(box_result, dict) + return base64.b64decode(box_result["value"]) + + def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: + return [self.get_box_value(app_id, box_name) for box_name in box_names] + + def get_box_value_from_abi_type( + self, app_id: int, box_name: BoxIdentifier, abi_type: algosdk.abi.ABIType + ) -> ABIValue: + value = self.get_box_value(app_id, box_name) + try: + return abi_type.decode(value) # type: ignore[no-any-return] + except Exception as e: + raise ValueError(f"Failed to decode box value {value.decode('utf-8')} with ABI type {abi_type}") from e + + def get_box_values_from_abi_type( + self, app_id: int, box_names: list[BoxIdentifier], abi_type: algosdk.abi.ABIType + ) -> list[ABIValue]: + return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] + + @staticmethod + def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: + state_values: dict[str, AppState] = {} + + for state_val in state: + key_base64 = state_val["key"] + key_raw = base64.b64decode(key_base64) + key = key_raw.decode("utf-8") + teal_value = state_val["value"] + + data_type_flag = teal_value.get("action", teal_value.get("type")) + + if data_type_flag == DataTypeFlag.BYTES: + value_base64 = teal_value.get("bytes", "") + value_raw = base64.b64decode(value_base64) + state_values[key] = AppState( + key_raw=key_raw, + key_base64=key_base64, + value_raw=value_raw, + value_base64=value_base64, + value=value_raw.decode("utf-8"), + ) + elif data_type_flag == DataTypeFlag.UINT: + value = teal_value.get("uint", 0) + state_values[key] = AppState( + key_raw=key_raw, + key_base64=key_base64, + value_raw=None, + value_base64=None, + value=int(value), + ) + else: + raise ValueError(f"Received unknown state data type of {data_type_flag}") + + return state_values + + @staticmethod + def replace_template_variables(program: str, template_values: TealTemplateParams) -> str: + program_lines = program.splitlines() + for template_variable_name, template_value in template_values.items(): + match template_value: + case int(): + value = str(template_value) + case str(): + value = "0x" + template_value.encode("utf-8").hex() + case bytes(): + value = "0x" + template_value.hex() + case _: + raise ValueError( + f"Unexpected template value type {template_variable_name}: {template_value.__class__}" + ) + + program_lines, _ = _replace_template_variable(program_lines, template_variable_name, value) + + return "\n".join(program_lines) + + @staticmethod + def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: dict[str, bool]) -> str: + if params.get("updatable") is not None: + if UPDATABLE_TEMPLATE_NAME not in teal_template_code: + raise ValueError( + f"Deploy-time updatability control requested for app deployment, but {UPDATABLE_TEMPLATE_NAME} " + "not present in TEAL code" + ) + teal_template_code = teal_template_code.replace(UPDATABLE_TEMPLATE_NAME, str(int(params["updatable"]))) + + if params.get("deletable") is not None: + if DELETABLE_TEMPLATE_NAME not in teal_template_code: + raise ValueError( + f"Deploy-time deletability control requested for app deployment, but {DELETABLE_TEMPLATE_NAME} " + "not present in TEAL code" + ) + teal_template_code = teal_template_code.replace(DELETABLE_TEMPLATE_NAME, str(int(params["deletable"]))) + + return teal_template_code + + @staticmethod + def strip_teal_comments(teal_code: str) -> str: + def _strip_comment(line: str) -> str: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + return line + return line[:comment_idx].rstrip() + + return "\n".join(_strip_comment(line) for line in teal_code.splitlines()) diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py new file mode 100644 index 00000000..4bef4802 --- /dev/null +++ b/src/algokit_utils/assets/asset_manager.py @@ -0,0 +1,2 @@ +class AssetManager: + """A manager for Algorand assets""" diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index 7e02e20a..f4851daf 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -9,6 +9,8 @@ from typing_extensions import Self from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager from algokit_utils.network_clients import ( AlgoClientConfigs, @@ -20,29 +22,31 @@ ) from algokit_utils.transactions.transaction_composer import ( AppCallParams, + AppMethodCallParams, AssetConfigParams, AssetCreateParams, AssetDestroyParams, AssetFreezeParams, AssetOptInParams, AssetTransferParams, - MethodCallParams, - OnlineKeyRegParams, - PayParams, + OnlineKeyRegistrationParams, + PaymentParams, TransactionComposer, ) +from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator +from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender __all__ = [ "AlgorandClient", "AssetCreateParams", "AssetOptInParams", - "MethodCallParams", - "PayParams", + "AppMethodCallParams", + "PaymentParams", "AssetFreezeParams", "AssetConfigParams", "AssetDestroyParams", "AppCallParams", - "OnlineKeyRegParams", + "OnlineKeyRegistrationParams", "AssetTransferParams", ] @@ -53,15 +57,15 @@ class AlgorandClientSendMethods: Methods used to send a transaction to the network and wait for confirmation """ - payment: Callable[[PayParams], dict[str, Any]] + payment: Callable[[PaymentParams], dict[str, Any]] asset_create: Callable[[AssetCreateParams], dict[str, Any]] asset_config: Callable[[AssetConfigParams], dict[str, Any]] asset_freeze: Callable[[AssetFreezeParams], dict[str, Any]] asset_destroy: Callable[[AssetDestroyParams], dict[str, Any]] asset_transfer: Callable[[AssetTransferParams], dict[str, Any]] app_call: Callable[[AppCallParams], dict[str, Any]] - online_key_reg: Callable[[OnlineKeyRegParams], dict[str, Any]] - method_call: Callable[[MethodCallParams], dict[str, Any]] + online_key_reg: Callable[[OnlineKeyRegistrationParams], dict[str, Any]] + method_call: Callable[[AppMethodCallParams], dict[str, Any]] asset_opt_in: Callable[[AssetOptInParams], dict[str, Any]] @@ -71,15 +75,15 @@ class AlgorandClientTransactionMethods: Methods used to form a transaction without signing or sending to the network """ - payment: Callable[[PayParams], Transaction] + payment: Callable[[PaymentParams], Transaction] asset_create: Callable[[AssetCreateParams], Transaction] asset_config: Callable[[AssetConfigParams], Transaction] asset_freeze: Callable[[AssetFreezeParams], Transaction] asset_destroy: Callable[[AssetDestroyParams], Transaction] asset_transfer: Callable[[AssetTransferParams], Transaction] app_call: Callable[[AppCallParams], Transaction] - online_key_reg: Callable[[OnlineKeyRegParams], Transaction] - method_call: Callable[[MethodCallParams], list[Transaction]] + online_key_reg: Callable[[OnlineKeyRegistrationParams], Transaction] + method_call: Callable[[AppMethodCallParams], list[Transaction]] asset_opt_in: Callable[[AssetOptInParams], Transaction] @@ -89,6 +93,15 @@ class AlgorandClient: def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): self._client_manager: ClientManager = ClientManager(config) self._account_manager: AccountManager = AccountManager(self._client_manager) + self._asset_manager: AssetManager = AssetManager() # TODO: implement + self._app_manager: AppManager = AppManager(self._client_manager.algod) # TODO: implement + self._transaction_sender = AlgorandClientTransactionSender( + new_group=lambda: self.new_group(), + asset_manager=self._asset_manager, + app_manager=self._app_manager, + algod_client=self._client_manager.algod, + ) + self._transaction_creator = AlgorandClientTransactionCreator() # TODO: implement self._cached_suggested_params: SuggestedParams | None = None self._cached_suggested_params_expiry: float | None = None @@ -187,53 +200,14 @@ def new_group(self) -> TransactionComposer: ) @property - def send(self) -> AlgorandClientSendMethods: + def send(self) -> AlgorandClientTransactionSender: """Methods for sending a transaction and waiting for confirmation""" - return AlgorandClientSendMethods( - payment=lambda params: self._unwrap_single_send_result(self.new_group().add_payment(params).execute()), - asset_create=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_create(params).execute() - ), - asset_config=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_config(params).execute() - ), - asset_freeze=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_freeze(params).execute() - ), - asset_destroy=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_destroy(params).execute() - ), - asset_transfer=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_transfer(params).execute() - ), - app_call=lambda params: self._unwrap_single_send_result(self.new_group().add_app_call(params).execute()), - online_key_reg=lambda params: self._unwrap_single_send_result( - self.new_group().add_online_key_reg(params).execute() - ), - method_call=lambda params: self._unwrap_single_send_result( - self.new_group().add_method_call(params).execute() - ), - asset_opt_in=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_opt_in(params).execute() - ), - ) + return self._transaction_sender @property - def transactions(self) -> AlgorandClientTransactionMethods: + def create_transaction(self) -> AlgorandClientTransactionCreator: """Methods for building transactions""" - - return AlgorandClientTransactionMethods( - payment=lambda params: self.new_group().add_payment(params).build_group()[0].txn, - asset_create=lambda params: self.new_group().add_asset_create(params).build_group()[0].txn, - asset_config=lambda params: self.new_group().add_asset_config(params).build_group()[0].txn, - asset_freeze=lambda params: self.new_group().add_asset_freeze(params).build_group()[0].txn, - asset_destroy=lambda params: self.new_group().add_asset_destroy(params).build_group()[0].txn, - asset_transfer=lambda params: self.new_group().add_asset_transfer(params).build_group()[0].txn, - app_call=lambda params: self.new_group().add_app_call(params).build_group()[0].txn, - online_key_reg=lambda params: self.new_group().add_online_key_reg(params).build_group()[0].txn, - method_call=lambda params: [txn.txn for txn in self.new_group().add_method_call(params).build_group()], - asset_opt_in=lambda params: self.new_group().add_asset_opt_in(params).build_group()[0].txn, - ) + return self._transaction_creator @staticmethod def default_local_net() -> "AlgorandClient": diff --git a/src/algokit_utils/clients/models.py b/src/algokit_utils/clients/models.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/algokit_utils/models/__init__.py b/src/algokit_utils/models/__init__.py index bcffc093..baf4664d 100644 --- a/src/algokit_utils/models/__init__.py +++ b/src/algokit_utils/models/__init__.py @@ -1 +1,4 @@ from algokit_utils._legacy_v2.models import * # noqa: F403 + +from .abi import * # noqa: F403 +from .account import * # noqa: F403 diff --git a/src/algokit_utils/models/abi.py b/src/algokit_utils/models/abi.py new file mode 100644 index 00000000..767eed09 --- /dev/null +++ b/src/algokit_utils/models/abi.py @@ -0,0 +1,4 @@ +ABIPrimitiveValue = bool | int | str | bytes | bytearray + +# NOTE: This is present in js-algorand-sdk, but sadly not in untyped py-algorand-sdk +ABIValue = ABIPrimitiveValue | list["ABIValue"] | dict[str, "ABIValue"] diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py new file mode 100644 index 00000000..3014b7af --- /dev/null +++ b/src/algokit_utils/models/account.py @@ -0,0 +1,35 @@ +import dataclasses + +import algosdk +from algosdk.atomic_transaction_composer import AccountTransactionSigner + + +@dataclasses.dataclass(kw_only=True) +class Account: + """Holds the private_key and address for an account""" + + private_key: str + """Base64 encoded private key""" + address: str = dataclasses.field(default="") + """Address for this account""" + + def __post_init__(self) -> None: + if not self.address: + self.address = algosdk.account.address_from_private_key(self.private_key) + + @property + def public_key(self) -> bytes: + """The public key for this account""" + public_key = algosdk.encoding.decode_address(self.address) + assert isinstance(public_key, bytes) + return public_key + + @property + def signer(self) -> AccountTransactionSigner: + """An AccountTransactionSigner for this account""" + return AccountTransactionSigner(self.private_key) + + @staticmethod + def new_account() -> "Account": + private_key, address = algosdk.account.generate_account() + return Account(private_key=private_key) diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py new file mode 100644 index 00000000..ac86cd3b --- /dev/null +++ b/src/algokit_utils/models/amount.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from decimal import Decimal + +import algosdk +from typing_extensions import Self + + +class AlgoAmount: + def __init__(self, amount: dict[str, int | Decimal]): + if "microAlgos" in amount: + self.amount_in_micro_algo = int(amount["microAlgos"]) + elif "microAlgo" in amount: + self.amount_in_micro_algo = int(amount["microAlgo"]) + elif "algos" in amount: + self.amount_in_micro_algo = algosdk.util.algos_to_microalgos(float(amount["algos"])) + elif "algo" in amount: + self.amount_in_micro_algo = algosdk.util.algos_to_microalgos(float(amount["algo"])) + else: + raise ValueError("Invalid amount provided") + + @property + def micro_algos(self) -> int: + return self.amount_in_micro_algo + + @property + def micro_algo(self) -> int: + return self.amount_in_micro_algo + + @property + def algos(self) -> int | Decimal: + return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] + + @property + def algo(self) -> int | Decimal: + return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] + + @staticmethod + def from_algos(amount: int | Decimal) -> AlgoAmount: + return AlgoAmount({"algos": amount}) + + @staticmethod + def from_algo(amount: int | Decimal) -> AlgoAmount: + return AlgoAmount({"algo": amount}) + + @staticmethod + def from_micro_algos(amount: int | Decimal) -> AlgoAmount: + return AlgoAmount({"microAlgos": amount}) + + @staticmethod + def from_micro_algo(amount: int | Decimal) -> AlgoAmount: + return AlgoAmount({"microAlgo": amount}) + + def __str__(self) -> str: + """Return a string representation of the amount.""" + return f"{self.micro_algo:,} µALGO" + + def __int__(self) -> int: + """Return the amount as an integer number of microAlgos.""" + return self.micro_algos + + def __add__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: + if isinstance(other, AlgoAmount): + total_micro_algos = self.micro_algos + other.micro_algos + elif isinstance(other, (int | Decimal)): + total_micro_algos = self.micro_algos + int(other) + else: + raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") + return AlgoAmount.from_micro_algos(total_micro_algos) + + def __radd__(self, other: int | Decimal) -> AlgoAmount: + return self.__add__(other) + + def __iadd__(self, other: int | Decimal | AlgoAmount) -> Self: + if isinstance(other, AlgoAmount): + self.amount_in_micro_algo += other.micro_algos + elif isinstance(other, (int | Decimal)): + self.amount_in_micro_algo += int(other) + else: + raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") + return self + + def __eq__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo == other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo == int(other) + raise TypeError(f"Unsupported operand type(s) for ==: 'AlgoAmount' and '{type(other).__name__}'") + + def __ne__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo != other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo != int(other) + raise TypeError(f"Unsupported operand type(s) for !=: 'AlgoAmount' and '{type(other).__name__}'") + + def __lt__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo < other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo < int(other) + raise TypeError(f"Unsupported operand type(s) for <: 'AlgoAmount' and '{type(other).__name__}'") + + def __le__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo <= other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo <= int(other) + raise TypeError(f"Unsupported operand type(s) for <=: 'AlgoAmount' and '{type(other).__name__}'") + + def __gt__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo > other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo > int(other) + raise TypeError(f"Unsupported operand type(s) for >: 'AlgoAmount' and '{type(other).__name__}'") + + def __ge__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo >= other.amount_in_micro_algo + elif isinstance(other, int | Decimal): + return self.amount_in_micro_algo >= int(other) + raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'") diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py new file mode 100644 index 00000000..c68e78af --- /dev/null +++ b/src/algokit_utils/models/application.py @@ -0,0 +1,5 @@ +UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" +"""The name of the TEAL template variable for deploy-time immutability control.""" + +DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" +"""The name of the TEAL template variable for deploy-time permanence control.""" diff --git a/src/algokit_utils/models/common.py b/src/algokit_utils/models/common.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py index e69de29b..251bbf96 100644 --- a/src/algokit_utils/transactions/models.py +++ b/src/algokit_utils/transactions/models.py @@ -0,0 +1,30 @@ +from typing import Any, Literal, TypedDict + + +# Define specific types for different formats +class BaseArc2Note(TypedDict): + """Base ARC-0002 transaction note structure""" + + dapp_name: str + + +class StringFormatArc2Note(BaseArc2Note): + """ARC-0002 note for string-based formats (m/b/u)""" + + format: Literal["m", "b", "u"] + data: str + + +class JsonFormatArc2Note(BaseArc2Note): + """ARC-0002 note for JSON format""" + + format: Literal["j"] + data: str | dict[str, Any] | list[Any] | int | None + + +# Combined type for all valid ARC-0002 notes +# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md +Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note + +TransactionNoteData = str | None | int | list[Any] | dict[str, Any] +TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 254b2a30..2d36c06d 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,18 +1,33 @@ -from collections.abc import Callable +from __future__ import annotations + +import math from dataclasses import dataclass -from typing import Union +from typing import TYPE_CHECKING, Union import algosdk -from algosdk.abi import Method +import algosdk.atomic_transaction_composer from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, - AtomicTransactionResponse, TransactionSigner, TransactionWithSigner, ) -from algosdk.box_reference import BoxReference from algosdk.transaction import OnComplete -from algosdk.v2client.algod import AlgodClient +from deprecated import deprecated + +from algokit_utils._debugging import simulate_and_persist_response, simulate_response +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.config import config + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.abi import Method + from algosdk.box_reference import BoxReference + from algosdk.v2client.algod import AlgodClient + + from algokit_utils.models.abi import ABIValue + from algokit_utils.models.amount import AlgoAmount + from algokit_utils.transactions.models import Arc2TransactionNote @dataclass(frozen=True) @@ -22,26 +37,44 @@ class SenderParam: @dataclass(frozen=True) class CommonTxnParams: + """ + Common transaction parameters. + + :param signer: The function used to sign transactions. + :param rekey_to: Change the signing key of the sender to the given address. + :param note: Note to attach to the transaction. + :param lease: Prevent multiple transactions with the same lease being included within the validity window. + :param static_fee: The transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be + covered by another transaction. + :param extra_fee: The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. + :param max_fee: Throw an error if the fee for the transaction is more than this amount. + :param validity_window: How many rounds the transaction should be valid for. + :param first_valid_round: Set the first round this transaction is valid. If left undefined, the value from algod + will be used. Only set this when you intentionally want this to be some time in the future. + :param last_valid_round: The last round this transaction is valid. It is recommended to use validity_window instead. + """ + + sender: str signer: TransactionSigner | None = None rekey_to: str | None = None note: bytes | None = None lease: bytes | None = None - static_fee: int | None = None - extra_fee: int | None = None - max_fee: int | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None validity_window: int | None = None first_valid_round: int | None = None last_valid_round: int | None = None @dataclass(frozen=True) -class _RequiredPayTxnParams(SenderParam): +class _RequiredPaymentParams: receiver: str - amount: int + amount: AlgoAmount @dataclass(frozen=True) -class PayParams(CommonTxnParams, _RequiredPayTxnParams): +class PaymentParams(CommonTxnParams, _RequiredPaymentParams): """ Payment transaction parameters. @@ -54,31 +87,69 @@ class PayParams(CommonTxnParams, _RequiredPayTxnParams): @dataclass(frozen=True) -class _RequiredAssetCreateParams(SenderParam): +class _RequiredAssetCreateParams: total: int + asset_name: str + unit_name: str + url: str @dataclass(frozen=True) -class AssetCreateParams(CommonTxnParams, _RequiredAssetCreateParams): +class AssetCreateParams( + CommonTxnParams, + _RequiredAssetCreateParams, +): + """ + Asset creation parameters. + + :param total: The total amount of the smallest divisible unit to create. + :param decimals: The amount of decimal places the asset should have. + :param default_frozen: Whether the asset is frozen by default in the creator address. + :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. + There will permanently be no manager if undefined or an empty string. + :param reserve: The address that holds the uncirculated supply. + :param freeze: The address that can freeze the asset in any account. + Freezing will be permanently disabled if undefined or an empty string. + :param clawback: The address that can clawback the asset from any account. + Clawback will be permanently disabled if undefined or an empty string. + :param unit_name: The short ticker name for the asset. + :param asset_name: The full name of the asset. + :param url: The metadata URL for the asset. + :param metadata_hash: Hash of the metadata contained in the metadata URL. + """ + decimals: int | None = None default_frozen: bool | None = None manager: str | None = None reserve: str | None = None freeze: str | None = None clawback: str | None = None - unit_name: str | None = None - asset_name: str | None = None - url: str | None = None metadata_hash: bytes | None = None @dataclass(frozen=True) -class _RequiredAssetConfigParams(SenderParam): +class _RequiredAssetConfigParams: asset_id: int @dataclass(frozen=True) -class AssetConfigParams(CommonTxnParams, _RequiredAssetConfigParams): +class AssetConfigParams( + CommonTxnParams, + _RequiredAssetConfigParams, +): + """ + Asset configuration parameters. + + :param asset_id: ID of the asset. + :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. + There will permanently be no manager if undefined or an empty string. + :param reserve: The address that holds the uncirculated supply. + :param freeze: The address that can freeze the asset in any account. + Freezing will be permanently disabled if undefined or an empty string. + :param clawback: The address that can clawback the asset from any account. + Clawback will be permanently disabled if undefined or an empty string. + """ + manager: str | None = None reserve: str | None = None freeze: str | None = None @@ -86,14 +157,17 @@ class AssetConfigParams(CommonTxnParams, _RequiredAssetConfigParams): @dataclass(frozen=True) -class _RequiredAssetFreezeParams(SenderParam): +class _RequiredAssetFreezeParams: asset_id: int account: str frozen: bool @dataclass(frozen=True) -class AssetFreezeParams(CommonTxnParams, _RequiredAssetFreezeParams): +class AssetFreezeParams( + CommonTxnParams, + _RequiredAssetFreezeParams, +): """ Asset freeze parameters. @@ -104,12 +178,15 @@ class AssetFreezeParams(CommonTxnParams, _RequiredAssetFreezeParams): @dataclass(frozen=True) -class _RequiredAssetDestroyParams(SenderParam): +class _RequiredAssetDestroyParams: asset_id: int @dataclass(frozen=True) -class AssetDestroyParams(CommonTxnParams, _RequiredAssetDestroyParams): +class AssetDestroyParams( + CommonTxnParams, + _RequiredAssetDestroyParams, +): """ Asset destruction parameters. @@ -118,7 +195,7 @@ class AssetDestroyParams(CommonTxnParams, _RequiredAssetDestroyParams): @dataclass(frozen=True) -class _RequiredOnlineKeyRegParams(SenderParam): +class _RequiredOnlineKeyRegistrationParams: vote_key: str selection_key: str vote_first: int @@ -127,30 +204,63 @@ class _RequiredOnlineKeyRegParams(SenderParam): @dataclass(frozen=True) -class OnlineKeyRegParams(CommonTxnParams, _RequiredOnlineKeyRegParams): +class OnlineKeyRegistrationParams( + CommonTxnParams, + _RequiredOnlineKeyRegistrationParams, +): + """ + Online key registration parameters. + + :param vote_key: The root participation public key. + :param selection_key: The VRF public key. + :param vote_first: The first round that the participation key is valid. + Not to be confused with the `first_valid` round of the keyreg transaction. + :param vote_last: The last round that the participation key is valid. + Not to be confused with the `last_valid` round of the keyreg transaction. + :param vote_key_dilution: This is the dilution for the 2-level participation key. + It determines the interval (number of rounds) for generating new ephemeral keys. + :param state_proof_key: The 64 byte state proof public key commitment. + """ + state_proof_key: bytes | None = None @dataclass(frozen=True) -class _RequiredAssetTransferParams(SenderParam): +class _RequiredAssetTransferParams: asset_id: int amount: int receiver: str @dataclass(frozen=True) -class AssetTransferParams(CommonTxnParams, _RequiredAssetTransferParams): +class AssetTransferParams( + CommonTxnParams, + _RequiredAssetTransferParams, +): + """ + Asset transfer parameters. + + :param asset_id: ID of the asset. + :param amount: Amount of the asset to transfer (smallest divisible unit). + :param receiver: The account to send the asset to. + :param clawback_target: The account to take the asset from. + :param close_asset_to: The account to close the asset to. + """ + clawback_target: str | None = None close_asset_to: str | None = None @dataclass(frozen=True) -class _RequiredAssetOptInParams(SenderParam): +class _RequiredAssetOptInParams: asset_id: int @dataclass(frozen=True) -class AssetOptInParams(CommonTxnParams, _RequiredAssetOptInParams): +class AssetOptInParams( + CommonTxnParams, + _RequiredAssetOptInParams, +): """ Asset opt-in parameters. @@ -158,6 +268,22 @@ class AssetOptInParams(CommonTxnParams, _RequiredAssetOptInParams): """ +@dataclass(frozen=True) +class _RequiredAssetOptOutParams: + asset_id: int + creator: str + + +@dataclass(frozen=True) +class AssetOptOutParams( + CommonTxnParams, + _RequiredAssetOptOutParams, +): + """ + Asset opt-out parameters. + """ + + @dataclass(frozen=True) class AppCallParams(CommonTxnParams, SenderParam): """ @@ -166,7 +292,7 @@ class AppCallParams(CommonTxnParams, SenderParam): :param on_complete: The OnComplete action. :param app_id: ID of the application. :param approval_program: The program to execute for all OnCompletes other than ClearState. - :param clear_program: The program to execute for ClearState OnComplete. + :param clear_state_program: The program to execute for ClearState OnComplete. :param schema: The state schema for the app. This is immutable. :param args: Application arguments. :param account_references: Account references. @@ -178,8 +304,8 @@ class AppCallParams(CommonTxnParams, SenderParam): on_complete: OnComplete | None = None app_id: int | None = None - approval_program: bytes | None = None - clear_program: bytes | None = None + approval_program: str | bytes | None = None + clear_state_program: str | bytes | None = None schema: dict[str, int] | None = None args: list[bytes] | None = None account_references: list[str] | None = None @@ -190,46 +316,296 @@ class AppCallParams(CommonTxnParams, SenderParam): @dataclass(frozen=True) -class _RequiredMethodCallParams(SenderParam): +class _RequiredAppCreateParams: + approval_program: str | bytes + clear_state_program: str | bytes + + +@dataclass(frozen=True) +class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): + """ + Application create parameters. + + :param approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) + or compiled teal (bytes) + :param clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + :param schema: The state schema for the app. This is immutable. + :param on_complete: The OnComplete action (cannot be ClearState) + :param args: Application arguments + :param account_references: Account references + :param app_references: App references + :param asset_references: Asset references + :param box_references: Box references + :param extra_program_pages: Number of extra pages required for the programs + """ + + schema: dict[str, int] | None = None + on_complete: OnComplete | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + extra_program_pages: int | None = None + + +@dataclass(frozen=True) +class _RequiredAppUpdateParams: + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes + + +@dataclass(frozen=True) +class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): + """ + Application update parameters. + + :param app_id: ID of the application + :param approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) or + compiled teal (bytes) + :param clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) or compiled + teal (bytes) + """ + + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + on_complete: OnComplete | None = None + + +@dataclass(frozen=True) +class _RequiredAppDeleteParams: + app_id: int + + +@dataclass(frozen=True) +class AppDeleteParams( + CommonTxnParams, + SenderParam, + _RequiredAppDeleteParams, +): + """ + Application delete parameters. + + :param app_id: ID of the application + """ + + app_id: int + on_complete: OnComplete = OnComplete.DeleteApplicationOC + + +@dataclass(frozen=True) +class _RequiredMethodCallParams: + app_id: int + method: Method + + +@dataclass(frozen=True) +class AppMethodCall(CommonTxnParams, SenderParam, _RequiredMethodCallParams): + """Base class for ABI method calls.""" + + args: list | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(frozen=True) +class _RequiredAppMethodCallParams: app_id: int method: Method @dataclass(frozen=True) -class MethodCallParams(CommonTxnParams, _RequiredMethodCallParams): +class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallParams): """ Method call parameters. - :param app_id: ID of the application. - :param method: The ABI method to call. - :param args: Arguments to the ABI method. + :param app_id: ID of the application + :param method: The ABI method to call + :param args: Arguments to the ABI method + :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) """ - args: list | None = None + args: list[bytes] | None = None + on_complete: OnComplete | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(frozen=True) +class AppCallMethodCall(AppMethodCall): + """Parameters for a regular ABI method call. + + :param app_id: ID of the application + :param method: The ABI method to call + :param args: Arguments to the ABI method, either: + * An ABI value + * A transaction with explicit signer + * A transaction (where the signer will be automatically assigned) + * Another method call + * None (represents a placeholder transaction argument) + :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) + """ + + app_id: int + on_complete: OnComplete | None = None + + +@dataclass(frozen=True) +class _RequiredAppCreateMethodCallParams: + approval_program: str | bytes + clear_state_program: str | bytes + + +@dataclass(frozen=True) +class AppCreateMethodCall(AppMethodCall, _RequiredAppCreateMethodCallParams): + """Parameters for an ABI method call that creates an application. + + :param approval_program: The program to execute for all OnCompletes other than ClearState + :param clear_state_program: The program to execute for ClearState OnComplete + :param schema: The state schema for the app + :param on_complete: The OnComplete action (cannot be ClearState) + :param extra_program_pages: Number of extra pages required for the programs + """ + + schema: dict[str, int] | None = None + on_complete: OnComplete | None = None + extra_program_pages: int | None = None + + +@dataclass(frozen=True) +class _RequiredAppUpdateMethodCallParams: + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes + + +@dataclass(frozen=True) +class AppUpdateMethodCall(AppMethodCall, _RequiredAppUpdateMethodCallParams): + """Parameters for an ABI method call that updates an application. + + :param app_id: ID of the application + :param approval_program: The program to execute for all OnCompletes other than ClearState + :param clear_state_program: The program to execute for ClearState OnComplete + """ + + on_complete: OnComplete = OnComplete.UpdateApplicationOC + + +@dataclass(frozen=True) +class AppDeleteMethodCall(AppMethodCall): + """Parameters for an ABI method call that deletes an application. + + :param app_id: ID of the application + """ + + app_id: int + on_complete: OnComplete = OnComplete.DeleteApplicationOC + + +# Type alias for all possible method call types +MethodCallParams = AppCallMethodCall | AppCreateMethodCall | AppUpdateMethodCall | AppDeleteMethodCall + + +# Type alias for transaction arguments in method calls +AppMethodCallTransactionArgument = ( + TransactionWithSigner + | algosdk.transaction.Transaction + | AppCreateMethodCall + | AppUpdateMethodCall + | AppCallMethodCall +) TxnParams = Union[ # noqa: UP007 - PayParams, + PaymentParams, AssetCreateParams, AssetConfigParams, AssetFreezeParams, AssetDestroyParams, - OnlineKeyRegParams, + OnlineKeyRegistrationParams, AssetTransferParams, AssetOptInParams, + AssetOptOutParams, AppCallParams, + AppCreateParams, + AppUpdateParams, + AppDeleteParams, MethodCallParams, ] +@dataclass +class BuiltTransactions: + """ + Set of transactions built by TransactionComposer. + + :param transactions: The built transactions. + :param method_calls: Any ABIMethod objects associated with any of the transactions in a map keyed by txn id. + :param signers: Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id. + """ + + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] + signers: dict[int, TransactionSigner] + + +@dataclass +class TransactionComposerBuildResult: + atc: AtomicTransactionComposer + transactions: list[TransactionWithSigner] + method_calls: dict[int, Method] + + class TransactionComposer: - def __init__( + """ + A class for composing and managing Algorand transactions using the Algosdk library. + + Attributes: + txn_method_map (dict[str, algosdk.abi.Method]): A dictionary that maps transaction IDs to their + corresponding ABI methods. + txns (List[Union[TransactionWithSigner, TxnParams, AtomicTransactionComposer]]): A list of transactions + that have not yet been composed. + atc (AtomicTransactionComposer): An instance of AtomicTransactionComposer used to compose transactions. + algod (AlgodClient): The AlgodClient instance used by the composer for suggested params. + get_suggested_params (Callable[[], algosdk.future.transaction.SuggestedParams]): A function that returns + suggested parameters for transactions. + get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a + TransactionSigner for that address. + default_validity_window (int): The default validity window for transactions. + """ + + NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() + + def __init__( # noqa: PLR0913 self, algod: AlgodClient, get_signer: Callable[[str], TransactionSigner], get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None, default_validity_window: int | None = None, + app_manager: AppManager | None = None, ): + """ + Initialize an instance of the TransactionComposer class. + + Args: + algod (AlgodClient): An instance of AlgodClient used to get suggested params and send transactions. + get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and + returns a TransactionSigner for that address. + get_suggested_params (Optional[Callable[[], algosdk.future.transaction.SuggestedParams]], optional): A + function that returns suggested parameters for transactions. If not provided, it defaults to using + algod.suggested_params(). Defaults to None. + default_validity_window (Optional[int], optional): The default validity window for transactions. If not + provided, it defaults to 10. Defaults to None. + """ self.txn_method_map: dict[str, algosdk.abi.Method] = {} self.txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] self.atc: AtomicTransactionComposer = AtomicTransactionComposer() @@ -238,51 +614,195 @@ def __init__( self.get_suggested_params = get_suggested_params or self.default_get_send_params self.get_signer: Callable[[str], TransactionSigner] = get_signer self.default_validity_window: int = default_validity_window or 10 + self.app_manager = app_manager or AppManager(algod) - def add_payment(self, params: PayParams) -> "TransactionComposer": + def add_transaction( + self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None + ) -> TransactionComposer: + self.txns.append(TransactionWithSigner(txn=transaction, signer=signer or self.get_signer(transaction.sender))) + return self + + def add_payment(self, params: PaymentParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_create(self, params: AssetCreateParams) -> "TransactionComposer": + def add_asset_create(self, params: AssetCreateParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_config(self, params: AssetConfigParams) -> "TransactionComposer": + def add_asset_config(self, params: AssetConfigParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_freeze(self, params: AssetFreezeParams) -> "TransactionComposer": + def add_asset_freeze(self, params: AssetFreezeParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_destroy(self, params: AssetDestroyParams) -> "TransactionComposer": + def add_asset_destroy(self, params: AssetDestroyParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_transfer(self, params: AssetTransferParams) -> "TransactionComposer": + def add_asset_transfer(self, params: AssetTransferParams) -> TransactionComposer: self.txns.append(params) return self - def add_asset_opt_in(self, params: AssetOptInParams) -> "TransactionComposer": + def add_asset_opt_in(self, params: AssetOptInParams) -> TransactionComposer: self.txns.append(params) return self - def add_app_call(self, params: AppCallParams) -> "TransactionComposer": + def add_asset_opt_out(self, params: AssetOptOutParams) -> TransactionComposer: self.txns.append(params) return self - def add_online_key_reg(self, params: OnlineKeyRegParams) -> "TransactionComposer": + def add_app_create(self, params: AppCreateParams) -> TransactionComposer: self.txns.append(params) return self - def add_atc(self, atc: AtomicTransactionComposer) -> "TransactionComposer": - self.txns.append(atc) + def add_app_update(self, params: AppUpdateParams) -> TransactionComposer: + self.txns.append(params) return self - def add_method_call(self, params: MethodCallParams) -> "TransactionComposer": + def add_app_delete(self, params: AppDeleteParams) -> TransactionComposer: self.txns.append(params) return self + def add_app_call(self, params: AppCallParams) -> TransactionComposer: + self.txns.append(params) + return self + + def add_app_create_method_call(self, params: AppCreateMethodCall) -> TransactionComposer: + self.txns.append(params) + return self + + def add_app_update_method_call(self, params: AppUpdateMethodCall) -> TransactionComposer: + self.txns.append(params) + return self + + def add_app_delete_method_call(self, params: AppDeleteMethodCall) -> TransactionComposer: + self.txns.append(params) + return self + + def add_app_call_method_call(self, params: AppCallMethodCall) -> TransactionComposer: + self.txns.append(params) + return self + + def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> TransactionComposer: + self.txns.append(params) + return self + + def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer: + self.txns.append(atc) + return self + + def count(self) -> int: + return len(self.build_transactions().transactions) + + def build(self) -> TransactionComposerBuildResult: + if self.atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: + suggested_params = self.get_suggested_params() + txn_with_signers: list[TransactionWithSigner] = [] + + for txn in self.txns: + txn_with_signers.extend(self._build_txn(txn, suggested_params)) + + for ts in txn_with_signers: + self.atc.add_transaction(ts) + method = self.txn_method_map.get(ts.txn.get_txid()) + if method: + self.atc.method_dict[len(self.atc.txn_list) - 1] = method + + return TransactionComposerBuildResult( + atc=self.atc, + transactions=self.atc.build_group(), + method_calls=self.atc.method_dict, + ) + + def rebuild(self) -> TransactionComposerBuildResult: + self.atc = AtomicTransactionComposer() + return self.build() + + def build_transactions(self) -> BuiltTransactions: + suggested_params = self.get_suggested_params() + + transactions: list[algosdk.transaction.Transaction] = [] + method_calls: dict[int, Method] = {} + signers: dict[int, TransactionSigner] = {} + + idx = 0 + + for txn in self.txns: + txn_with_signers: list[TransactionWithSigner] = [] + + if isinstance(txn, MethodCallParams): + txn_with_signers.extend(self._build_method_call(txn, suggested_params)) + else: + txn_with_signers.extend(self._build_txn(txn, suggested_params)) + + for ts in txn_with_signers: + transactions.append(ts.txn) + if ts.signer and ts.signer != self.NULL_SIGNER: + signers[idx] = ts.signer + method = self.txn_method_map.get(ts.txn.get_txid()) + if method: + method_calls[idx] = method + idx += 1 + + return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) + + @deprecated(reason="Use send() instead", version="3.0.0") + def execute( + self, + *, + max_rounds_to_wait: int | None = None, + ) -> algosdk.atomic_transaction_composer.AtomicTransactionResponse: + return self.send( + max_rounds_to_wait=max_rounds_to_wait, + ) + + def send( + self, + max_rounds_to_wait: int | None = None, + ) -> algosdk.atomic_transaction_composer.AtomicTransactionResponse: + group = self.build().transactions + + wait_rounds = max_rounds_to_wait + if wait_rounds is None: + last_round = max(txn.txn.last_valid_round for txn in group) + first_round = self.get_suggested_params().first + wait_rounds = last_round - first_round + 1 + + try: + return self.atc.execute(client=self.algod, wait_rounds=wait_rounds) # TODO: reimplement ATC + except algosdk.error.AlgodHTTPError as e: + raise Exception(f"Transaction failed: {e}") from e + + def simulate(self) -> algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse: + if config.debug and config.project_root and config.trace_all: + return simulate_and_persist_response( + self.atc, + config.project_root, + self.algod, + config.trace_buffer_size_mb, + ) + + return simulate_response( + self.atc, + self.algod, + ) + + @staticmethod + def arc2_note(note: Arc2TransactionNote) -> bytes: + """ + Create an encoded transaction note that follows the ARC-2 spec. + + https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md + + :param note: The ARC-2 note to encode. + """ + + arc2_payload = f"{note['dapp_name']}:{note['format']}{note['data']}" + return arc2_payload.encode("utf-8") + def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSigner]: group = atc.build_group() @@ -291,7 +811,7 @@ def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSign method = atc.method_dict.get(len(group) - 1) if method: - self.txn_method_map[group[-1].txn.get_txid()] = method # type: ignore[no-untyped-call] + self.txn_method_map[group[-1].txn.get_txid()] = method return group @@ -304,7 +824,7 @@ def _common_txn_build_step( if params.lease: txn.lease = params.lease if params.rekey_to: - txn.rekey_to = algosdk.encoding.decode_address(params.rekey_to) # type: ignore[no-untyped-call] + txn.rekey_to = algosdk.encoding.decode_address(params.rekey_to) if params.note: txn.note = params.note @@ -320,9 +840,9 @@ def _common_txn_build_step( raise ValueError("Cannot set both static_fee and extra_fee") if params.static_fee is not None: - txn.fee = params.static_fee + txn.fee = params.static_fee.micro_algos else: - txn.fee = txn.estimate_size() * suggested_params.fee or algosdk.constants.min_txn_fee # type: ignore[no-untyped-call] + txn.fee = txn.estimate_size() * suggested_params.fee or algosdk.constants.min_txn_fee if params.extra_fee: txn.fee += params.extra_fee @@ -331,23 +851,95 @@ def _common_txn_build_step( return txn + def _build_method_call( # noqa: C901, PLR0912 + self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> list[TransactionWithSigner]: + method_args: list[ABIValue | TransactionWithSigner] = [] + arg_offset = 0 + + if params.args: + for i, arg in enumerate(params.args): + if self._is_abi_value(arg): + method_args.append(arg) + continue + + if algosdk.abi.is_abi_transaction_type(params.method.args[i + arg_offset].type): + match arg: + case ( + AppCreateMethodCall() + | AppCallMethodCall() + | AppUpdateMethodCall() + | AppDeleteMethodCall() + ): + temp_txn_with_signers = self._build_method_call(arg, suggested_params) + method_args.extend(temp_txn_with_signers) + arg_offset += len(temp_txn_with_signers) - 1 + continue + case AppCallParams(): + txn = self._build_app_call(arg, suggested_params) + case PaymentParams(): + txn = self._build_payment(arg, suggested_params) + case AssetOptInParams(): + txn = self._build_asset_transfer( + AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params + ) + case AssetCreateParams(): + txn = self._build_asset_create(arg, suggested_params) + case AssetConfigParams(): + txn = self._build_asset_config(arg, suggested_params) + case AssetDestroyParams(): + txn = self._build_asset_destroy(arg, suggested_params) + case AssetFreezeParams(): + txn = self._build_asset_freeze(arg, suggested_params) + case AssetTransferParams(): + txn = self._build_asset_transfer(arg, suggested_params) + case OnlineKeyRegistrationParams(): + txn = self._build_key_reg(arg, suggested_params) + case _: + raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + + method_args.append( + TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + ) + + continue + + raise ValueError(f"Unsupported method arg: {arg!s}") + + method_atc = AtomicTransactionComposer() + + method_atc.add_method_call( + app_id=params.app_id or 0, + method=params.method, + sender=params.sender, + sp=suggested_params, + signer=params.signer or self.get_signer(params.sender), + method_args=method_args, + on_complete=algosdk.transaction.OnComplete.NoOpOC, + note=params.note, + lease=params.lease, + boxes=[(ref.app_index, ref.name) for ref in params.box_references] if params.box_references else None, + ) + + return self._build_atc(method_atc) + def _build_payment( - self, params: PayParams, suggested_params: algosdk.transaction.SuggestedParams + self, params: PaymentParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: txn = algosdk.transaction.PaymentTxn( sender=params.sender, sp=suggested_params, receiver=params.receiver, - amt=params.amount, + amt=params.amount.micro_algos, close_remainder_to=params.close_remainder_to, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) def _build_asset_create( self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetConfigTxn( + txn = algosdk.transaction.AssetCreateTxn( sender=params.sender, sp=suggested_params, total=params.total, @@ -361,46 +953,71 @@ def _build_asset_create( url=params.url, metadata_hash=params.metadata_hash, decimals=params.decimals or 0, - strict_empty_address_check=False, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) def _build_app_call( - self, params: AppCallParams, suggested_params: algosdk.transaction.SuggestedParams + self, + params: AppCallParams | AppUpdateParams | AppCreateParams, + suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: + app_id = params.app_id if isinstance(params, AppCallParams | AppUpdateMethodCall) else None + + approval_program = None + clear_program = None + + if isinstance(params, AppUpdateParams | AppCreateParams): + if isinstance(params.approval_program, str): + approval_program = self.app_manager.compile_teal(params.approval_program).compiled_base64_to_bytes + elif isinstance(params.approval_program, bytes): + approval_program = params.approval_program + + if isinstance(params.clear_state_program, str): + clear_program = self.app_manager.compile_teal(params.clear_state_program).compiled_base64_to_bytes + elif isinstance(params.clear_state_program, bytes): + clear_program = params.clear_state_program + + approval_program_len = len(approval_program) if approval_program else 0 + clear_program_len = len(clear_program) if clear_program else 0 + sdk_params = { "sender": params.sender, "sp": suggested_params, - "index": params.app_id or 0, - "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, - "approval_program": params.approval_program, - "clear_program": params.clear_program, "app_args": params.args, + "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, "accounts": params.account_references, "foreign_apps": params.app_references, "foreign_assets": params.asset_references, - "extra_pages": params.extra_pages, - "local_schema": algosdk.transaction.StateSchema( - num_uints=params.schema.get("local_uints", 0), num_byte_slices=params.schema.get("local_byte_slices", 0) - ) # type: ignore[no-untyped-call] - if params.schema - else None, - "global_schema": algosdk.transaction.StateSchema( - num_uints=params.schema.get("global_uints", 0), - num_byte_slices=params.schema.get("global_byte_slices", 0), - ) # type: ignore[no-untyped-call] - if params.schema - else None, + "boxes": params.box_references, + "approval_program": approval_program, + "clear_program": clear_program, } - if not params.app_id: - if params.approval_program is None or params.clear_program is None: + if not app_id and isinstance(params, AppCreateParams): + if not sdk_params["approval_program"] or not sdk_params["clear_program"]: raise ValueError("approval_program and clear_program are required for application creation") - txn = algosdk.transaction.ApplicationCreateTxn(**sdk_params) # type: ignore[no-untyped-call] + if not params.schema: + raise ValueError("schema is required for application creation") + + txn = algosdk.transaction.ApplicationCreateTxn( + **sdk_params, + global_schema=algosdk.transaction.StateSchema( + num_uints=params.schema.get("global_uints", 0), + num_byte_slices=params.schema.get("global_byte_slices", 0), + ), + local_schema=algosdk.transaction.StateSchema( + num_uints=params.schema.get("local_uints", 0), + num_byte_slices=params.schema.get("local_byte_slices", 0), + ), + extra_pages=params.extra_program_pages + or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) + if params.extra_program_pages + else 0, + ) else: - txn = algosdk.transaction.ApplicationCallTxn(**sdk_params) # type: ignore[assignment,no-untyped-call] + txn = algosdk.transaction.ApplicationCallTxn(**sdk_params, index=app_id) # type: ignore[assignment] return self._common_txn_build_step(params, txn, suggested_params) @@ -416,7 +1033,7 @@ def _build_asset_config( freeze=params.freeze, clawback=params.clawback, strict_empty_address_check=False, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) @@ -427,7 +1044,7 @@ def _build_asset_destroy( sender=params.sender, sp=suggested_params, index=params.asset_id, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) @@ -440,7 +1057,7 @@ def _build_asset_freeze( index=params.asset_id, target=params.account, new_freeze_state=params.frozen, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) @@ -455,12 +1072,12 @@ def _build_asset_transfer( index=params.asset_id, close_assets_to=params.close_asset_to, revocation_target=params.clawback_target, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) def _build_key_reg( - self, params: OnlineKeyRegParams, suggested_params: algosdk.transaction.SuggestedParams + self, params: OnlineKeyRegistrationParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: txn = algosdk.transaction.KeyregTxn( sender=params.sender, @@ -473,7 +1090,7 @@ def _build_key_reg( rekey_to=params.rekey_to, nonpart=False, sprfkey=params.state_proof_key, - ) # type: ignore[no-untyped-call] + ) return self._common_txn_build_step(params, txn, suggested_params) @@ -483,72 +1100,6 @@ def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> boo return isinstance(x, bool | int | float | str | bytes) - def _build_method_call( # noqa: C901, PLR0912 - self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> list[TransactionWithSigner]: - method_args = [] - arg_offset = 0 - - if params.args: - for i, arg in enumerate(params.args): - if self._is_abi_value(arg): - method_args.append(arg) - continue - - if algosdk.abi.is_abi_transaction_type(params.method.args[i + arg_offset].type): - match arg: - case MethodCallParams(): - temp_txn_with_signers = self._build_method_call(arg, suggested_params) - method_args.extend(temp_txn_with_signers) - arg_offset += len(temp_txn_with_signers) - 1 - continue - case AppCallParams(): - txn = self._build_app_call(arg, suggested_params) - case PayParams(): - txn = self._build_payment(arg, suggested_params) - case AssetOptInParams(): - txn = self._build_asset_transfer( - AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params - ) - case AssetCreateParams(): - txn = self._build_asset_create(arg, suggested_params) - case AssetConfigParams(): - txn = self._build_asset_config(arg, suggested_params) - case AssetDestroyParams(): - txn = self._build_asset_destroy(arg, suggested_params) - case AssetFreezeParams(): - txn = self._build_asset_freeze(arg, suggested_params) - case AssetTransferParams(): - txn = self._build_asset_transfer(arg, suggested_params) - case OnlineKeyRegParams(): - txn = self._build_key_reg(arg, suggested_params) - case _: - raise ValueError(f"Unsupported method arg transaction type: {arg}") - - method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) - ) - - continue - - raise ValueError(f"Unsupported method arg: {arg}") - - method_atc = AtomicTransactionComposer() - - method_atc.add_method_call( - app_id=params.app_id or 0, - method=params.method, - sender=params.sender, - sp=suggested_params, - signer=params.signer or self.get_signer(params.sender), - method_args=method_args, - on_complete=algosdk.transaction.OnComplete.NoOpOC, - note=params.note, - lease=params.lease, - ) - - return self._build_atc(method_atc) - def _build_txn( # noqa: C901, PLR0912, PLR0911 self, txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, @@ -559,19 +1110,19 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 return [txn] case AtomicTransactionComposer(): return self._build_atc(txn) - case MethodCallParams(): + case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): return self._build_method_call(txn, suggested_params) signer = txn.signer or self.get_signer(txn.sender) match txn: - case PayParams(): + case PaymentParams(): payment = self._build_payment(txn, suggested_params) return [TransactionWithSigner(txn=payment, signer=signer)] case AssetCreateParams(): asset_create = self._build_asset_create(txn, suggested_params) return [TransactionWithSigner(txn=asset_create, signer=signer)] - case AppCallParams(): + case AppCallParams() | AppUpdateParams() | AppCreateParams(): app_call = self._build_app_call(txn, suggested_params) return [TransactionWithSigner(txn=app_call, signer=signer)] case AssetConfigParams(): @@ -591,42 +1142,8 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params ) return [TransactionWithSigner(txn=asset_transfer, signer=signer)] - case OnlineKeyRegParams(): + case OnlineKeyRegistrationParams(): key_reg = self._build_key_reg(txn, suggested_params) return [TransactionWithSigner(txn=key_reg, signer=signer)] case _: raise ValueError(f"Unsupported txn: {txn}") - - def build_group(self) -> list[TransactionWithSigner]: - suggested_params = self.get_suggested_params() - - txn_with_signers: list[TransactionWithSigner] = [] - - for txn in self.txns: - txn_with_signers.extend(self._build_txn(txn, suggested_params)) - - for ts in txn_with_signers: - self.atc.add_transaction(ts) - - method_calls = {} - - for i, ts in enumerate(txn_with_signers): - method = self.txn_method_map.get(ts.txn.get_txid()) # type: ignore[no-untyped-call] - if method: - method_calls[i] = method - - self.atc.method_dict = method_calls - - return self.atc.build_group() - - def execute(self, *, max_rounds_to_wait: int | None = None) -> AtomicTransactionResponse: - group = self.build_group() - - wait_rounds = max_rounds_to_wait - - if wait_rounds is None: - last_round = max(txn.txn.last_valid_round for txn in group) - first_round = self.get_suggested_params().first - wait_rounds = last_round - first_round - - return self.atc.execute(client=self.algod, wait_rounds=wait_rounds) diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py new file mode 100644 index 00000000..e4ae0e03 --- /dev/null +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -0,0 +1,2 @@ +class AlgorandClientTransactionCreator: + """A creator for Algorand transactions""" diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py new file mode 100644 index 00000000..41c5aa4b --- /dev/null +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -0,0 +1,88 @@ +import logging +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from algosdk.transaction import wait_for_confirmation +from algosdk.v2client.algod import AlgodClient + +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.transactions.transaction_composer import ( + PaymentParams, + TransactionComposer, +) + +logger = logging.getLogger(__name__) +TxnParam = TypeVar("TxnParam") +TxnResult = TypeVar("TxnResult") + + +@dataclass +class SendTransactionResult(Generic[TxnResult]): + """Result of sending a transaction""" + + confirmation: dict[str, Any] + tx_id: str + return_value: TxnResult | None = None + + +class AlgorandClientTransactionSender: + """Orchestrates sending transactions for AlgorandClient.""" + + def __init__( + self, + new_group: Callable[[], TransactionComposer], + asset_manager: AssetManager, + app_manager: AppManager, + algod_client: AlgodClient, + ) -> None: + self._new_group = new_group + self._asset_manager = asset_manager + self._app_manager = app_manager + self._algod_client = algod_client + + def new_group(self) -> TransactionComposer: + """Create a new transaction group""" + return self._new_group() + + def _send( + self, + c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]], + log: dict[str, Callable[[TxnParam, Any], str]] | None = None, + ) -> Callable[[TxnParam], SendTransactionResult[Any]]: + """Generic method to send transactions with logging.""" + + def send_transaction(params: TxnParam) -> SendTransactionResult[Any]: + composer = self._new_group() + c(composer)(params) + + if log and log.get("pre_log"): + transaction = composer.build().transactions[-1].txn + logger.debug(log["pre_log"](params, transaction)) + + result = composer.send() + + if log and log.get("post_log"): + logger.debug(log["post_log"](params, result)) + + confirmation = wait_for_confirmation(self._algod_client, result.tx_ids[0]) + return SendTransactionResult( + confirmation=confirmation, + tx_id=result.tx_ids[0], + ) + + return send_transaction + + @property + def payment(self) -> Callable[[PaymentParams], SendTransactionResult[None]]: + """Send a payment transaction""" + return self._send( + lambda c: c.add_payment, + { + "pre_log": lambda params, txn: ( + f"Sending {params.amount.micro_algos} µALGO from {params.sender} " + f"to {params.receiver} via transaction {txn.get_txid()}" + ) + }, + ) diff --git a/src/algokit_utils/accounts/models.py b/tests/applications/__init__.py similarity index 100% rename from src/algokit_utils/accounts/models.py rename to tests/applications/__init__.py diff --git a/tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt b/tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt new file mode 100644 index 00000000..3795ccbf --- /dev/null +++ b/tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt @@ -0,0 +1,30 @@ + + +op arg +op "arg" +op "//" +op " //comment " +op "\" //" +op "// \" //" +op "" + +op 123 +op 123 +op "" +op "//" +op "//" +pushbytes base64(//8=) +pushbytes b64(//8=) + +pushbytes base64(//8=) +pushbytes b64(//8=) +pushbytes "base64(//8=)" +pushbytes "b64(//8=)" + +pushbytes base64 //8= +pushbytes b64 //8= + +pushbytes base64 //8= +pushbytes b64 //8= +pushbytes "base64 //8=" +pushbytes "b64 //8=" diff --git a/tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt b/tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt new file mode 100644 index 00000000..6cbde085 --- /dev/null +++ b/tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt @@ -0,0 +1,21 @@ + +test 123 // TMPL_INT +test 123 +no change +test 0x414243 // TMPL_STR +0x414243 +0x414243 // TMPL_INT +0x414243 // foo // +0x414243 // bar +test "TMPL_STR" // not replaced +test "TMPL_STRING" // not replaced +test TMPL_STRING // not replaced +test TMPL_STRI // not replaced +test 0x414243 123 123 0x414243 // TMPL_STR TMPL_INT TMPL_INT TMPL_STR +test 123 0x414243 TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING +test 123 123 TMPL_STRING TMPL_STRING TMPL_STRING 123 TMPL_STRING //keep +0x414243 0x414243 0x414243 +TMPL_STRING +test NOTTMPL_STR // not replaced +NOTTMPL_STR // not replaced +0x414243 // replaced \ No newline at end of file diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py new file mode 100644 index 00000000..8c9c1002 --- /dev/null +++ b/tests/applications/test_app_manager.py @@ -0,0 +1,77 @@ +import pytest +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.account import Account + +from tests.conftest import check_output_stability + + +@pytest.fixture() +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +def test_template_substitution() -> None: + program = """ +test TMPL_INT // TMPL_INT +test TMPL_INT +no change +test TMPL_STR // TMPL_STR +TMPL_STR +TMPL_STR // TMPL_INT +TMPL_STR // foo // +TMPL_STR // bar +test "TMPL_STR" // not replaced +test "TMPL_STRING" // not replaced +test TMPL_STRING // not replaced +test TMPL_STRI // not replaced +test TMPL_STR TMPL_INT TMPL_INT TMPL_STR // TMPL_STR TMPL_INT TMPL_INT TMPL_STR +test TMPL_INT TMPL_STR TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING +test TMPL_INT TMPL_INT TMPL_STRING TMPL_STRING TMPL_STRING TMPL_INT TMPL_STRING //keep +TMPL_STR TMPL_STR TMPL_STR +TMPL_STRING +test NOTTMPL_STR // not replaced +NOTTMPL_STR // not replaced +TMPL_STR // replaced +""" + result = AppManager.replace_template_variables(program, {"INT": 123, "STR": "ABC"}) + check_output_stability(result) + + +def test_comment_stripping() -> None: + program = r""" +//comment +op arg //comment +op "arg" //comment +op "//" //comment +op " //comment " //comment +op "\" //" //comment +op "// \" //" //comment +op "" //comment +// +op 123 +op 123 // something +op "" // more comments +op "//" //op "//" +op "//" +pushbytes base64(//8=) +pushbytes b64(//8=) + +pushbytes base64(//8=) // pushbytes base64(//8=) +pushbytes b64(//8=) // pushbytes b64(//8=) +pushbytes "base64(//8=)" // pushbytes "base64(//8=)" +pushbytes "b64(//8=)" // pushbytes "b64(//8=)" + +pushbytes base64 //8= +pushbytes b64 //8= + +pushbytes base64 //8= // pushbytes base64 //8= +pushbytes b64 //8= // pushbytes b64 //8= +pushbytes "base64 //8=" // pushbytes "base64 //8=" +pushbytes "b64 //8=" // pushbytes "b64 //8=" + +""" + result = AppManager.strip_teal_comments(program) + check_output_stability(result) diff --git a/src/algokit_utils/applications/models.py b/tests/clients/__init__.py similarity index 100% rename from src/algokit_utils/applications/models.py rename to tests/clients/__init__.py diff --git a/tests/clients/test_algorand_client.py b/tests/clients/test_algorand_client.py new file mode 100644 index 00000000..ce0f90d5 --- /dev/null +++ b/tests/clients/test_algorand_client.py @@ -0,0 +1,223 @@ +# TODO: Update tests for latest version of algokit-utils +# import json +# from pathlib import Path + +# import pytest +# from algokit_utils import Account, ApplicationClient +# from algokit_utils.accounts.account_manager import AddressAndSigner +# from algokit_utils.clients.algorand_client import ( +# AlgorandClient, +# AppMethodCallParams, +# AssetCreateParams, +# AssetOptInParams, +# PaymentParams, +# ) +# from algosdk.abi import Contract +# from algosdk.atomic_transaction_composer import AtomicTransactionComposer + + +# @pytest.fixture() +# def algorand(funded_account: Account) -> AlgorandClient: +# client = AlgorandClient.default_local_net() +# client.set_signer(sender=funded_account.address, signer=funded_account.signer) +# return client + + +# @pytest.fixture() +# def alice(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: +# acct = algorand.account.random() +# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) +# return acct + + +# @pytest.fixture() +# def bob(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: +# acct = algorand.account.random() +# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) +# return acct + + +# @pytest.fixture() +# def app_client(algorand: AlgorandClient, alice: AddressAndSigner) -> ApplicationClient: +# client = ApplicationClient( +# algorand.client.algod, +# Path(__file__).parent / "app_algorand_client.json", +# sender=alice.address, +# signer=alice.signer, +# ) +# client.create(call_abi_method="createApplication") +# return client + + +# @pytest.fixture() +# def contract() -> Contract: +# with Path.open(Path(__file__).parent / "app_algorand_client.json") as f: +# return Contract.from_json(json.dumps(json.load(f)["contract"])) + + +# def test_send_payment(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: +# amount = 100_000 + +# alice_pre_balance = algorand.account.get_information(alice.address)["amount"] +# bob_pre_balance = algorand.account.get_information(bob.address)["amount"] +# result = algorand.send.payment(PaymentParams(sender=alice.address, receiver=bob.address, amount=amount)) +# alice_post_balance = algorand.account.get_information(alice.address)["amount"] +# bob_post_balance = algorand.account.get_information(bob.address)["amount"] + +# assert result["confirmation"] is not None +# assert alice_post_balance == alice_pre_balance - 1000 - amount +# assert bob_post_balance == bob_pre_balance + amount + + +# def test_send_asset_create(algorand: AlgorandClient, alice: AddressAndSigner) -> None: +# total = 100 + +# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) +# asset_index = result["confirmation"]["asset-index"] + +# assert asset_index > 0 + + +# def test_asset_opt_in(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: +# total = 100 + +# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) +# asset_index = result["confirmation"]["asset-index"] + +# algorand.send.asset_opt_in(AssetOptInParams(sender=bob.address, asset_id=asset_index)) + +# assert algorand.account.get_asset_information(bob.address, asset_index) is not None + + +# DO_MATH_VALUE = 3 + + +# def test_add_atc(algorand: AlgorandClient, app_client: ApplicationClient, alice: AddressAndSigner) -> None: +# atc = AtomicTransactionComposer() +# app_client.compose_call(atc, call_abi_method="doMath", a=1, b=2, operation="sum") + +# result = ( +# algorand.new_group() +# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) +# .add_atc(atc) +# .execute() +# ) +# assert result.abi_results[0].return_value == DO_MATH_VALUE + + +# def test_add_method_call( +# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient +# ) -> None: +# result = ( +# algorand.new_group() +# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) +# .add_method_call( +# AppMethodCallParams( +# method=contract.get_method_by_name("doMath"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[1, 2, "sum"], +# ) +# ) +# .execute() +# ) +# assert result.abi_results[0].return_value == DO_MATH_VALUE + + +# def test_add_method_with_txn_arg( +# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient +# ) -> None: +# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) +# result = ( +# algorand.new_group() +# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) +# .add_method_call( +# AppMethodCallParams( +# method=contract.get_method_by_name("txnArg"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[pay_arg], +# ) +# ) +# .execute() +# ) +# assert result.abi_results[0].return_value == alice.address + + +# def test_add_method_call_with_method_call_arg( +# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient +# ) -> None: +# hello_world_call = AppMethodCallParams( +# method=contract.get_method_by_name("helloWorld"), sender=alice.address, app_id=app_client.app_id +# ) +# result = ( +# algorand.new_group() +# .add_method_call( +# AppMethodCallParams( +# method=contract.get_method_by_name("methodArg"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[hello_world_call], +# ) +# ) +# .execute() +# ) +# assert result.abi_results[0].return_value == "Hello, World!" +# assert result.abi_results[1].return_value == app_client.app_id + + +# def test_add_method_call_with_method_call_arg_with_txn_arg( +# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient +# ) -> None: +# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) +# txn_arg_call = AppMethodCallParams( +# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg] +# ) +# result = ( +# algorand.new_group() +# .add_method_call( +# AppMethodCallParams( +# method=contract.get_method_by_name("nestedTxnArg"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[txn_arg_call], +# ) +# ) +# .execute() +# ) +# assert result.abi_results[0].return_value == alice.address +# assert result.abi_results[1].return_value == app_client.app_id + + +# def test_add_method_call_with_two_method_call_args_with_txn_arg( +# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient +# ) -> None: +# pay_arg_1 = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) +# txn_arg_call_1 = AppMethodCallParams( +# method=contract.get_method_by_name("txnArg"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[pay_arg_1], +# note=b"1", +# ) + +# pay_arg_2 = PaymentParams(sender=alice.address, receiver=alice.address, amount=2) +# txn_arg_call_2 = AppMethodCallParams( +# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg_2] +# ) + +# result = ( +# algorand.new_group() +# .add_method_call( +# AppMethodCallParams( +# method=contract.get_method_by_name("doubleNestedTxnArg"), +# sender=alice.address, +# app_id=app_client.app_id, +# args=[txn_arg_call_1, txn_arg_call_2], +# ) +# ) +# .execute() +# ) +# assert result.abi_results[0].return_value == alice.address +# assert result.abi_results[1].return_value == alice.address +# assert result.abi_results[2].return_value == app_client.app_id diff --git a/tests/conftest.py b/tests/conftest.py index e3997a2c..18021c21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,7 @@ def check_output_stability(logs: str, *, test_name: str | None = None) -> None: caller_dir = caller_path.parent test_name = test_name or caller_frame.function caller_stem = Path(caller_frame.filename).stem - output_dir = caller_dir / f"{caller_stem}.approvals" + output_dir = caller_dir / "snapshots" / f"{caller_stem}.approvals" output_dir.mkdir(exist_ok=True) output_file = output_dir / f"{test_name}.approved.txt" output_file_str = str(output_file) @@ -188,11 +188,11 @@ def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int note=None, lease=None, rekey_to=None, - ) # type: ignore[no-untyped-call] + ) - signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call] + signed_transaction = txn.sign(sender.private_key) algod_client.send_transaction(signed_transaction) - ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call] + ptx = algod_client.pending_transaction_info(txn.get_txid()) if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): return ptx["asset-index"] diff --git a/tests/test_algorand_client.py b/tests/test_algorand_client.py deleted file mode 100644 index 8b7c448d..00000000 --- a/tests/test_algorand_client.py +++ /dev/null @@ -1,222 +0,0 @@ -import json -from pathlib import Path - -import pytest -from algokit_utils import Account, ApplicationClient -from algokit_utils.accounts.account_manager import AddressAndSigner -from algokit_utils.clients.algorand_client import ( - AlgorandClient, - AssetCreateParams, - AssetOptInParams, - MethodCallParams, - PayParams, -) -from algosdk.abi import Contract -from algosdk.atomic_transaction_composer import AtomicTransactionComposer - - -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client - - -@pytest.fixture() -def alice(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: - acct = algorand.account.random() - algorand.send.payment(PayParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) - return acct - - -@pytest.fixture() -def bob(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: - acct = algorand.account.random() - algorand.send.payment(PayParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) - return acct - - -@pytest.fixture() -def app_client(algorand: AlgorandClient, alice: AddressAndSigner) -> ApplicationClient: - client = ApplicationClient( - algorand.client.algod, - Path(__file__).parent / "app_algorand_client.json", - sender=alice.address, - signer=alice.signer, - ) - client.create(call_abi_method="createApplication") - return client - - -@pytest.fixture() -def contract() -> Contract: - with Path.open(Path(__file__).parent / "app_algorand_client.json") as f: - return Contract.from_json(json.dumps(json.load(f)["contract"])) - - -def test_send_payment(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: - amount = 100_000 - - alice_pre_balance = algorand.account.get_information(alice.address)["amount"] - bob_pre_balance = algorand.account.get_information(bob.address)["amount"] - result = algorand.send.payment(PayParams(sender=alice.address, receiver=bob.address, amount=amount)) - alice_post_balance = algorand.account.get_information(alice.address)["amount"] - bob_post_balance = algorand.account.get_information(bob.address)["amount"] - - assert result["confirmation"] is not None - assert alice_post_balance == alice_pre_balance - 1000 - amount - assert bob_post_balance == bob_pre_balance + amount - - -def test_send_asset_create(algorand: AlgorandClient, alice: AddressAndSigner) -> None: - total = 100 - - result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) - asset_index = result["confirmation"]["asset-index"] - - assert asset_index > 0 - - -def test_asset_opt_in(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: - total = 100 - - result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) - asset_index = result["confirmation"]["asset-index"] - - algorand.send.asset_opt_in(AssetOptInParams(sender=bob.address, asset_id=asset_index)) - - assert algorand.account.get_asset_information(bob.address, asset_index) is not None - - -DO_MATH_VALUE = 3 - - -def test_add_atc(algorand: AlgorandClient, app_client: ApplicationClient, alice: AddressAndSigner) -> None: - atc = AtomicTransactionComposer() - app_client.compose_call(atc, call_abi_method="doMath", a=1, b=2, operation="sum") - - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_atc(atc) - .execute() - ) - assert result.abi_results[0].return_value == DO_MATH_VALUE - - -def test_add_method_call( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("doMath"), - sender=alice.address, - app_id=app_client.app_id, - args=[1, 2, "sum"], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == DO_MATH_VALUE - - -def test_add_method_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg = PayParams(sender=alice.address, receiver=alice.address, amount=1) - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("txnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[pay_arg], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - - -def test_add_method_call_with_method_call_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - hello_world_call = MethodCallParams( - method=contract.get_method_by_name("helloWorld"), sender=alice.address, app_id=app_client.app_id - ) - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("methodArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[hello_world_call], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == "Hello, World!" - assert result.abi_results[1].return_value == app_client.app_id - - -def test_add_method_call_with_method_call_arg_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg = PayParams(sender=alice.address, receiver=alice.address, amount=1) - txn_arg_call = MethodCallParams( - method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg] - ) - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("nestedTxnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[txn_arg_call], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - assert result.abi_results[1].return_value == app_client.app_id - - -def test_add_method_call_with_two_method_call_args_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg_1 = PayParams(sender=alice.address, receiver=alice.address, amount=1) - txn_arg_call_1 = MethodCallParams( - method=contract.get_method_by_name("txnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[pay_arg_1], - note=b"1", - ) - - pay_arg_2 = PayParams(sender=alice.address, receiver=alice.address, amount=2) - txn_arg_call_2 = MethodCallParams( - method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg_2] - ) - - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("doubleNestedTxnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[txn_arg_call_1, txn_arg_call_2], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - assert result.abi_results[1].return_value == alice.address - assert result.abi_results[2].return_value == app_client.app_id diff --git a/tests/test_transaction_composer.py b/tests/test_transaction_composer.py new file mode 100644 index 00000000..1cbab861 --- /dev/null +++ b/tests/test_transaction_composer.py @@ -0,0 +1,212 @@ +from typing import TYPE_CHECKING + +import pytest +from algokit_utils._legacy_v2.account import get_account +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + PaymentParams, + TransactionComposer, +) +from algosdk.atomic_transaction_composer import ( + AtomicTransactionResponse, +) +from algosdk.transaction import ( + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + +from legacy_v2_tests.conftest import get_unique_name + +if TYPE_CHECKING: + from algokit_utils.transactions.models import Arc2TransactionNote + + +@pytest.fixture() +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +@pytest.fixture() +def funded_secondary_account(algorand: AlgorandClient) -> Account: + secondary_name = get_unique_name() + return get_account(algorand.client.algod, secondary_name) + + +def test_add_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + txn = PaymentTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + receiver=funded_account.address, + amt=AlgoAmount.from_algos(1).micro_algos, + ) + composer.add_transaction(txn) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], PaymentTxn) + assert built.transactions[0].sender == funded_account.address + assert built.transactions[0].receiver == funded_account.address + assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos + + +def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + expected_total = 1000 + params = AssetCreateParams( + sender=funded_account.address, + total=expected_total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + + composer.add_asset_create(params) + built = composer.build_transactions() + response = composer.execute(max_rounds_to_wait=20) + created_asset = algorand.client.algod.asset_info( + algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] + )["params"] + + assert len(response.tx_ids) == 1 + assert response.confirmed_round > 0 + assert isinstance(built.transactions[0], AssetCreateTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert created_asset["creator"] == funded_account.address + assert txn.total == created_asset["total"] == expected_total + assert txn.decimals == created_asset["decimals"] == 0 + assert txn.default_frozen == created_asset["default-frozen"] is False + assert txn.unit_name == created_asset["unit-name"] == "TEST" + assert txn.asset_name == created_asset["name"] == "Test Asset" + + +def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account) -> None: + # First create an asset + asset_txn = AssetCreateTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + total=1000, + decimals=0, + default_frozen=False, + unit_name="CFG", + asset_name="Configurable Asset", + manager=funded_account.address, + ) + signed_asset_txn = asset_txn.sign(funded_account.signer.private_key) + tx_id = algorand.client.algod.send_transaction(signed_asset_txn) + asset_before_config = algorand.client.algod.asset_info( + algorand.client.algod.pending_transaction_info(tx_id)["asset-index"] # type: ignore[call-overload] + ) + asset_before_config_index = asset_before_config["index"] # type: ignore[call-overload] + + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + params = AssetConfigParams( + sender=funded_account.address, + asset_id=asset_before_config_index, + manager=funded_secondary_account.address, + ) + composer.add_asset_config(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], AssetConfigTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.index == asset_before_config_index + assert txn.manager == funded_secondary_account.address + + composer.execute(max_rounds_to_wait=20) + updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] + assert updated_asset["manager"] == funded_secondary_account.address + + +def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + params = AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + composer.add_app_create(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], ApplicationCreateTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + composer.execute(max_rounds_to_wait=20) + + +def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + composer.build() + simulate_response = composer.simulate() + assert simulate_response + + +def test_send(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + response = composer.send() + assert isinstance(response, AtomicTransactionResponse) + assert len(response.tx_ids) == 1 + assert response.confirmed_round > 0 + + +def test_arc2_note() -> None: + note_data: Arc2TransactionNote = { + "dapp_name": "TestDApp", + "format": "j", + "data": '{"key":"value"}', + } + encoded_note = TransactionComposer.arc2_note(note_data) + expected_note = b'TestDApp:j{"key":"value"}' + assert encoded_note == expected_note diff --git a/src/algokit_utils/assets/models.py b/tests/transactions/__init__.py similarity index 100% rename from src/algokit_utils/assets/models.py rename to tests/transactions/__init__.py diff --git a/tests/transactions/artifacts/hello_world/approval.teal b/tests/transactions/artifacts/hello_world/approval.teal new file mode 100644 index 00000000..d38f6432 --- /dev/null +++ b/tests/transactions/artifacts/hello_world/approval.teal @@ -0,0 +1,62 @@ +#pragma version 10 + +smart_contracts.hello_world.contract.HelloWorld.approval_program: + intcblock 0 1 + callsub __puya_arc4_router__ + return + + +// smart_contracts.hello_world.contract.HelloWorld.__puya_arc4_router__() -> uint64: +__puya_arc4_router__: + proto 0 1 + txn NumAppArgs + bz __puya_arc4_router___bare_routing@5 + pushbytes 0x02bece11 // method "hello(string)string" + txna ApplicationArgs 0 + match __puya_arc4_router___hello_route@2 + intc_0 // 0 + retsub + +__puya_arc4_router___hello_route@2: + txn OnCompletion + ! + assert // OnCompletion is NoOp + txn ApplicationID + assert // is not creating + txna ApplicationArgs 1 + extract 2 0 + callsub hello + dup + len + itob + extract 6 2 + swap + concat + pushbytes 0x151f7c75 + swap + concat + log + intc_1 // 1 + retsub + +__puya_arc4_router___bare_routing@5: + txn OnCompletion + bnz __puya_arc4_router___after_if_else@9 + txn ApplicationID + ! + assert // is creating + intc_1 // 1 + retsub + +__puya_arc4_router___after_if_else@9: + intc_0 // 0 + retsub + + +// smart_contracts.hello_world.contract.HelloWorld.hello(name: bytes) -> bytes: +hello: + proto 1 1 + pushbytes "Hello, " + frame_dig -1 + concat + retsub diff --git a/tests/transactions/artifacts/hello_world/clear.teal b/tests/transactions/artifacts/hello_world/clear.teal new file mode 100644 index 00000000..5a70c80b --- /dev/null +++ b/tests/transactions/artifacts/hello_world/clear.teal @@ -0,0 +1,5 @@ +#pragma version 10 + +smart_contracts.hello_world.contract.HelloWorld.clear_state_program: + pushint 1 // 1 + return diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py new file mode 100644 index 00000000..0a75961a --- /dev/null +++ b/tests/transactions/test_transaction_composer.py @@ -0,0 +1,256 @@ +from pathlib import Path +from typing import TYPE_CHECKING + +import algosdk +import pytest +from algokit_utils._legacy_v2.account import get_account +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + PaymentParams, + TransactionComposer, +) +from algosdk.atomic_transaction_composer import ( + AtomicTransactionResponse, +) +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + +from legacy_v2_tests.conftest import get_unique_name + +if TYPE_CHECKING: + from algokit_utils.transactions.models import Arc2TransactionNote + + +@pytest.fixture() +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +@pytest.fixture() +def funded_secondary_account(algorand: AlgorandClient) -> Account: + secondary_name = get_unique_name() + return get_account(algorand.client.algod, secondary_name) + + +def test_add_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + txn = PaymentTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + receiver=funded_account.address, + amt=AlgoAmount.from_algos(1).micro_algos, + ) + composer.add_transaction(txn) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], PaymentTxn) + assert built.transactions[0].sender == funded_account.address + assert built.transactions[0].receiver == funded_account.address + assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos + + +def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + expected_total = 1000 + params = AssetCreateParams( + sender=funded_account.address, + total=expected_total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + + composer.add_asset_create(params) + built = composer.build_transactions() + response = composer.execute(max_rounds_to_wait=20) + created_asset = algorand.client.algod.asset_info( + algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] + )["params"] + + assert len(response.tx_ids) == 1 + assert response.confirmed_round > 0 + assert isinstance(built.transactions[0], AssetCreateTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert created_asset["creator"] == funded_account.address + assert txn.total == created_asset["total"] == expected_total + assert txn.decimals == created_asset["decimals"] == 0 + assert txn.default_frozen == created_asset["default-frozen"] is False + assert txn.unit_name == created_asset["unit-name"] == "TEST" + assert txn.asset_name == created_asset["name"] == "Test Asset" + + +def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account) -> None: + # First create an asset + asset_txn = AssetCreateTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + total=1000, + decimals=0, + default_frozen=False, + unit_name="CFG", + asset_name="Configurable Asset", + manager=funded_account.address, + ) + signed_asset_txn = asset_txn.sign(funded_account.signer.private_key) + tx_id = algorand.client.algod.send_transaction(signed_asset_txn) + asset_before_config = algorand.client.algod.asset_info( + algorand.client.algod.pending_transaction_info(tx_id)["asset-index"] # type: ignore[call-overload] + ) + asset_before_config_index = asset_before_config["index"] # type: ignore[call-overload] + + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + params = AssetConfigParams( + sender=funded_account.address, + asset_id=asset_before_config_index, + manager=funded_secondary_account.address, + ) + composer.add_asset_config(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], AssetConfigTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.index == asset_before_config_index + assert txn.manager == funded_secondary_account.address + + composer.execute(max_rounds_to_wait=20) + updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] + assert updated_asset["manager"] == funded_secondary_account.address + + +def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + params = AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + composer.add_app_create(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], ApplicationCreateTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + composer.execute(max_rounds_to_wait=20) + + +def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + composer.add_app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + ) + response = composer.execute() + app_id = algorand.client.algod.pending_transaction_info(response.tx_ids[0])["application-index"] # type: ignore[call-overload] + + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_app_call_method_call( + AppCallMethodCall( + sender=funded_account.address, + app_id=app_id, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["world"], + ) + ) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], ApplicationCallTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + response = composer.execute(max_rounds_to_wait=20) + assert response.abi_results[0].return_value == "Hello, world" + + +def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + composer.build() + simulate_response = composer.simulate() + assert simulate_response + + +def test_send(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + response = composer.send() + assert isinstance(response, AtomicTransactionResponse) + assert len(response.tx_ids) == 1 + assert response.confirmed_round > 0 + + +def test_arc2_note() -> None: + note_data: Arc2TransactionNote = { + "dapp_name": "TestDApp", + "format": "j", + "data": '{"key":"value"}', + } + encoded_note = TransactionComposer.arc2_note(note_data) + expected_note = b'TestDApp:j{"key":"value"}' + assert encoded_note == expected_note From 6cd11be5a1fcfd0d2ba7a8c17b6c477a14e61039 Mon Sep 17 00:00:00 2001 From: Al Date: Wed, 6 Nov 2024 15:26:59 +0100 Subject: [PATCH 03/31] feat: AlgorandClientTransaction(Creator|Sender) and AssetManager abstractions (#123) * chore: wip * feat: initial implementation of TransactionSender, TransactionCreator and AssetManager --- src/algokit_utils/applications/app_manager.py | 21 + src/algokit_utils/assets/asset_manager.py | 267 +++++++++++- src/algokit_utils/clients/algorand_client.py | 86 ++-- .../transactions/transaction_composer.py | 164 +++++++- .../transactions/transaction_creator.py | 150 ++++++- .../transactions/transaction_sender.py | 349 ++++++++++++++-- tests/assets/__init__.py | 0 tests/assets/test_asset_manager.py | 207 ++++++++++ tests/test_transaction_composer.py | 10 +- .../transactions/test_transaction_composer.py | 12 +- .../transactions/test_transaction_creator.py | 267 ++++++++++++ tests/transactions/test_transaction_sender.py | 386 ++++++++++++++++++ 12 files changed, 1801 insertions(+), 118 deletions(-) create mode 100644 tests/assets/__init__.py create mode 100644 tests/assets/test_asset_manager.py create mode 100644 tests/transactions/test_transaction_creator.py create mode 100644 tests/transactions/test_transaction_sender.py diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index 91b0a407..307d5e0f 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -268,6 +268,27 @@ def get_box_values_from_abi_type( ) -> list[ABIValue]: return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] + @staticmethod + def get_abi_return( + confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None + ) -> ABIValue | None: + """Get the ABI return value from a transaction confirmation.""" + if not method: + return None + + # Use the SDK's built-in ABI result parsing + atc = algosdk.atomic_transaction_composer.AtomicTransactionComposer() + abi_result = atc.parse_result( + method, # Map of transaction index to ABI method + "dummy_txn", # List of transaction info + confirmation, # type: ignore[arg-type] + ) + + if not abi_result: + return None + + return abi_result.return_value # type: ignore[no-any-return] + @staticmethod def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: state_values: dict[str, AppState] = {} diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 4bef4802..ee642dac 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -1,2 +1,267 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +import algosdk +from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.v2client import algod + +from algokit_utils.models.account import Account +from algokit_utils.transactions.transaction_composer import ( + AssetOptInParams, + AssetOptOutParams, + TransactionComposer, +) + + +@dataclass(frozen=True) +class AccountAssetInformation: + """Information about an account's holding of a particular asset.""" + + asset_id: int + """The ID of the asset.""" + balance: int + """The amount of the asset held by the account.""" + frozen: bool + """Whether the asset is frozen for this account.""" + round: int + """The round this information was retrieved at.""" + + +@dataclass(frozen=True) +class AssetInformation: + """Information about an asset.""" + + asset_id: int + """The ID of the asset.""" + creator: str + """The address of the account that created the asset.""" + total: int + """The total amount of the smallest divisible units that were created of the asset.""" + decimals: int + """The amount of decimal places the asset was created with.""" + default_frozen: bool | None = None + """Whether the asset was frozen by default for all accounts.""" + manager: str | None = None + """The address of the optional account that can manage the configuration of the asset and destroy it.""" + reserve: str | None = None + """The address of the optional account that holds the reserve (uncirculated supply) units of the asset.""" + freeze: str | None = None + """The address of the optional account that can be used to freeze or unfreeze holdings of this asset.""" + clawback: str | None = None + """The address of the optional account that can clawback holdings of this asset from any account.""" + unit_name: str | None = None + """The optional name of the unit of this asset (e.g. ticker name).""" + unit_name_b64: bytes | None = None + """The optional name of the unit of this asset as bytes.""" + asset_name: str | None = None + """The optional name of the asset.""" + asset_name_b64: bytes | None = None + """The optional name of the asset as bytes.""" + url: str | None = None + """Optional URL where more information about the asset can be retrieved.""" + url_b64: bytes | None = None + """Optional URL where more information about the asset can be retrieved as bytes.""" + metadata_hash: bytes | None = None + """32-byte hash of some metadata that is relevant to the asset and/or asset holders.""" + + +@dataclass(frozen=True) +class BulkAssetOptInOutResult: + """Individual result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.""" + + asset_id: int + """The ID of the asset opted into / out of""" + transaction_id: str + """The transaction ID of the resulting opt in / out""" + + class AssetManager: - """A manager for Algorand assets""" + """A manager for Algorand assets.""" + + def __init__(self, algod_client: algod.AlgodClient, new_group: Callable[[], TransactionComposer]): + """Create a new asset manager. + + Args: + algod_client: An algod client + new_group: A function that creates a new `TransactionComposer` transaction group + """ + self._algod = algod_client + self._new_group = new_group + + def get_by_id(self, asset_id: int) -> AssetInformation: + """Returns the current asset information for the asset with the given ID. + + Args: + asset_id: The ID of the asset + + Returns: + The asset information + """ + asset = self._algod.asset_info(asset_id) + assert isinstance(asset, dict) + params = asset["params"] + + return AssetInformation( + asset_id=asset_id, + total=params["total"], + decimals=params["decimals"], + asset_name=params.get("name"), + asset_name_b64=params.get("name-b64"), + unit_name=params.get("unit-name"), + unit_name_b64=params.get("unit-name-b64"), + url=params.get("url"), + url_b64=params.get("url-b64"), + creator=params["creator"], + manager=params.get("manager"), + clawback=params.get("clawback"), + freeze=params.get("freeze"), + reserve=params.get("reserve"), + default_frozen=params.get("default-frozen"), + metadata_hash=params.get("metadata-hash"), + ) + + def get_account_information( + self, sender: str | Account | TransactionSigner, asset_id: int + ) -> AccountAssetInformation: + """Returns the given sender account's asset holding for a given asset. + + Args: + sender: The address of the sender/account to look up + asset_id: The ID of the asset to return a holding for + + Returns: + The account asset holding information + """ + address = self._get_address_from_sender(sender) + info = self._algod.account_asset_info(address, asset_id) + assert isinstance(info, dict) + + return AccountAssetInformation( + asset_id=asset_id, + balance=info["asset-holding"]["amount"], + frozen=info["asset-holding"]["is-frozen"], + round=info["round"], + ) + + def bulk_opt_in( + self, + account: str | Account | TransactionSigner, + asset_ids: list[int], + *, + suppress_log: bool = False, + **transaction_params: Any, + ) -> list[BulkAssetOptInOutResult]: + """Opt an account in to a list of Algorand Standard Assets. + + Args: + account: The account to opt-in + asset_ids: The list of asset IDs to opt-in to + suppress_log: Whether to suppress logging + **transaction_params: Any additional transaction parameters + + Returns: + An array of records matching asset ID to transaction ID of the opt in + """ + results: list[BulkAssetOptInOutResult] = [] + sender = self._get_address_from_sender(account) + + for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT): + composer = self._new_group() + + for asset_id in asset_group: + params = AssetOptInParams( + sender=sender, + asset_id=asset_id, + **transaction_params, + ) + composer.add_asset_opt_in(params) + + result = composer.send(suppress_log=suppress_log) + + for i, asset_id in enumerate(asset_group): + results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) + + return results + + def bulk_opt_out( # noqa: C901 + self, + account: str | Account | TransactionSigner, + asset_ids: list[int], + *, + ensure_zero_balance: bool = True, + suppress_log: bool = False, + **transaction_params: Any, + ) -> list[BulkAssetOptInOutResult]: + """Opt an account out of a list of Algorand Standard Assets. + + Args: + account: The account to opt-out + asset_ids: The list of asset IDs to opt-out of + ensure_zero_balance: Whether to check if the account has a zero balance first + suppress_log: Whether to suppress logging + **transaction_params: Any additional transaction parameters + + Returns: + An array of records matching asset ID to transaction ID of the opt out + """ + results: list[BulkAssetOptInOutResult] = [] + sender = self._get_address_from_sender(account) + + for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT): + composer = self._new_group() + + not_opted_in_asset_ids: list[int] = [] + non_zero_balance_asset_ids: list[int] = [] + + if ensure_zero_balance: + for asset_id in asset_group: + try: + account_asset_info = self.get_account_information(sender, asset_id) + if account_asset_info.balance != 0: + non_zero_balance_asset_ids.append(asset_id) + except Exception: + not_opted_in_asset_ids.append(asset_id) + + if not_opted_in_asset_ids or non_zero_balance_asset_ids: + error_message = f"Account {sender}" + if not_opted_in_asset_ids: + error_message += f" is not opted-in to Asset(s) {', '.join(map(str, not_opted_in_asset_ids))}" + if non_zero_balance_asset_ids: + error_message += ( + f" has non-zero balance for Asset(s) {', '.join(map(str, non_zero_balance_asset_ids))}" + ) + error_message += "; can't opt-out." + raise ValueError(error_message) + + for asset_id in asset_group: + asset_info = self.get_by_id(asset_id) + params = AssetOptOutParams( + sender=sender, + asset_id=asset_id, + creator=asset_info.creator, + **transaction_params, + ) + composer.add_asset_opt_out(params) + + result = composer.send(suppress_log=suppress_log) + + for i, asset_id in enumerate(asset_group): + results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) + + return results + + @staticmethod + def _get_address_from_sender(sender: str | Account | TransactionSigner) -> str: + if isinstance(sender, str): + return sender + if isinstance(sender, Account): + return sender.address + if isinstance(sender, AccountTransactionSigner): + return str(algosdk.account.address_from_private_key(sender.private_key)) + raise ValueError(f"Unsupported sender type: {type(sender)}") + + +def _chunk_array(array: list, size: int) -> list[list]: + """Split an array into chunks of the given size.""" + return [array[i : i + size] for i in range(0, len(array), size)] diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index f4851daf..f679c95e 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -1,11 +1,9 @@ import copy import time -from collections.abc import Callable -from dataclasses import dataclass from typing import Any from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner -from algosdk.transaction import SuggestedParams, Transaction, wait_for_confirmation +from algosdk.transaction import SuggestedParams, wait_for_confirmation from typing_extensions import Self from algokit_utils.accounts.account_manager import AccountManager @@ -51,57 +49,23 @@ ] -@dataclass -class AlgorandClientSendMethods: - """ - Methods used to send a transaction to the network and wait for confirmation - """ - - payment: Callable[[PaymentParams], dict[str, Any]] - asset_create: Callable[[AssetCreateParams], dict[str, Any]] - asset_config: Callable[[AssetConfigParams], dict[str, Any]] - asset_freeze: Callable[[AssetFreezeParams], dict[str, Any]] - asset_destroy: Callable[[AssetDestroyParams], dict[str, Any]] - asset_transfer: Callable[[AssetTransferParams], dict[str, Any]] - app_call: Callable[[AppCallParams], dict[str, Any]] - online_key_reg: Callable[[OnlineKeyRegistrationParams], dict[str, Any]] - method_call: Callable[[AppMethodCallParams], dict[str, Any]] - asset_opt_in: Callable[[AssetOptInParams], dict[str, Any]] - - -@dataclass -class AlgorandClientTransactionMethods: - """ - Methods used to form a transaction without signing or sending to the network - """ - - payment: Callable[[PaymentParams], Transaction] - asset_create: Callable[[AssetCreateParams], Transaction] - asset_config: Callable[[AssetConfigParams], Transaction] - asset_freeze: Callable[[AssetFreezeParams], Transaction] - asset_destroy: Callable[[AssetDestroyParams], Transaction] - asset_transfer: Callable[[AssetTransferParams], Transaction] - app_call: Callable[[AppCallParams], Transaction] - online_key_reg: Callable[[OnlineKeyRegistrationParams], Transaction] - method_call: Callable[[AppMethodCallParams], list[Transaction]] - asset_opt_in: Callable[[AssetOptInParams], Transaction] - - class AlgorandClient: """A client that brokers easy access to Algorand functionality.""" def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): self._client_manager: ClientManager = ClientManager(config) self._account_manager: AccountManager = AccountManager(self._client_manager) - self._asset_manager: AssetManager = AssetManager() # TODO: implement - self._app_manager: AppManager = AppManager(self._client_manager.algod) # TODO: implement + self._asset_manager: AssetManager = AssetManager(self._client_manager.algod, lambda: self.new_group()) + self._app_manager: AppManager = AppManager(self._client_manager.algod) self._transaction_sender = AlgorandClientTransactionSender( new_group=lambda: self.new_group(), asset_manager=self._asset_manager, app_manager=self._app_manager, algod_client=self._client_manager.algod, ) - self._transaction_creator = AlgorandClientTransactionCreator() # TODO: implement + self._transaction_creator = AlgorandClientTransactionCreator( + new_group=lambda: self.new_group(), + ) self._cached_suggested_params: SuggestedParams | None = None self._cached_suggested_params_expiry: float | None = None @@ -109,12 +73,6 @@ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): self._default_validity_window: int = 10 - def _unwrap_single_send_result(self, results: AtomicTransactionResponse) -> dict[str, Any]: - return { - "confirmation": wait_for_confirmation(self._client_manager.algod, results.tx_ids[0]), - "tx_id": results.tx_ids[0], - } - def set_default_validity_window(self, validity_window: int) -> Self: """ Sets the default validity window for transactions. @@ -180,6 +138,15 @@ def get_suggested_params(self) -> SuggestedParams: return copy.deepcopy(self._cached_suggested_params) + def new_group(self) -> TransactionComposer: + """Start a new `TransactionComposer` transaction group""" + return TransactionComposer( + algod=self.client.algod, + get_signer=lambda addr: self.account.get_signer(addr), + get_suggested_params=self.get_suggested_params, + default_validity_window=self._default_validity_window, + ) + @property def client(self) -> ClientManager: """Get clients, including algosdk clients and app clients.""" @@ -190,14 +157,15 @@ def account(self) -> AccountManager: """Get or create accounts that can sign transactions.""" return self._account_manager - def new_group(self) -> TransactionComposer: - """Start a new `TransactionComposer` transaction group""" - return TransactionComposer( - algod=self.client.algod, - get_signer=lambda addr: self.account.get_signer(addr), - get_suggested_params=self.get_suggested_params, - default_validity_window=self._default_validity_window, - ) + @property + def asset(self) -> AssetManager: + """Get or create assets.""" + return self._asset_manager + + @property + def app_deployer(self) -> AppManager: + """Get or create applications.""" + return self._app_manager @property def send(self) -> AlgorandClientTransactionSender: @@ -209,6 +177,12 @@ def create_transaction(self) -> AlgorandClientTransactionCreator: """Methods for building transactions""" return self._transaction_creator + def _unwrap_single_send_result(self, results: AtomicTransactionResponse) -> dict[str, Any]: + return { + "confirmation": wait_for_confirmation(self._client_manager.algod, results.tx_ids[0]), + "tx_id": results.tx_ids[0], + } + @staticmethod def default_local_net() -> "AlgorandClient": """ diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 2d36c06d..77dea2e9 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,8 +1,9 @@ from __future__ import annotations +import logging import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Union import algosdk import algosdk.atomic_transaction_composer @@ -11,7 +12,9 @@ TransactionSigner, TransactionWithSigner, ) -from algosdk.transaction import OnComplete +from algosdk.error import AlgodHTTPError +from algosdk.transaction import OnComplete, Transaction +from algosdk.v2client.algod import AlgodClient from deprecated import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response @@ -29,6 +32,8 @@ from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.models import Arc2TransactionNote +logger = logging.getLogger(__name__) + @dataclass(frozen=True) class SenderParam: @@ -565,6 +570,136 @@ class TransactionComposerBuildResult: method_calls: dict[int, Method] +@dataclass +class SendAtomicTransactionComposerResults: + """Results from sending an AtomicTransactionComposer transaction group""" + + group_id: str | None + """The group ID if this was a transaction group""" + confirmations: list[algosdk.v2client.algod.AlgodResponseType] + """The confirmation info for each transaction""" + tx_ids: list[str] + """The transaction IDs that were sent""" + transactions: list[Transaction] + """The transactions that were sent""" + returns: list[Any] + """The ABI return values from any ABI method calls""" + + +def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 + atc: AtomicTransactionComposer, + algod: AlgodClient, + *, + max_rounds_to_wait: int | None = 5, + skip_waiting: bool = False, + suppress_log: bool = False, + populate_resources: bool | None = None, # TODO: implement/clarify # noqa: ARG001 +) -> SendAtomicTransactionComposerResults: + """Send an AtomicTransactionComposer transaction group + + Args: + atc: The AtomicTransactionComposer to send + algod: The Algod client to use + max_rounds_to_wait: Maximum number of rounds to wait for confirmation + skip_waiting: If True, don't wait for transaction confirmation + suppress_log: If True, suppress logging + populate_resources: If True, populate app call resources + + Returns: + The results of sending the transaction group + + Raises: + Exception: If there is an error sending the transactions + """ + + try: + # Build transactions + transactions_with_signer = atc.build_group() + transactions_to_send = [t.txn for t in transactions_with_signer] + + # Get group ID if multiple transactions + group_id = None + if len(transactions_to_send) > 1: + group_id = transactions_to_send[0].group.hex() if transactions_to_send[0].group else None + + if not suppress_log: + logger.info(f"Sending group of {len(transactions_to_send)} transactions ({group_id})") + logger.debug(f"Transaction IDs ({group_id}): {[t.get_txid() for t in transactions_to_send]}") + + # Simulate if debug enabled + if config.debug and config.trace_all and config.project_root: + simulate_and_persist_response( + atc, + config.project_root, + algod, + config.trace_buffer_size_mb, + ) + + # Execute transactions + result = atc.execute(algod, wait_rounds=max_rounds_to_wait or 5) + + # Log results + if not suppress_log: + if len(transactions_to_send) > 1: + logger.info(f"Group transaction ({group_id}) sent with {len(transactions_to_send)} transactions") + else: + logger.info(f"Sent transaction ID {transactions_to_send[0].get_txid()}") + + # Get confirmations if not skipping + confirmations = None + if not skip_waiting: + confirmations = [algod.pending_transaction_info(t.get_txid()) for t in transactions_to_send] + + # Return results + return SendAtomicTransactionComposerResults( + group_id=group_id, + confirmations=confirmations or [], + tx_ids=[t.get_txid() for t in transactions_to_send], + transactions=transactions_to_send, + returns=[r.return_value for r in result.abi_results], + ) + + except AlgodHTTPError as e: + # Handle error with debug info if enabled + if config.debug: + logger.error( + "Received error executing Atomic Transaction Composer and debug flag enabled; " + "attempting simulation to get more information" + ) + + simulate = None + if config.project_root and not config.trace_all: + # Only simulate if trace_all is disabled and project_root is set + simulate = simulate_and_persist_response(atc, config.project_root, algod, config.trace_buffer_size_mb) + else: + simulate = simulate_response(atc, algod) + + traces = [] + if simulate and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget = txn_group.get("app-budget-added") + app_budget_consumed = txn_group.get("app-budget-consumed") + failure_message = txn_group.get("failure-message") + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + + traces.append( + { + "trace": exec_trace, + "app_budget": app_budget, + "app_budget_consumed": app_budget_consumed, + "failure_message": failure_message, + } + ) + + error = Exception(f"Transaction failed: {e}") + error.traces = traces # type: ignore[attr-defined] + raise error from e + + logger.error("Received error executing Atomic Transaction Composer, for more information enable the debug flag") + raise Exception(f"Transaction failed: {e}") from e + + class TransactionComposer: """ A class for composing and managing Algorand transactions using the Algosdk library. @@ -754,15 +889,18 @@ def execute( self, *, max_rounds_to_wait: int | None = None, - ) -> algosdk.atomic_transaction_composer.AtomicTransactionResponse: + ) -> SendAtomicTransactionComposerResults: return self.send( max_rounds_to_wait=max_rounds_to_wait, ) def send( self, + *, max_rounds_to_wait: int | None = None, - ) -> algosdk.atomic_transaction_composer.AtomicTransactionResponse: + suppress_log: bool = False, + populate_app_call_resources: bool = False, + ) -> SendAtomicTransactionComposerResults: group = self.build().transactions wait_rounds = max_rounds_to_wait @@ -772,7 +910,13 @@ def send( wait_rounds = last_round - first_round + 1 try: - return self.atc.execute(client=self.algod, wait_rounds=wait_rounds) # TODO: reimplement ATC + return send_atomic_transaction_composer( + self.atc, + self.algod, + max_rounds_to_wait=wait_rounds, + suppress_log=suppress_log, + populate_resources=populate_app_call_resources, + ) except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e @@ -824,7 +968,7 @@ def _common_txn_build_step( if params.lease: txn.lease = params.lease if params.rekey_to: - txn.rekey_to = algosdk.encoding.decode_address(params.rekey_to) + txn.rekey_to = params.rekey_to if params.note: txn.note = params.note @@ -1142,6 +1286,14 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params ) return [TransactionWithSigner(txn=asset_transfer, signer=signer)] + case AssetOptOutParams(): + txn_dict = txn.__dict__ + creator = txn_dict.pop("creator") + asset_transfer = self._build_asset_transfer( + AssetTransferParams(**txn_dict, receiver=txn.sender, amount=0, close_asset_to=creator), + suggested_params, + ) + return [TransactionWithSigner(txn=asset_transfer, signer=signer)] case OnlineKeyRegistrationParams(): key_reg = self._build_key_reg(txn, suggested_params) return [TransactionWithSigner(txn=key_reg, signer=signer)] diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py index e4ae0e03..a5bc8926 100644 --- a/src/algokit_utils/transactions/transaction_creator.py +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -1,2 +1,150 @@ +from collections.abc import Callable +from typing import TypeVar + +from algosdk.transaction import Transaction + +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppDeleteParams, + AppUpdateMethodCall, + AppUpdateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + BuiltTransactions, + OnlineKeyRegistrationParams, + PaymentParams, + TransactionComposer, +) + +TxnParam = TypeVar("TxnParam") +TxnResult = TypeVar("TxnResult") + + class AlgorandClientTransactionCreator: - """A creator for Algorand transactions""" + """A creator for Algorand transactions.""" + + def __init__(self, new_group: Callable[[], TransactionComposer]) -> None: + """ + Creates a new `AlgorandClientTransactionCreator`. + + Args: + new_group: A lambda that starts a new `TransactionComposer` transaction group + """ + self._new_group = new_group + + def _transaction( + self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] + ) -> Callable[[TxnParam], Transaction]: + """Generic method to create a single transaction.""" + + def create_transaction(params: TxnParam) -> Transaction: + composer = self._new_group() + result = c(composer)(params).build_transactions() + return result.transactions[-1] + + return create_transaction + + def _transactions( + self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] + ) -> Callable[[TxnParam], BuiltTransactions]: + """Generic method to create multiple transactions.""" + + def create_transactions(params: TxnParam) -> BuiltTransactions: + composer = self._new_group() + return c(composer)(params).build_transactions() + + return create_transactions + + @property + def payment(self) -> Callable[[PaymentParams], Transaction]: + """Create a payment transaction to transfer Algo between accounts.""" + return self._transaction(lambda c: c.add_payment) + + @property + def asset_create(self) -> Callable[[AssetCreateParams], Transaction]: + """Create a create Algorand Standard Asset transaction.""" + return self._transaction(lambda c: c.add_asset_create) + + @property + def asset_config(self) -> Callable[[AssetConfigParams], Transaction]: + """Create an asset config transaction to reconfigure an existing Algorand Standard Asset.""" + return self._transaction(lambda c: c.add_asset_config) + + @property + def asset_freeze(self) -> Callable[[AssetFreezeParams], Transaction]: + """Create an Algorand Standard Asset freeze transaction.""" + return self._transaction(lambda c: c.add_asset_freeze) + + @property + def asset_destroy(self) -> Callable[[AssetDestroyParams], Transaction]: + """Create an Algorand Standard Asset destroy transaction.""" + return self._transaction(lambda c: c.add_asset_destroy) + + @property + def asset_transfer(self) -> Callable[[AssetTransferParams], Transaction]: + """Create an Algorand Standard Asset transfer transaction.""" + return self._transaction(lambda c: c.add_asset_transfer) + + @property + def asset_opt_in(self) -> Callable[[AssetOptInParams], Transaction]: + """Create an Algorand Standard Asset opt-in transaction.""" + return self._transaction(lambda c: c.add_asset_opt_in) + + @property + def asset_opt_out(self) -> Callable[[AssetOptOutParams], Transaction]: + """Create an asset opt-out transaction.""" + return self._transaction(lambda c: c.add_asset_opt_out) + + @property + def app_create(self) -> Callable[[AppCreateParams], Transaction]: + """Create an application create transaction.""" + return self._transaction(lambda c: c.add_app_create) + + @property + def app_update(self) -> Callable[[AppUpdateParams], Transaction]: + """Create an application update transaction.""" + return self._transaction(lambda c: c.add_app_update) + + @property + def app_delete(self) -> Callable[[AppDeleteParams], Transaction]: + """Create an application delete transaction.""" + return self._transaction(lambda c: c.add_app_delete) + + @property + def app_call(self) -> Callable[[AppCallParams], Transaction]: + """Create an application call transaction.""" + return self._transaction(lambda c: c.add_app_call) + + @property + def app_create_method_call(self) -> Callable[[AppCreateMethodCall], BuiltTransactions]: + """Create an application create call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_create_method_call) + + @property + def app_update_method_call(self) -> Callable[[AppUpdateMethodCall], BuiltTransactions]: + """Create an application update call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_update_method_call) + + @property + def app_delete_method_call(self) -> Callable[[AppDeleteMethodCall], BuiltTransactions]: + """Create an application delete call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_delete_method_call) + + @property + def app_call_method_call(self) -> Callable[[AppCallMethodCall], BuiltTransactions]: + """Create an application call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_call_method_call) + + @property + def online_key_registration(self) -> Callable[[OnlineKeyRegistrationParams], Transaction]: + """Create an online key registration transaction.""" + return self._transaction(lambda c: c.add_online_key_registration) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 41c5aa4b..3050dcb7 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,30 +1,86 @@ -import logging from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar +from logging import getLogger +from typing import Any, TypedDict, TypeVar -from algosdk.transaction import wait_for_confirmation -from algosdk.v2client.algod import AlgodClient +import algosdk +import algosdk.atomic_transaction_composer +from algosdk.atomic_transaction_composer import AtomicTransactionResponse +from algosdk.transaction import Transaction from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.models.abi import ABIValue from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppDeleteParams, + AppUpdateMethodCall, + AppUpdateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OnlineKeyRegistrationParams, PaymentParams, TransactionComposer, + TxnParams, ) -logger = logging.getLogger(__name__) -TxnParam = TypeVar("TxnParam") -TxnResult = TypeVar("TxnResult") +logger = getLogger(__name__) @dataclass -class SendTransactionResult(Generic[TxnResult]): - """Result of sending a transaction""" +class SendSingleTransactionResult: + tx_id: str # Single transaction ID (last from txIds array) + transaction: Transaction # Last transaction + confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation - confirmation: dict[str, Any] - tx_id: str - return_value: TxnResult | None = None + # Fields from SendAtomicTransactionComposerResults + group_id: str + tx_ids: list[str] # Full array of transaction IDs + transactions: list[Transaction] + confirmations: list[algosdk.v2client.algod.AlgodResponseType] + returns: list[algosdk.atomic_transaction_composer.ABIResult] | None = None + + # Fields from AssetCreateParams + asset_id: int | None = None + + +@dataclass +class SendAppTransactionResult(SendSingleTransactionResult): + return_value: ABIValue | None = None + + +@dataclass +class SendAppUpdateTransactionResult(SendAppTransactionResult): + compiled_approval: Any | None = None + compiled_clear: Any | None = None + + +@dataclass +class _RequiredSendAppTransactionResult: + app_id: int + app_address: str + + +@dataclass +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult, _RequiredSendAppTransactionResult): + pass + + +class LogConfig(TypedDict, total=False): + pre_log: Callable[[TxnParams, Transaction], str] + post_log: Callable[[TxnParams, AtomicTransactionResponse], str] + + +T = TypeVar("T", bound=TxnParams) class AlgorandClientTransactionSender: @@ -35,54 +91,265 @@ def __init__( new_group: Callable[[], TransactionComposer], asset_manager: AssetManager, app_manager: AppManager, - algod_client: AlgodClient, + algod_client: algosdk.v2client.algod.AlgodClient, ) -> None: self._new_group = new_group self._asset_manager = asset_manager self._app_manager = app_manager - self._algod_client = algod_client + self._algod = algod_client def new_group(self) -> TransactionComposer: - """Create a new transaction group""" return self._new_group() def _send( self, - c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]], - log: dict[str, Callable[[TxnParam, Any], str]] | None = None, - ) -> Callable[[TxnParam], SendTransactionResult[Any]]: - """Generic method to send transactions with logging.""" - - def send_transaction(params: TxnParam) -> SendTransactionResult[Any]: - composer = self._new_group() + c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], + pre_log: Callable[[T, Transaction], str] | None = None, + post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[T], SendSingleTransactionResult]: + def send_transaction(params: T) -> SendSingleTransactionResult: + composer = self.new_group() c(composer)(params) - if log and log.get("pre_log"): + if pre_log: transaction = composer.build().transactions[-1].txn - logger.debug(log["pre_log"](params, transaction)) - - result = composer.send() + logger.debug(pre_log(params, transaction)) - if log and log.get("post_log"): - logger.debug(log["post_log"](params, result)) + raw_result = composer.send() - confirmation = wait_for_confirmation(self._algod_client, result.tx_ids[0]) - return SendTransactionResult( - confirmation=confirmation, - tx_id=result.tx_ids[0], + result = SendSingleTransactionResult( + **raw_result.__dict__, + confirmation=raw_result.confirmations[-1], + transaction=raw_result.transactions[-1], + tx_id=raw_result.tx_ids[-1], ) + if post_log: + logger.debug(post_log(params, result)) + + return result + return send_transaction - @property - def payment(self) -> Callable[[PaymentParams], SendTransactionResult[None]]: - """Send a payment transaction""" + def _send_app_call( + self, + c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], + pre_log: Callable[[T, Transaction], str] | None = None, + post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[T], SendAppTransactionResult]: + def send_app_call(params: T) -> SendAppTransactionResult: + result = self._send(c, pre_log, post_log)(params) + return SendAppTransactionResult( + **result.__dict__, + return_value=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), + ) + + return send_app_call + + def _send_app_update_call( + self, + c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], + pre_log: Callable[[T, Transaction], str] | None = None, + post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[T], SendAppUpdateTransactionResult]: + def send_app_update_call(params: T) -> SendAppUpdateTransactionResult: + result = self._send_app_call(c, pre_log, post_log)(params) + + if not isinstance(params, AppCreateParams | AppUpdateParams | AppCreateMethodCall | AppUpdateMethodCall): + raise TypeError("Invalid parameter type") + + compiled_approval = ( + self._app_manager.get_compilation_result(params.approval_program) + if isinstance(params.approval_program, str) + else None + ) + compiled_clear = ( + self._app_manager.get_compilation_result(params.clear_state_program) + if isinstance(params.clear_state_program, str) + else None + ) + + return SendAppUpdateTransactionResult( + **result.__dict__, + compiled_approval=compiled_approval, + compiled_clear=compiled_clear, + ) + + return send_app_update_call + + def _send_app_create_call( + self, + c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], + pre_log: Callable[[T, Transaction], str] | None = None, + post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[T], SendAppCreateTransactionResult]: + def send_app_create_call(params: T) -> SendAppCreateTransactionResult: + result = self._send_app_update_call(c, pre_log, post_log)(params) + app_id = int(result.confirmation["application-index"]) # type: ignore[call-overload] + + return SendAppCreateTransactionResult( + **result.__dict__, + app_id=app_id, + app_address=algosdk.logic.get_application_address(app_id), + ) + + return send_app_create_call + + def _get_method_call_for_log(self, method: algosdk.abi.Method, args: list[Any]) -> str: + """Helper function to format method call logs similar to TypeScript version""" + args_str = str([str(a) if not isinstance(a, bytes | bytearray) else a.hex() for a in args]) + return f"{method.name}({args_str})" + + def payment(self, params: PaymentParams) -> SendSingleTransactionResult: + """Send a payment transaction to transfer Algo between accounts.""" return self._send( lambda c: c.add_payment, - { - "pre_log": lambda params, txn: ( - f"Sending {params.amount.micro_algos} µALGO from {params.sender} " - f"to {params.receiver} via transaction {txn.get_txid()}" - ) - }, + pre_log=lambda params, transaction: ( + f"Sending {params.amount} from {params.sender} to {params.receiver} " + f"via transaction {transaction.get_txid()}" + ), + )(params) + + def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult: + """Create a new Algorand Standard Asset.""" + result = self._send( + lambda c: c.add_asset_create, + post_log=lambda params, result: ( + f"Created asset{f' {params.asset_name}' if hasattr(params, 'asset_name') else ''}" + f"{f' ({params.unit_name})' if hasattr(params, 'unit_name') else ''} with " + f"{params.total} units and {getattr(params, 'decimals', 0)} decimals created by " + f"{params.sender} with ID {result.confirmation['asset-index']} via transaction " # type: ignore[call-overload] + f"{result.tx_ids[-1]}" + ), + )(params) + + result = SendSingleTransactionResult( + **result.__dict__, ) + result.asset_id = int(result.confirmation["asset-index"]) # type: ignore[call-overload] + return result + + def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: + """Configure an existing Algorand Standard Asset.""" + return self._send( + lambda c: c.add_asset_config, + pre_log=lambda params, transaction: ( + f"Configuring asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params) + + def asset_freeze(self, params: AssetFreezeParams) -> SendSingleTransactionResult: + """Freeze or unfreeze an Algorand Standard Asset for an account.""" + return self._send( + lambda c: c.add_asset_freeze, + pre_log=lambda params, transaction: ( + f"Freezing asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params) + + def asset_destroy(self, params: AssetDestroyParams) -> SendSingleTransactionResult: + """Destroys an Algorand Standard Asset.""" + return self._send( + lambda c: c.add_asset_destroy, + pre_log=lambda params, transaction: ( + f"Destroying asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params) + + def asset_transfer(self, params: AssetTransferParams) -> SendSingleTransactionResult: + """Transfer an Algorand Standard Asset.""" + return self._send( + lambda c: c.add_asset_transfer, + pre_log=lambda params, transaction: ( + f"Transferring {params.amount} units of asset with ID {params.asset_id} from " + f"{params.sender} to {params.receiver} via transaction {transaction.get_txid()}" + ), + )(params) + + def asset_opt_in(self, params: AssetOptInParams) -> SendSingleTransactionResult: + """Opt an account into an Algorand Standard Asset.""" + return self._send( + lambda c: c.add_asset_opt_in, + pre_log=lambda params, transaction: ( + f"Opting in {params.sender} to asset with ID {params.asset_id} via transaction " + f"{transaction.get_txid()}" + ), + )(params) + + def asset_opt_out( + self, + *, + params: AssetOptOutParams, + ensure_zero_balance: bool = True, + ) -> SendSingleTransactionResult: + """Opt an account out of an Algorand Standard Asset.""" + if ensure_zero_balance: + try: + account_asset_info = self._asset_manager.get_account_information(params.sender, params.asset_id) + balance = account_asset_info.balance + if balance != 0: + raise ValueError( + f"Account {params.sender} does not have a zero balance for Asset " + f"{params.asset_id}; can't opt-out." + ) + except Exception as e: + raise ValueError( + f"Account {params.sender} is not opted-in to Asset {params.asset_id}; " "can't opt-out." + ) from e + + if not hasattr(params, "creator"): + asset_info = self._asset_manager.get_by_id(params.asset_id) + params = AssetOptOutParams( + **params.__dict__, + creator=asset_info.creator, + ) + + creator = params.__dict__.get("creator") + return self._send( + lambda c: c.add_asset_opt_out, + pre_log=lambda params, transaction: ( + f"Opting {params.sender} out of asset with ID {params.asset_id} to creator " + f"{creator} via transaction {transaction.get_txid()}" + ), + )(params) + + def app_create(self, params: AppCreateParams) -> SendAppCreateTransactionResult: + """Create a new application.""" + return self._send_app_create_call(lambda c: c.add_app_create)(params) + + def app_update(self, params: AppUpdateParams) -> SendAppUpdateTransactionResult: + """Update an application.""" + return self._send_app_update_call(lambda c: c.add_app_update)(params) + + def app_delete(self, params: AppDeleteParams) -> SendAppTransactionResult: + """Delete an application.""" + return self._send_app_call(lambda c: c.add_app_delete)(params) + + def app_call(self, params: AppCallParams) -> SendAppTransactionResult: + """Call an application.""" + return self._send_app_call(lambda c: c.add_app_call)(params) + + def app_create_method_call(self, params: AppCreateMethodCall) -> SendAppCreateTransactionResult: + """Call an application's create method.""" + return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params) + + def app_update_method_call(self, params: AppUpdateMethodCall) -> SendAppUpdateTransactionResult: + """Call an application's update method.""" + return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params) + + def app_delete_method_call(self, params: AppDeleteMethodCall) -> SendAppTransactionResult: + """Call an application's delete method.""" + return self._send_app_call(lambda c: c.add_app_delete_method_call)(params) + + def app_call_method_call(self, params: AppCallMethodCall) -> SendAppTransactionResult: + """Call an application's call method.""" + return self._send_app_call(lambda c: c.add_app_call_method_call)(params) + + def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSingleTransactionResult: + """Register an online key.""" + return self._send( + lambda c: c.add_online_key_registration, + pre_log=lambda params, transaction: ( + f"Registering online key for {params.sender} via transaction {transaction.get_txid()}" + ), + )(params) diff --git a/tests/assets/__init__.py b/tests/assets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py new file mode 100644 index 00000000..61e5c255 --- /dev/null +++ b/tests/assets/test_asset_manager.py @@ -0,0 +1,207 @@ +import algosdk +import pytest +from algokit_utils import Account, get_account +from algokit_utils.assets.asset_manager import ( + AccountAssetInformation, + AssetInformation, + BulkAssetOptInOutResult, +) +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AssetCreateParams, + PaymentParams, +) +from algosdk.atomic_transaction_composer import AccountTransactionSigner + +from tests.conftest import get_unique_name + + +@pytest.fixture() +def sender(funded_account: Account) -> Account: + return funded_account + + +@pytest.fixture() +def receiver(algod_client: algosdk.v2client.algod.AlgodClient) -> Account: + return get_account(algod_client, get_unique_name()) + + +@pytest.fixture() +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +def test_get_by_id(algorand: AlgorandClient, sender: Account) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get its info + asset_info = algorand.asset.get_by_id(asset_id) + + assert isinstance(asset_info, AssetInformation) + assert asset_info.asset_id == asset_id + assert asset_info.total == total + assert asset_info.decimals == 0 + assert asset_info.default_frozen is False + assert asset_info.unit_name == "TEST" + assert asset_info.asset_name == "Test Asset" + assert asset_info.url == "https://example.com" + assert asset_info.creator == sender.address + + +def test_get_account_information_with_address(algorand: AlgorandClient, sender: Account) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info + account_info = algorand.asset.get_account_information(sender.address, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_get_account_information_with_account(algorand: AlgorandClient, sender: Account) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info + account_info = algorand.asset.get_account_information(sender, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_get_account_information_with_transaction_signer(algorand: AlgorandClient, sender: Account) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info using transaction signer + signer = AccountTransactionSigner(sender.private_key) + account_info = algorand.asset.get_account_information(signer, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: Account, receiver: Account) -> None: + # First create some assets + asset_ids = [] + for i in range(3): + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name=f"TST{i}", + asset_name=f"Test Asset {i}", + url="https://example.com", + signer=sender.signer, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + asset_ids.append(asset_id) + + # Fund receiver + algorand.send.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + # Then bulk opt-in + results = algorand.asset.bulk_opt_in(receiver.address, asset_ids, signer=receiver.signer) + + assert len(results) == len(asset_ids) + for result in results: + assert isinstance(result, BulkAssetOptInOutResult) + assert result.asset_id in asset_ids + assert result.transaction_id + + +def test_bulk_opt_out_not_opted_in_fails(algorand: AlgorandClient, sender: Account, receiver: Account) -> None: + # First create an asset + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Fund receiver but don't opt-in + algorand.send.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + # Then attempt to opt-out + with pytest.raises(ValueError, match="is not opted-in"): + algorand.asset.bulk_opt_out(receiver.address, [asset_id]) diff --git a/tests/test_transaction_composer.py b/tests/test_transaction_composer.py index 1cbab861..5ea937ec 100644 --- a/tests/test_transaction_composer.py +++ b/tests/test_transaction_composer.py @@ -10,11 +10,9 @@ AssetConfigParams, AssetCreateParams, PaymentParams, + SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.atomic_transaction_composer import ( - AtomicTransactionResponse, -) from algosdk.transaction import ( ApplicationCreateTxn, AssetConfigTxn, @@ -86,7 +84,7 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> )["params"] assert len(response.tx_ids) == 1 - assert response.confirmed_round > 0 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] assert isinstance(built.transactions[0], AssetCreateTxn) txn = built.transactions[0] assert txn.sender == funded_account.address @@ -196,9 +194,9 @@ def test_send(algorand: AlgorandClient, funded_account: Account) -> None: ) ) response = composer.send() - assert isinstance(response, AtomicTransactionResponse) + assert isinstance(response, SendAtomicTransactionComposerResults) assert len(response.tx_ids) == 1 - assert response.confirmed_round > 0 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] def test_arc2_note() -> None: diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 0a75961a..619668e8 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -13,11 +13,9 @@ AssetConfigParams, AssetCreateParams, PaymentParams, + SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.atomic_transaction_composer import ( - AtomicTransactionResponse, -) from algosdk.transaction import ( ApplicationCallTxn, ApplicationCreateTxn, @@ -90,7 +88,7 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> )["params"] assert len(response.tx_ids) == 1 - assert response.confirmed_round > 0 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] assert isinstance(built.transactions[0], AssetCreateTxn) txn = built.transactions[0] assert txn.sender == funded_account.address @@ -207,7 +205,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco txn = built.transactions[0] assert txn.sender == funded_account.address response = composer.execute(max_rounds_to_wait=20) - assert response.abi_results[0].return_value == "Hello, world" + assert response.returns[-1] == "Hello, world" def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: @@ -240,9 +238,9 @@ def test_send(algorand: AlgorandClient, funded_account: Account) -> None: ) ) response = composer.send() - assert isinstance(response, AtomicTransactionResponse) + assert isinstance(response, SendAtomicTransactionComposerResults) assert len(response.tx_ids) == 1 - assert response.confirmed_round > 0 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] def test_arc2_note() -> None: diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py new file mode 100644 index 00000000..ec84d650 --- /dev/null +++ b/tests/transactions/test_transaction_creator.py @@ -0,0 +1,267 @@ +from pathlib import Path + +import algosdk +import pytest +from algokit_utils._legacy_v2.account import get_account +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OnlineKeyRegistrationParams, + PaymentParams, +) +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + AssetDestroyTxn, + AssetFreezeTxn, + AssetTransferTxn, + KeyregTxn, + PaymentTxn, +) + +from legacy_v2_tests.conftest import get_unique_name + + +@pytest.fixture() +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +@pytest.fixture() +def funded_secondary_account(algorand: AlgorandClient, funded_account: Account) -> Account: + secondary_name = get_unique_name() + account = get_account(algorand.client.algod, secondary_name) + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=account.address, amount=AlgoAmount.from_algos(1)) + ) + return account + + +def test_create_payment_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + assert isinstance(txn, PaymentTxn) + assert txn.sender == funded_account.address + assert txn.receiver == funded_account.address + assert txn.amt == AlgoAmount.from_algos(1).micro_algos + + +def test_create_asset_create_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + expected_total = 1000 + txn = algorand.create_transaction.asset_create( + AssetCreateParams( + sender=funded_account.address, + total=expected_total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + + assert isinstance(txn, AssetCreateTxn) + assert txn.sender == funded_account.address + assert txn.total == expected_total + assert txn.decimals == 0 + assert txn.default_frozen is False + assert txn.unit_name == "TEST" + assert txn.asset_name == "Test Asset" + assert txn.url == "https://example.com" + + +def test_create_asset_config_transaction( + algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account +) -> None: + txn = algorand.create_transaction.asset_config( + AssetConfigParams( + sender=funded_account.address, + asset_id=1, + manager=funded_secondary_account.address, + ) + ) + + assert isinstance(txn, AssetConfigTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.manager == funded_secondary_account.address + + +def test_create_asset_freeze_transaction( + algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account +) -> None: + txn = algorand.create_transaction.asset_freeze( + AssetFreezeParams( + sender=funded_account.address, + asset_id=1, + account=funded_secondary_account.address, + frozen=True, + ) + ) + + assert isinstance(txn, AssetFreezeTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.target == funded_secondary_account.address + assert txn.new_freeze_state is True + + +def test_create_asset_destroy_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + txn = algorand.create_transaction.asset_destroy( + AssetDestroyParams( + sender=funded_account.address, + asset_id=1, + ) + ) + + assert isinstance(txn, AssetDestroyTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + + +def test_create_asset_transfer_transaction( + algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account +) -> None: + expected_amount = 100 + txn = algorand.create_transaction.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + asset_id=1, + amount=expected_amount, + receiver=funded_secondary_account.address, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == expected_amount + assert txn.receiver == funded_secondary_account.address + + +def test_create_asset_opt_in_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + txn = algorand.create_transaction.asset_opt_in( + AssetOptInParams( + sender=funded_account.address, + asset_id=1, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == 0 + assert txn.receiver == funded_account.address + + +def test_create_asset_opt_out_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + txn = algorand.create_transaction.asset_opt_out( + AssetOptOutParams( + sender=funded_account.address, + asset_id=1, + creator=funded_account.address, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == 0 + assert txn.receiver == funded_account.address + assert txn.close_assets_to == funded_account.address + + +def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + txn = algorand.create_transaction.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + ) + + assert isinstance(txn, ApplicationCreateTxn) + assert txn.sender == funded_account.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + + +def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + + # First create the app + create_result = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + ) + app_id = algorand.client.algod.pending_transaction_info(create_result.tx_id)["application-index"] # type: ignore[call-overload] + + # Then test creating a method call transaction + result = algorand.create_transaction.app_call_method_call( + AppCallMethodCall( + sender=funded_account.address, + app_id=app_id, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["world"], + ) + ) + + assert len(result.transactions) == 1 + assert isinstance(result.transactions[0], ApplicationCallTxn) + assert result.transactions[0].sender == funded_account.address + assert result.transactions[0].index == app_id + + +def test_create_online_key_registration_transaction(algorand: AlgorandClient, funded_account: Account) -> None: + sp = algorand.get_suggested_params() + expected_dilution = 100 + expected_first = sp.first + expected_last = sp.first + int(10e6) + + txn = algorand.create_transaction.online_key_registration( + OnlineKeyRegistrationParams( + sender=funded_account.address, + vote_key="G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=", + selection_key="LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=", + state_proof_key=b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", + vote_first=expected_first, + vote_last=expected_last, + vote_key_dilution=expected_dilution, + ) + ) + + assert isinstance(txn, KeyregTxn) + assert txn.sender == funded_account.address + assert txn.selkey == "LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=" + assert txn.sprfkey == b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==" + assert txn.votefst == expected_first + assert txn.votelst == expected_last + assert txn.votekd == expected_dilution diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py new file mode 100644 index 00000000..b8514cdb --- /dev/null +++ b/tests/transactions/test_transaction_sender.py @@ -0,0 +1,386 @@ +from typing import TYPE_CHECKING, cast +from unittest.mock import MagicMock, patch + +import pytest +from algokit_utils import ( + Account, + get_account, +) +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OnlineKeyRegistrationParams, + PaymentParams, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender +from algosdk.transaction import ( + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + AssetDestroyTxn, + AssetFreezeTxn, + AssetTransferTxn, + PaymentTxn, +) + +from tests.conftest import get_unique_name + +if TYPE_CHECKING: + import algosdk + + +@pytest.fixture() +def sender(funded_account: Account) -> Account: + return funded_account + + +@pytest.fixture() +def receiver(algod_client: "algosdk.v2client.algod.AlgodClient") -> Account: + return get_account(algod_client, get_unique_name()) + + +@pytest.fixture() +def transaction_sender( + algod_client: "algosdk.v2client.algod.AlgodClient", sender: Account +) -> AlgorandClientTransactionSender: + def new_group() -> TransactionComposer: + return TransactionComposer( + algod=algod_client, + get_signer=lambda _: sender.signer, + ) + + return AlgorandClientTransactionSender( + new_group=new_group, + asset_manager=AssetManager(algod_client, new_group), + app_manager=AppManager(algod_client), + algod_client=algod_client, + ) + + +def test_payment(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: + amount = AlgoAmount.from_algos(1) + result = transaction_sender.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=amount, + ) + ) + + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + txn = cast(PaymentTxn, result.transaction) + assert txn.sender == sender.address + assert txn.receiver == receiver.address + assert txn.amt == amount.micro_algos + + +def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: + total = 1000 + params = AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + + result = transaction_sender.asset_create(params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + txn = cast(AssetCreateTxn, result.transaction) + assert txn.sender == sender.address + assert txn.total == total + assert txn.decimals == 0 + assert txn.default_frozen is False + assert txn.unit_name == "TEST" + assert txn.asset_name == "Test Asset" + assert txn.url == "https://example.com" + + +def test_asset_config(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="CFG", + asset_name="Config Asset", + url="https://example.com", + manager=sender.address, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then configure it + config_params = AssetConfigParams( + sender=sender.address, + asset_id=asset_id, + manager=receiver.address, + ) + result = transaction_sender.asset_config(config_params) + + assert len(result.tx_ids) == 1 + assert isinstance(result.transaction, AssetConfigTxn) + assert result.transaction.sender == sender.address + assert result.transaction.index == asset_id + assert result.transaction.manager == receiver.address + + +def test_asset_freeze( + transaction_sender: AlgorandClientTransactionSender, + sender: Account, +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="FRZ", + url="https://example.com", + asset_name="Freeze Asset", + freeze=sender.address, + manager=sender.address, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then freeze it + freeze_params = AssetFreezeParams( + sender=sender.address, + asset_id=asset_id, + account=sender.address, + frozen=True, + ) + result = transaction_sender.asset_freeze(freeze_params) + + assert len(result.tx_ids) == 1 + txn = cast(AssetFreezeTxn, result.transaction) + assert txn.sender == sender.address + assert txn.index == asset_id + assert txn.target == sender.address + assert txn.new_freeze_state is True + + +def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="DEL", + asset_name="Delete Asset", + manager=sender.address, + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then destroy it + destroy_params = AssetDestroyParams( + sender=sender.address, + asset_id=asset_id, + ) + result = transaction_sender.asset_destroy(destroy_params) + + assert len(result.tx_ids) == 1 + txn = cast(AssetDestroyTxn, result.transaction) + assert txn.sender == sender.address + assert txn.index == asset_id + + +def test_asset_transfer( + transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="XFR", + asset_name="Transfer Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in receiver + transaction_sender.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + ) + + # Then transfer it + amount = 100 + transfer_params = AssetTransferParams( + sender=sender.address, + asset_id=asset_id, + receiver=receiver.address, + amount=amount, + ) + result = transaction_sender.asset_transfer(transfer_params) + + assert len(result.tx_ids) == 1 + txn = cast(AssetTransferTxn, result.transaction) + assert txn.sender == sender.address + assert txn.index == asset_id + assert txn.receiver == receiver.address + assert txn.amount == amount + + +def test_asset_opt_in(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="OPT", + asset_name="Opt Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in + opt_in_params = AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + result = transaction_sender.asset_opt_in(opt_in_params) + + assert len(result.tx_ids) == 1 + txn = cast(AssetTransferTxn, result.transaction) + assert txn.sender == receiver.address + assert txn.index == asset_id + assert txn.amount == 0 + assert txn.receiver == receiver.address + + +def test_asset_opt_out(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="OUT", + asset_name="Opt Out Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in + transaction_sender.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + ) + + # Then opt-out + opt_out_params = AssetOptOutParams( + sender=receiver.address, + asset_id=asset_id, + creator=sender.address, + signer=receiver.signer, + ) + result = transaction_sender.asset_opt_out(params=opt_out_params) + + txn = cast(AssetTransferTxn, result.transaction) + assert txn.sender == receiver.address + assert txn.index == asset_id + assert txn.amount == 0 + assert txn.receiver == receiver.address + assert txn.close_assets_to == sender.address + + +def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + params = AppCreateParams( + sender=sender.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + ) + + result = transaction_sender.app_create(params) + assert result.app_id > 0 + assert result.app_address + txn = cast(ApplicationCreateTxn, result.transaction) + assert txn.sender == sender.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + + +# TODO: add remaining app call and app method call tests + + +@patch("logging.Logger.debug") +def test_payment_logging( + mock_debug: MagicMock, + transaction_sender: AlgorandClientTransactionSender, + sender: Account, + receiver: Account, +) -> None: + amount = AlgoAmount.from_algos(1) + transaction_sender.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=amount, + ) + ) + + assert mock_debug.call_count == 1 + log_message = mock_debug.call_args[0][0] + assert "Sending 1,000,000 µALGO" in log_message + assert sender.address in log_message + assert receiver.address in log_message + + +def test_online_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: + sp = transaction_sender._algod.suggested_params() # noqa: SLF001 + + params = OnlineKeyRegistrationParams( + sender=sender.address, + vote_key="G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=", + selection_key="LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=", + state_proof_key=b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", + vote_first=sp.first, + vote_last=sp.first + int(10e6), + vote_key_dilution=100, + ) + + result = transaction_sender.online_key_registration(params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] From 62c121a96881877c7086f9fd7708f39224ae8c08 Mon Sep 17 00:00:00 2001 From: Al Date: Wed, 11 Dec 2024 18:20:32 +0100 Subject: [PATCH 04/31] feat: AppClient, AppFactory, AppDeployer interface and various refinements on top of existing new interfaces (#124) * chore: bump ruff * chore: wip; arc32 to arc56 converter * chore: wip * chore: wip * chore: wip * chore: wip * chore: finalizing initial tests around call * chore: wip * chore: extra tests; wip * chore: adding initial logic error exposer * chore: resource population; skeletons for appdeployer and appfactory * chore: adding draft deprecation warnings; wip * chore: wip * chore: wip * chore: wip * chore: wip * chore: mypy and ruff tweaks wip * chore: make some asset param fields optional; add subtraction dunder to AlgoAmount * chore: more tests; updating deprecation decorators; initial tweaks for ruff --- .pre-commit-config.yaml | 2 + .vscode/launch.json | 16 + .vscode/settings.json | 23 +- docs/markdown/index.md | 69 +- legacy_v2_tests/conftest.py | 6 +- legacy_v2_tests/test_account.py | 1 - legacy_v2_tests/test_app.py | 1 + legacy_v2_tests/test_app_client.py | 1 + ...new_client_missing_source_map.approved.txt | 6 +- legacy_v2_tests/test_app_client_call.py | 40 +- .../test_app_client_clear_state.py | 4 +- legacy_v2_tests/test_app_client_close_out.py | 4 +- legacy_v2_tests/test_app_client_create.py | 6 +- legacy_v2_tests/test_app_client_delete.py | 4 +- legacy_v2_tests/test_app_client_deploy.py | 4 +- legacy_v2_tests/test_app_client_opt_in.py | 4 +- legacy_v2_tests/test_app_client_prepare.py | 3 +- legacy_v2_tests/test_app_client_resolve.py | 1 - .../test_app_client_signer_sender.py | 5 +- .../test_app_client_template_values.py | 2 +- legacy_v2_tests/test_app_client_update.py | 2 +- legacy_v2_tests/test_asset.py | 3 +- legacy_v2_tests/test_debug_utils.py | 16 +- legacy_v2_tests/test_deploy.py | 1 - legacy_v2_tests/test_deploy_scenarios.py | 8 +- legacy_v2_tests/test_dispenser_api_client.py | 3 +- legacy_v2_tests/test_transfer.py | 18 +- poetry.lock | 800 +++++----- pyproject.toml | 18 +- src/algokit_utils/__init__.py | 110 +- src/algokit_utils/_debugging.py | 52 +- .../_legacy_v2/_ensure_funded.py | 5 + src/algokit_utils/_legacy_v2/_transfer.py | 2 +- src/algokit_utils/_legacy_v2/account.py | 27 +- .../_legacy_v2/application_client.py | 23 +- .../_legacy_v2/application_specification.py | 11 +- src/algokit_utils/_legacy_v2/asset.py | 9 + src/algokit_utils/_legacy_v2/deploy.py | 30 +- src/algokit_utils/_legacy_v2/logic_error.py | 11 +- src/algokit_utils/_legacy_v2/models.py | 8 +- .../_legacy_v2/network_clients.py | 14 +- src/algokit_utils/accounts/account_manager.py | 463 +++++- .../accounts/kmd_account_manager.py | 190 +++ src/algokit_utils/applications/app_client.py | 1396 +++++++++++++++++ .../applications/app_deployer.py | 602 +++++++ src/algokit_utils/applications/app_factory.py | 690 ++++++++ src/algokit_utils/applications/app_manager.py | 119 +- src/algokit_utils/applications/utils.py | 428 +++++ src/algokit_utils/assets/asset_manager.py | 6 +- src/algokit_utils/clients/algorand_client.py | 57 +- src/algokit_utils/clients/client_manager.py | 249 ++- .../clients/dispenser_api_client.py | 5 +- src/algokit_utils/config.py | 60 +- src/algokit_utils/errors/logic_error.py | 129 ++ src/algokit_utils/models/abi.py | 12 +- src/algokit_utils/models/account.py | 4 +- src/algokit_utils/models/amount.py | 24 + src/algokit_utils/models/application.py | 464 ++++++ src/algokit_utils/models/network.py | 20 + src/algokit_utils/models/transaction.py | 8 + src/algokit_utils/protocols/__init__.py | 0 src/algokit_utils/protocols/application.py | 61 + src/algokit_utils/transactions/models.py | 52 +- .../transactions/transaction_composer.py | 443 +++--- .../transactions/transaction_sender.py | 51 +- src/algokit_utils/transactions/utils.py | 302 ++++ tests/accounts/__init__.py | 0 tests/accounts/test_account_manager.py | 108 ++ tests/applications/test_app_client.py | 733 +++++++++ tests/applications/test_app_factory.py | 488 ++++++ tests/applications/test_app_manager.py | 22 +- tests/applications/test_utils.py | 16 + .../artifacts/hello_world/approval.teal | 0 .../artifacts/hello_world/arc32_app_spec.json | 55 + .../artifacts/hello_world/clear.teal | 0 .../legacy_hello_world/arc32_app_spec.json | 378 +++++ .../artifacts/testing_app/arc32_app_spec.json | 400 +++++ tests/artifacts/testing_app/contract.py | 185 +++ .../testing_app/sources.teal.map.json | 22 + .../testing_app_arc56/arc56_app_spec.json | 681 ++++++++ .../testing_app_puya/arc32_app_spec.json | 184 +++ tests/artifacts/testing_app_puya/contract.py | 43 + tests/assets/test_asset_manager.py | 39 +- tests/clients/algorand_client/__init__.py | 0 .../clients/algorand_client/test_transfer.py | 427 +++++ tests/clients/test_algorand_client.py | 223 --- tests/conftest.py | 93 +- tests/test_transaction_composer.py | 35 +- .../transactions/test_transaction_composer.py | 53 +- .../transactions/test_transaction_creator.py | 51 +- tests/transactions/test_transaction_sender.py | 163 +- tests/utils.py | 29 + 92 files changed, 10226 insertions(+), 1410 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/algokit_utils/accounts/kmd_account_manager.py create mode 100644 src/algokit_utils/applications/app_client.py create mode 100644 src/algokit_utils/applications/app_deployer.py create mode 100644 src/algokit_utils/applications/app_factory.py create mode 100644 src/algokit_utils/applications/utils.py create mode 100644 src/algokit_utils/errors/logic_error.py create mode 100644 src/algokit_utils/models/network.py create mode 100644 src/algokit_utils/models/transaction.py create mode 100644 src/algokit_utils/protocols/__init__.py create mode 100644 src/algokit_utils/protocols/application.py create mode 100644 src/algokit_utils/transactions/utils.py create mode 100644 tests/accounts/__init__.py create mode 100644 tests/accounts/test_account_manager.py create mode 100644 tests/applications/test_app_client.py create mode 100644 tests/applications/test_app_factory.py create mode 100644 tests/applications/test_utils.py rename tests/{transactions => }/artifacts/hello_world/approval.teal (100%) create mode 100644 tests/artifacts/hello_world/arc32_app_spec.json rename tests/{transactions => }/artifacts/hello_world/clear.teal (100%) create mode 100644 tests/artifacts/legacy_hello_world/arc32_app_spec.json create mode 100644 tests/artifacts/testing_app/arc32_app_spec.json create mode 100644 tests/artifacts/testing_app/contract.py create mode 100644 tests/artifacts/testing_app/sources.teal.map.json create mode 100644 tests/artifacts/testing_app_arc56/arc56_app_spec.json create mode 100644 tests/artifacts/testing_app_puya/arc32_app_spec.json create mode 100644 tests/artifacts/testing_app_puya/contract.py create mode 100644 tests/clients/algorand_client/__init__.py create mode 100644 tests/clients/algorand_client/test_transfer.py delete mode 100644 tests/clients/test_algorand_client.py create mode 100644 tests/utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c320aa..fdfd6d3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,7 @@ repos: additional_dependencies: [] minimum_pre_commit_version: "0" files: "^(src|tests)/" + exclude: "^tests/artifacts/" - id: mypy name: mypy description: "`mypy` will check Python types for correctness" @@ -33,3 +34,4 @@ repos: additional_dependencies: [] minimum_pre_commit_version: "2.9.2" files: "^(src|tests)/" + exclude: "^tests/artifacts/" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6b9d5948 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Tests", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "purpose": [ + "debug-test" + ], + "console": "integratedTerminal", + "justMyCode": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e570b2a6..a1162966 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,18 +16,24 @@ "**/__pycache__": true, ".idea": true }, - // Python "platformSettings.autoLoad": true, "python.defaultInterpreterPath": "${workspaceFolder}/.venv", - "python.analysis.extraPaths": ["${workspaceFolder}/src"], + "python.analysis.extraPaths": [ + "${workspaceFolder}/src" + ], "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" }, + "python.analysis.exclude": [ + "tests/artifacts/**" + ], "python.analysis.typeCheckingMode": "basic", "ruff.enable": true, "ruff.lint.run": "onSave", - "ruff.lint.args": ["--config=pyproject.toml"], + "ruff.lint.args": [ + "--config=pyproject.toml" + ], "ruff.importStrategy": "fromEnvironment", "ruff.fixAll": true, //lint and fix all files in workspace "ruff.organizeImports": true, //organize imports on save @@ -37,7 +43,6 @@ "ruff.codeAction.fixViolation": { "enable": true }, - "mypy.configFile": "pyproject.toml", // set to empty array to use config from project "mypy.targets": [], @@ -52,11 +57,7 @@ } ] }, - - // PowerShell - "[powershell]": { - "editor.defaultFormatter": "ms-vscode.powershell" - }, - "powershell.codeFormatting.preset": "Stroustrup", - "python.testing.pytestArgs": ["."] + "python.testing.pytestArgs": [ + "." + ], } diff --git a/docs/markdown/index.md b/docs/markdown/index.md index a3fa0518..71972566 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -7,46 +7,47 @@ The goal of this library is to provide intuitive, productive utility functions t Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. #### NOTE + If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). [Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]() # Contents -* [Account management](capabilities/account.md) - * [`Account`](capabilities/account.md#account) -* [Client management](capabilities/client.md) - * [Network configuration](capabilities/client.md#network-configuration) - * [Clients](capabilities/client.md#clients) -* [App client](capabilities/app-client.md) - * [Design](capabilities/app-client.md#design) - * [Creating an application client](capabilities/app-client.md#creating-an-application-client) - * [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) - * [Composing calls](capabilities/app-client.md#composing-calls) - * [Reading state](capabilities/app-client.md#reading-state) - * [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) -* [App deployment](capabilities/app-deploy.md) - * [Design](capabilities/app-deploy.md#design) - * [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) - * [Deploying an application](capabilities/app-deploy.md#deploying-an-application) -* [Algo transfers](capabilities/transfer.md) - * [Transferring Algos](capabilities/transfer.md#transferring-algos) - * [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) - * [Transfering Assets](capabilities/transfer.md#transfering-assets) - * [Dispenser](capabilities/transfer.md#dispenser) -* [TestNet Dispenser Client](capabilities/dispenser-client.md) - * [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) - * [Funding an Account](capabilities/dispenser-client.md#funding-an-account) - * [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) - * [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) - * [Error Handling](capabilities/dispenser-client.md#error-handling) -* [Debugger](capabilities/debugger.md) - * [Configuration](capabilities/debugger.md#configuration) - * [Debugging Utilities](capabilities/debugger.md#debugging-utilities) -* [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) - * [Data](apidocs/algokit_utils/algokit_utils.md#data) - * [Classes](apidocs/algokit_utils/algokit_utils.md#classes) - * [Functions](apidocs/algokit_utils/algokit_utils.md#functions) +- [Account management](capabilities/account.md) + - [`Account`](capabilities/account.md#account) +- [Client management](capabilities/client.md) + - [Network configuration](capabilities/client.md#network-configuration) + - [Clients](capabilities/client.md#clients) +- [App client](capabilities/app-client.md) + - [Design](capabilities/app-client.md#design) + - [Creating an application client](capabilities/app-client.md#creating-an-application-client) + - [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) + - [Composing calls](capabilities/app-client.md#composing-calls) + - [Reading state](capabilities/app-client.md#reading-state) + - [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) +- [App deployment](capabilities/app-deploy.md) + - [Design](capabilities/app-deploy.md#design) + - [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) + - [Deploying an application](capabilities/app-deploy.md#deploying-an-application) +- [Algo transfers](capabilities/transfer.md) + - [Transferring Algos](capabilities/transfer.md#transferring-algos) + - [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) + - [Transfering Assets](capabilities/transfer.md#transfering-assets) + - [Dispenser](capabilities/transfer.md#dispenser) +- [TestNet Dispenser Client](capabilities/dispenser-client.md) + - [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) + - [Funding an Account](capabilities/dispenser-client.md#funding-an-account) + - [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) + - [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) + - [Error Handling](capabilities/dispenser-client.md#error-handling) +- [Debugger](capabilities/debugger.md) + - [Configuration](capabilities/debugger.md#configuration) + - [Debugging Utilities](capabilities/debugger.md#debugging-utilities) +- [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) + - [Data](apidocs/algokit_utils/algokit_utils.md#data) + - [Classes](apidocs/algokit_utils/algokit_utils.md#classes) + - [Functions](apidocs/algokit_utils/algokit_utils.md#functions) diff --git a/legacy_v2_tests/conftest.py b/legacy_v2_tests/conftest.py index dbe4be46..f8989eb8 100644 --- a/legacy_v2_tests/conftest.py +++ b/legacy_v2_tests/conftest.py @@ -8,6 +8,8 @@ import algosdk.transaction import pytest +from dotenv import load_dotenv + from algokit_utils import ( DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, @@ -22,8 +24,6 @@ get_kmd_client_from_algod_client, replace_template_variables, ) -from dotenv import load_dotenv - from legacy_v2_tests import app_client_test if TYPE_CHECKING: @@ -142,7 +142,7 @@ def indexer_client() -> "IndexerClient": return get_indexer_client() -@pytest.fixture() +@pytest.fixture def creator(algod_client: "AlgodClient") -> Account: creator_name = get_unique_name() return get_account(algod_client, creator_name) diff --git a/legacy_v2_tests/test_account.py b/legacy_v2_tests/test_account.py index bb0ee272..e1ee2228 100644 --- a/legacy_v2_tests/test_account.py +++ b/legacy_v2_tests/test_account.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from algokit_utils import get_account - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app.py b/legacy_v2_tests/test_app.py index 07e258a1..1b79f708 100644 --- a/legacy_v2_tests/test_app.py +++ b/legacy_v2_tests/test_app.py @@ -1,4 +1,5 @@ import pytest + from algokit_utils import AppDeployMetaData diff --git a/legacy_v2_tests/test_app_client.py b/legacy_v2_tests/test_app_client.py index 87826175..b6565148 100644 --- a/legacy_v2_tests/test_app_client.py +++ b/legacy_v2_tests/test_app_client.py @@ -1,4 +1,5 @@ import pytest + from algokit_utils import ( DeploymentFailedError, get_next_version, diff --git a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt index 598d4c2f..70d16cc9 100644 --- a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt +++ b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt @@ -2,6 +2,6 @@ Txn {txn} had error 'assert failed pc=743' at PC 743: Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map \ No newline at end of file + 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2. Set approval_source_map from a previously compiled approval program OR + 3. Import a previously exported source map using import_source_map \ No newline at end of file diff --git a/legacy_v2_tests/test_app_client_call.py b/legacy_v2_tests/test_app_client_call.py index 67acd4d5..14933f1b 100644 --- a/legacy_v2_tests/test_app_client_call.py +++ b/legacy_v2_tests/test_app_client_call.py @@ -1,17 +1,10 @@ from collections.abc import Generator +from hashlib import sha256 from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import Mock, patch -import algokit_utils import pytest -from algokit_utils import ( - Account, - ApplicationClient, - ApplicationSpecification, - CreateCallParameters, - get_account, -) from algosdk.atomic_transaction_composer import ( AccountTransactionSigner, AtomicTransactionComposer, @@ -19,6 +12,16 @@ ) from algosdk.transaction import ApplicationCallTxn, PaymentTxn +import algokit_utils +import algokit_utils._legacy_v2 +import algokit_utils._legacy_v2.logic_error +from algokit_utils import ( + Account, + ApplicationClient, + ApplicationSpecification, + CreateCallParameters, + get_account, +) from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: @@ -84,7 +87,7 @@ def test_abi_call_with_transaction_arg(client_fixture: ApplicationClient, funded sender=funded_account.address, receiver=client_fixture.app_address, amt=1_000_000, - note=b"Payment", + note=sha256(b"self-payment").digest(), sp=client_fixture.algod_client.suggested_params(), ) # type: ignore[no-untyped-call] payment_with_signer = TransactionWithSigner(payment, AccountTransactionSigner(funded_account.private_key)) @@ -186,7 +189,7 @@ def test_readonly_call(client_fixture: ApplicationClient) -> None: def test_readonly_call_with_error(client_fixture: ApplicationClient) -> None: - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -211,7 +214,7 @@ def test_readonly_call_with_error_with_new_client_provided_template_values( ) new_client.approval_source_map = client.approval_source_map - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -234,7 +237,7 @@ def test_readonly_call_with_error_with_new_client_provided_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) new_client.approval_source_map = client.approval_source_map - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -259,7 +262,7 @@ def test_readonly_call_with_error_with_imported_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) new_client.import_source_map(source_map_export) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -281,7 +284,7 @@ def test_readonly_call_with_error_with_new_client_missing_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -292,7 +295,7 @@ def test_readonly_call_with_error_with_new_client_missing_source_map( def test_readonly_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixture: ApplicationClient) -> None: mock_config.debug = False - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -302,7 +305,7 @@ def test_readonly_call_with_error_debug_mode_disabled(mock_config: Mock, client_ def test_readonly_call_with_error_debug_mode_enabled(client_fixture: ApplicationClient) -> None: - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -322,7 +325,7 @@ def test_app_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixtu min_funding_increment_micro_algos=200_000, ), ) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "set_box", name=b"ssss", @@ -342,7 +345,7 @@ def test_app_call_with_error_debug_mode_enabled(client_fixture: ApplicationClien min_funding_increment_micro_algos=200_000, ), ) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "set_box", name=b"ssss", @@ -350,4 +353,3 @@ def test_app_call_with_error_debug_mode_enabled(client_fixture: ApplicationClien ) assert ex.value.traces is not None - assert ex.value.traces[0].exec_trace["approval-program-trace"] is not None diff --git a/legacy_v2_tests/test_app_client_clear_state.py b/legacy_v2_tests/test_app_client_clear_state.py index f26a7094..1d2f6529 100644 --- a/legacy_v2_tests/test_app_client_clear_state.py +++ b/legacy_v2_tests/test_app_client_clear_state.py @@ -2,12 +2,12 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, ) - from legacy_v2_tests.conftest import is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_close_out.py b/legacy_v2_tests/test_app_client_close_out.py index 5ee5e9c6..81ac5ea9 100644 --- a/legacy_v2_tests/test_app_client_close_out.py +++ b/legacy_v2_tests/test_app_client_close_out.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_create.py b/legacy_v2_tests/test_app_client_create.py index 1da7bbf7..00fd9691 100644 --- a/legacy_v2_tests/test_app_client_create.py +++ b/legacy_v2_tests/test_app_client_create.py @@ -2,6 +2,9 @@ from typing import TYPE_CHECKING import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionSigner +from algosdk.transaction import ApplicationCallTxn, GenericSignedTransaction, OnComplete, Transaction + from algokit_utils import ( Account, ApplicationClient, @@ -10,9 +13,6 @@ get_account, get_app_id_from_tx_id, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionSigner -from algosdk.transaction import ApplicationCallTxn, GenericSignedTransaction, OnComplete, Transaction - from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_delete.py b/legacy_v2_tests/test_app_client_delete.py index 353bbfab..d5df42cb 100644 --- a/legacy_v2_tests/test_app_client_delete.py +++ b/legacy_v2_tests/test_app_client_delete.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_deploy.py b/legacy_v2_tests/test_app_client_deploy.py index 4eed49b6..e51392b4 100644 --- a/legacy_v2_tests/test_app_client_deploy.py +++ b/legacy_v2_tests/test_app_client_deploy.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( ABICreateCallArgs, Account, @@ -9,7 +10,6 @@ TransferParameters, transfer, ) - from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: @@ -17,7 +17,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_opt_in.py b/legacy_v2_tests/test_app_client_opt_in.py index 816e96f0..afc1fb1e 100644 --- a/legacy_v2_tests/test_app_client_opt_in.py +++ b/legacy_v2_tests/test_app_client_opt_in.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_prepare.py b/legacy_v2_tests/test_app_client_prepare.py index 6c6355b0..affacd50 100644 --- a/legacy_v2_tests/test_app_client_prepare.py +++ b/legacy_v2_tests/test_app_client_prepare.py @@ -1,11 +1,12 @@ import base64 from typing import TYPE_CHECKING +from algosdk.atomic_transaction_composer import AccountTransactionSigner + from algokit_utils import ( ApplicationClient, ApplicationSpecification, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/legacy_v2_tests/test_app_client_resolve.py b/legacy_v2_tests/test_app_client_resolve.py index 6c6023f3..d7e8b1d1 100644 --- a/legacy_v2_tests/test_app_client_resolve.py +++ b/legacy_v2_tests/test_app_client_resolve.py @@ -5,7 +5,6 @@ ApplicationClient, DefaultArgumentDict, ) - from legacy_v2_tests.conftest import read_spec if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_signer_sender.py b/legacy_v2_tests/test_app_client_signer_sender.py index d6c383cb..cfdef0ac 100644 --- a/legacy_v2_tests/test_app_client_signer_sender.py +++ b/legacy_v2_tests/test_app_client_signer_sender.py @@ -3,12 +3,13 @@ from typing import TYPE_CHECKING, Any import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner + from algokit_utils import ( ApplicationClient, ApplicationSpecification, get_sender_from_signer, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner if TYPE_CHECKING: from algosdk import transaction @@ -30,7 +31,7 @@ def sign_transactions( @pytest.mark.parametrize("override_signer", [CustomSigner(), AccountTransactionSigner(fake_key), None]) @pytest.mark.parametrize("default_sender", ["default_sender", None]) @pytest.mark.parametrize("default_signer", [CustomSigner(), AccountTransactionSigner(fake_key), None]) -def test_resolve_signer_sender( # noqa: PLR0913 +def test_resolve_signer_sender( *, algod_client: "AlgodClient", app_spec: ApplicationSpecification, diff --git a/legacy_v2_tests/test_app_client_template_values.py b/legacy_v2_tests/test_app_client_template_values.py index 5b27f320..a01f53d9 100644 --- a/legacy_v2_tests/test_app_client_template_values.py +++ b/legacy_v2_tests/test_app_client_template_values.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING -import algokit_utils import pytest +import algokit_utils from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_update.py b/legacy_v2_tests/test_app_client_update.py index 60cd10d9..4dc082e0 100644 --- a/legacy_v2_tests/test_app_client_update.py +++ b/legacy_v2_tests/test_app_client_update.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_asset.py b/legacy_v2_tests/test_asset.py index 3d75fa86..c26906ff 100644 --- a/legacy_v2_tests/test_asset.py +++ b/legacy_v2_tests/test_asset.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, EnsureBalanceParameters, @@ -19,7 +20,7 @@ from legacy_v2_tests.conftest import assure_funds, generate_test_asset, get_unique_name -@pytest.fixture() +@pytest.fixture def to_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index 9b6d8ca8..b827ecd3 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -3,6 +3,13 @@ from unittest.mock import Mock import pytest +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + from algokit_utils._debugging import ( AVMDebuggerSourceMap, PersistSourceMapInput, @@ -14,20 +21,13 @@ from algokit_utils.application_specification import ApplicationSpecification from algokit_utils.common import Program from algokit_utils.models import Account -from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, - AtomicTransactionComposer, - TransactionWithSigner, -) -from algosdk.transaction import PaymentTxn - from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -@pytest.fixture() +@pytest.fixture def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: creator_name = get_unique_name() creator = get_account(algod_client, creator_name) diff --git a/legacy_v2_tests/test_deploy.py b/legacy_v2_tests/test_deploy.py index 51708f52..4d2cf8c0 100644 --- a/legacy_v2_tests/test_deploy.py +++ b/legacy_v2_tests/test_deploy.py @@ -2,7 +2,6 @@ replace_template_variables, ) from algokit_utils._legacy_v2.deploy import strip_comments - from legacy_v2_tests.conftest import check_output_stability diff --git a/legacy_v2_tests/test_deploy_scenarios.py b/legacy_v2_tests/test_deploy_scenarios.py index 309fe4a3..c230ce37 100644 --- a/legacy_v2_tests/test_deploy_scenarios.py +++ b/legacy_v2_tests/test_deploy_scenarios.py @@ -6,6 +6,7 @@ from unittest.mock import Mock, patch import pytest + from algokit_utils import ( Account, ApplicationClient, @@ -19,7 +20,6 @@ get_indexer_client, get_localnet_default_account, ) - from legacy_v2_tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def __init__( self.creator = creator self.app_name = get_unique_name() - def deploy( # noqa: PLR0913 + def deploy( self, app_spec: ApplicationSpecification, *, @@ -128,12 +128,12 @@ def creator(creator_name: str) -> Account: return get_account(get_algod_client(), creator_name) -@pytest.fixture() +@pytest.fixture def app_name() -> str: return get_unique_name() -@pytest.fixture() +@pytest.fixture def deploy_fixture( caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest, creator_name: str, creator: Account ) -> DeployFixture: diff --git a/legacy_v2_tests/test_dispenser_api_client.py b/legacy_v2_tests/test_dispenser_api_client.py index baa2e1db..ac7fa0f8 100644 --- a/legacy_v2_tests/test_dispenser_api_client.py +++ b/legacy_v2_tests/test_dispenser_api_client.py @@ -1,13 +1,14 @@ import json import pytest +from pytest_httpx import HTTPXMock + from algokit_utils.dispenser_api import ( DISPENSER_ASSETS, DispenserApiConfig, DispenserAssetName, TestNetDispenserApiClient, ) -from pytest_httpx import HTTPXMock class TestDispenserApiTestnetClient: diff --git a/legacy_v2_tests/test_transfer.py b/legacy_v2_tests/test_transfer.py index 8253a5eb..335fcf1a 100644 --- a/legacy_v2_tests/test_transfer.py +++ b/legacy_v2_tests/test_transfer.py @@ -3,6 +3,11 @@ import algosdk import httpx import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.transaction import PaymentTxn +from algosdk.util import algos_to_microalgos +from pytest_httpx import HTTPXMock + from algokit_utils import ( Account, EnsureBalanceParameters, @@ -19,11 +24,6 @@ ) from algokit_utils.dispenser_api import DispenserApiConfig from algokit_utils.network_clients import get_algod_client, get_algonode_config -from algosdk.atomic_transaction_composer import AccountTransactionSigner -from algosdk.transaction import PaymentTxn -from algosdk.util import algos_to_microalgos -from pytest_httpx import HTTPXMock - from legacy_v2_tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name from legacy_v2_tests.test_network_clients import DEFAULT_TOKEN @@ -35,12 +35,12 @@ MINIMUM_BALANCE = 100_000 # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance -@pytest.fixture() +@pytest.fixture def to_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) -@pytest.fixture() +@pytest.fixture def rekeyed_from_account(algod_client: "AlgodClient", kmd_client: "KMDClient") -> Account: account = create_kmd_wallet_account(kmd_client, get_unique_name()) rekey_account = create_kmd_wallet_account(kmd_client, get_unique_name()) @@ -68,7 +68,7 @@ def rekeyed_from_account(algod_client: "AlgodClient", kmd_client: "KMDClient") - return Account(address=account.address, private_key=rekey_account.private_key) -@pytest.fixture() +@pytest.fixture def transaction_signer_from_account( kmd_client: "KMDClient", algod_client: "AlgodClient", @@ -87,7 +87,7 @@ def transaction_signer_from_account( return AccountTransactionSigner(private_key=account.private_key) -@pytest.fixture() +@pytest.fixture def clawback_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) diff --git a/poetry.lock b/poetry.lock index 3544afa0..e173428a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,35 +13,35 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.6" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, - {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, + {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, + {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, ] [package.dependencies] @@ -104,13 +104,13 @@ files = [ [[package]] name = "cachecontrol" -version = "0.14.0" +version = "0.14.1" description = "httplib2 caching for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, + {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, + {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, ] [package.dependencies] @@ -119,7 +119,7 @@ msgpack = ">=0.5.2,<2.0.0" requests = ">=2.16.0" [package.extras] -dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "furo", "mypy", "pytest", "pytest-cov", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] @@ -379,73 +379,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.3" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, - {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, - {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, - {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, - {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, - {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, - {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, - {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, - {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, - {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, - {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, - {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, - {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, - {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, - {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, - {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.dependencies] @@ -456,51 +456,53 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -538,20 +540,20 @@ files = [ [[package]] name = "deprecated" -version = "1.2.14" +version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, + {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, + {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, ] [package.dependencies] wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] [[package]] name = "distlib" @@ -765,13 +767,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.3" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -939,13 +941,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.4.1" +version = "25.5.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, - {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, ] [package.dependencies] @@ -968,13 +970,13 @@ type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "license-expression" -version = "30.3.1" +version = "30.4.0" description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "license_expression-30.3.1-py3-none-any.whl", hash = "sha256:97904b9185c7bbb1e98799606fa7424191c375e70ba63a524b6f7100e42ddc46"}, - {file = "license_expression-30.3.1.tar.gz", hash = "sha256:60d5bec1f3364c256a92b9a08583d7ea933c7aa272c8d36d04144a89a3858c01"}, + {file = "license_expression-30.4.0-py3-none-any.whl", hash = "sha256:7c8f240c6e20d759cb8455e49cb44a923d9e25c436bf48d7e5b8eea660782c04"}, + {file = "license_expression-30.4.0.tar.gz", hash = "sha256:6464397f8ed4353cc778999caec43b099f8d8d5b335f282e26a9eb9435522f05"}, ] [package.dependencies] @@ -1030,72 +1032,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1214,43 +1216,43 @@ files = [ [[package]] name = "mypy" -version = "1.12.0" +version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, - {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, - {file = "mypy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e"}, - {file = "mypy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a"}, - {file = "mypy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff"}, - {file = "mypy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7"}, - {file = "mypy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57"}, - {file = "mypy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309"}, - {file = "mypy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f"}, - {file = "mypy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9"}, - {file = "mypy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164"}, - {file = "mypy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475"}, - {file = "mypy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9"}, - {file = "mypy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642"}, - {file = "mypy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601"}, - {file = "mypy-1.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521"}, - {file = "mypy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893"}, - {file = "mypy-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721"}, - {file = "mypy-1.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3"}, - {file = "mypy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b"}, - {file = "mypy-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f"}, - {file = "mypy-1.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e"}, - {file = "mypy-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7"}, - {file = "mypy-1.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b"}, - {file = "mypy-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0"}, - {file = "mypy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8"}, - {file = "mypy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0"}, - {file = "mypy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1"}, - {file = "mypy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e"}, - {file = "mypy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa"}, - {file = "mypy-1.12.0-py3-none-any.whl", hash = "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266"}, - {file = "mypy-1.12.0.tar.gz", hash = "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] @@ -1260,6 +1262,7 @@ typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -1303,27 +1306,35 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, [[package]] name = "nh3" -version = "0.2.18" +version = "0.2.19" description = "Python bindings to the ammonia HTML sanitization library." optional = false python-versions = "*" files = [ - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86"}, - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204"}, - {file = "nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be"}, - {file = "nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844"}, - {file = "nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4"}, + {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, + {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, + {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, + {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, + {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, + {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, + {file = "nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804"}, ] [[package]] @@ -1339,13 +1350,13 @@ files = [ [[package]] name = "packageurl-python" -version = "0.15.6" +version = "0.16.0" description = "A purl aka. Package URL parser and builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packageurl_python-0.15.6-py3-none-any.whl", hash = "sha256:a40210652c89022772a6c8340d6066f7d5dc67132141e5284a4db7a27d0a8ab0"}, - {file = "packageurl_python-0.15.6.tar.gz", hash = "sha256:cbc89afd15d5f4d05db4f1b61297e5b97a43f61f28799f6d282aff467ed2ee96"}, + {file = "packageurl_python-0.16.0-py3-none-any.whl", hash = "sha256:5c3872638b177b0f1cf01c3673017b7b27ebee485693ae12a8bed70fa7fa7c35"}, + {file = "packageurl_python-0.16.0.tar.gz", hash = "sha256:69e3bf8a3932fe9c2400f56aaeb9f86911ecee2f9398dbe1b58ec34340be365d"}, ] [package.extras] @@ -1356,13 +1367,13 @@ test = ["pytest"] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1378,13 +1389,13 @@ files = [ [[package]] name = "pip" -version = "24.2" +version = "24.3.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" files = [ - {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, - {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, + {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, + {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, ] [[package]] @@ -1450,13 +1461,13 @@ testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pyte [[package]] name = "pkginfo" -version = "1.11.2" +version = "1.12.0" description = "Query metadata from sdists / bdists / installed packages." optional = false python-versions = ">=3.8" files = [ - {file = "pkginfo-1.11.2-py3-none-any.whl", hash = "sha256:9ec518eefccd159de7ed45386a6bb4c6ca5fa2cb3bd9b71154fae44f6f1b36a3"}, - {file = "pkginfo-1.11.2.tar.gz", hash = "sha256:c6bc916b8298d159e31f2c216e35ee5b86da7da18874f879798d0a1983537c86"}, + {file = "pkginfo-1.12.0-py3-none-any.whl", hash = "sha256:dcd589c9be4da8973eceffa247733c144812759aa67eaf4bbf97016a02f39088"}, + {file = "pkginfo-1.12.0.tar.gz", hash = "sha256:8ad91a0445a036782b9366ef8b8c2c50291f83a553478ba8580c73d3215700cf"}, ] [package.extras] @@ -1988,13 +1999,13 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.9.2" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, - {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] @@ -2007,28 +2018,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.4.10" +version = "0.8.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, - {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, - {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, - {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, - {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, - {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, + {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, + {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, + {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, + {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, + {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, + {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, + {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, ] [[package]] @@ -2074,33 +2086,33 @@ files = [ [[package]] name = "setuptools" -version = "75.2.0" +version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, - {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2394,13 +2406,43 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -2416,20 +2458,21 @@ files = [ [[package]] name = "tqdm" -version = "4.66.5" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, - {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -2459,13 +2502,13 @@ urllib3 = ">=1.26.0" [[package]] name = "types-deprecated" -version = "1.2.9.20240311" +version = "1.2.15.20241117" description = "Typing stubs for Deprecated" optional = false python-versions = ">=3.8" files = [ - {file = "types-Deprecated-1.2.9.20240311.tar.gz", hash = "sha256:0680e89989a8142707de8103f15d182445a533c1047fd9b7e8c5459101e9b90a"}, - {file = "types_Deprecated-1.2.9.20240311-py3-none-any.whl", hash = "sha256:d7793aaf32ff8f7e49a8ac781de4872248e0694c4b75a7a8a186c51167463f9d"}, + {file = "types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e"}, + {file = "types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884"}, ] [[package]] @@ -2512,13 +2555,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -2543,13 +2586,13 @@ files = [ [[package]] name = "wheel" -version = "0.44.0" +version = "0.45.1" description = "A built-package format for Python" optional = false python-versions = ">=3.8" files = [ - {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, - {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, ] [package.extras] @@ -2557,92 +2600,87 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -2656,4 +2694,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "66e85df44cca4d3edccb50f730dfb4e9dccf93582e78fa0074dc9b47baa925e2" +content-hash = "726e13c507aac04c65d86a3ad85222f7c218eaed40689343445e9ff8574e8ec2" diff --git a/pyproject.toml b/pyproject.toml index bd391ae5..f5efc256 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ deprecated = "^1.2.14" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" -ruff = ">=0.1.6,<0.5.0" +ruff = ">=0.1.6,<=0.8.2" pip-audit = "^2.5.6" pytest-mock = "^3.11.1" mypy = "^1.5.1" @@ -93,8 +93,6 @@ lint.select = [ "RUF", # Ruff-specific rules ] lint.ignore = [ - "ANN101", # no type for self - "ANN102", # no type for cls "RET505", # allow else after return "SIM108", # allow if-else in place of ternary "E111", # indentation is not a multiple of four @@ -106,6 +104,7 @@ lint.ignore = [ "Q002", # bad quotes docstring "Q003", # avoidable escaped quotes "W191", # indentation contains tabs + "ERA001", # commented out code ] # Exclude a variety of commonly ignored directories. extend-exclude = [ @@ -113,31 +112,38 @@ extend-exclude = [ ".git", ".mypy_cache", ".ruff_cache", - + "tests/artifacts", ] # Assume Python 3.10. target-version = "py310" +[tool.ruff.lint.pylint] +max-args = 10 + [tool.ruff.lint.flake8-annotations] allow-star-arg-any = true suppress-none-returning = true [tool.ruff.lint.per-file-ignores] "src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"] -"path/to/file.py" = ["E402"] +"src/algokit_utils/applications/app_client.py" = ["SLF001"] +"src/algokit_utils/applications/app_factory.py" = ["SLF001"] "tests/clients/test_algorand_client.py" = ["ERA001"] +"src/algokit_utils/_legacy_v2/**/*" = ["E501"] +"tests/**/*" = ["PLR2004"] [tool.poe.tasks] docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" +"tests/**/*" = ["PLR2004"] [tool.pytest.ini_options] pythonpath = ["src", "tests"] [tool.mypy] files = ["src", "tests"] -exclude = ["dist"] +exclude = ["dist", "tests/artifacts", "src/algokit_utils/_legacy_v2"] python_version = "3.10" warn_unused_ignores = true warn_redundant_casts = true diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index d89bad9b..5b3a1647 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -92,93 +92,93 @@ from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME __all__ = [ - # ==== LEGACY V2 EXPORTS BEGIN ==== - "create_kmd_wallet_account", - "get_account_from_mnemonic", - "get_or_create_kmd_wallet_account", - "get_localnet_default_account", - "get_dispenser_account", - "get_kmd_wallet_account", - "get_account", - "UPDATABLE_TEMPLATE_NAME", "DELETABLE_TEMPLATE_NAME", + "DISPENSER_ACCESS_TOKEN_KEY", + "DISPENSER_REQUEST_TIMEOUT", "NOTE_PREFIX", - "DeploymentFailedError", - "AppReference", - "AppDeployMetaData", - "AppMetaData", - "AppLookup", - "get_creator_apps", - "replace_template_variables", + "UPDATABLE_TEMPLATE_NAME", "ABIArgsDict", "ABICallArgs", "ABICallArgsDict", "ABICreateCallArgs", "ABICreateCallArgsDict", "ABIMethod", + "ABITransactionResponse", + "Account", + "AlgoClientConfig", + "AppDeployMetaData", + "AppLookup", + "AppMetaData", + "AppReference", + "AppSpecStateDict", + "ApplicationClient", + "ApplicationSpecification", + "CallConfig", + "CommonCallParameters", + "CommonCallParametersDict", "CreateCallParameters", "CreateCallParametersDict", "CreateTransactionParameters", - "CommonCallParameters", - "CommonCallParametersDict", + "DefaultArgumentDict", + "DefaultArgumentType", "DeployCallArgs", - "DeployCreateCallArgs", "DeployCallArgsDict", + "DeployCreateCallArgs", "DeployCreateCallArgsDict", + "DeployResponse", + "DeploymentFailedError", + "DispenserFundResponse", + "DispenserLimitResponse", + "EnsureBalanceParameters", + "EnsureFundedResponse", + "LogicError", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", "OnCompleteCallParameters", "OnCompleteCallParametersDict", - "TransactionParameters", - "TransactionParametersDict", - "ApplicationClient", - "DeployResponse", - "OnUpdate", "OnSchemaBreak", + "OnUpdate", "OperationPerformed", + "PersistSourceMapInput", + "Program", "TemplateValueDict", "TemplateValueMapping", - "Program", - "execute_atc_with_logic_error", - "get_app_id_from_tx_id", - "get_next_version", - "get_sender_from_signer", - "num_extra_program_pages", - "AppSpecStateDict", - "ApplicationSpecification", - "CallConfig", - "DefaultArgumentDict", - "DefaultArgumentType", - "MethodConfigDict", - "OnCompleteActionName", - "MethodHints", - "LogicError", - "ABITransactionResponse", - "Account", + "TestNetDispenserApiClient", + "TransactionParameters", + "TransactionParametersDict", "TransactionResponse", - "AlgoClientConfig", + "TransferAssetParameters", + "TransferParameters", + # ==== LEGACY V2 EXPORTS BEGIN ==== + "create_kmd_wallet_account", + "ensure_funded", + "execute_atc_with_logic_error", + "get_account", + "get_account_from_mnemonic", "get_algod_client", "get_algonode_config", + "get_app_id_from_tx_id", + "get_creator_apps", "get_default_localnet_config", + "get_dispenser_account", "get_indexer_client", "get_kmd_client_from_algod_client", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_next_version", + "get_or_create_kmd_wallet_account", + "get_sender_from_signer", "is_localnet", "is_mainnet", "is_testnet", - "TestNetDispenserApiClient", - "DispenserFundResponse", - "DispenserLimitResponse", - "DISPENSER_ACCESS_TOKEN_KEY", - "DISPENSER_REQUEST_TIMEOUT", - "EnsureBalanceParameters", - "EnsureFundedResponse", - "TransferParameters", - "ensure_funded", - "transfer", - "TransferAssetParameters", - "transfer_asset", + "num_extra_program_pages", "opt_in", "opt_out", "persist_sourcemaps", - "PersistSourceMapInput", + "replace_template_variables", "simulate_and_persist_response", + "transfer", + "transfer_asset", # ==== LEGACY V2 EXPORTS END ==== ] diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index de5ed182..0b9f798f 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -143,7 +143,7 @@ def _write_to_file(path: Path, content: str) -> None: path.write_text(content) -def _build_avm_sourcemap( # noqa: PLR0913 +def _build_avm_sourcemap( *, app_name: str, file_name: str, @@ -201,7 +201,18 @@ def persist_sourcemaps( _upsert_debug_sourcemaps(sourcemaps, project_root) -def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse: +def simulate_response( + atc: AtomicTransactionComposer, + algod_client: "AlgodClient", + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, # noqa: ARG001 TODO: revisit + fix_signers: bool | None = None, # noqa: ARG001 TODO: revisit +) -> SimulateAtomicTransactionResponse: """ Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. @@ -221,13 +232,31 @@ def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True, state_change=True) simulate_request = SimulateRequest( - txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config + txn_groups=txn_group, + allow_more_logs=allow_more_logs or True, + round=round, + extra_opcode_budget=extra_opcode_budget or 0, + allow_unnamed_resources=allow_unnamed_resources or True, + allow_empty_signatures=allow_empty_signatures or True, + exec_trace_config=exec_trace_config or trace_config, ) + return atc.simulate(algod_client, simulate_request) -def simulate_and_persist_response( - atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256 +def simulate_and_persist_response( # noqa: PLR0913 TODO: revisit + atc: AtomicTransactionComposer, + project_root: Path, + algod_client: "AlgodClient", + buffer_size_mb: float = 256, + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, + fix_signers: bool | None = None, ) -> SimulateAtomicTransactionResponse: """ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, @@ -252,7 +281,18 @@ def simulate_and_persist_response( txn_with_sign.txn.last_valid_round = sp.last txn_with_sign.txn.genesis_hash = sp.gh - response = simulate_response(atc_to_simulate, algod_client) + response = simulate_response( + atc_to_simulate, + algod_client, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) txn_results = response.simulate_response["txn-groups"] txn_types = [txn_result["txn-results"][0]["txn-result"]["txn"]["txn"]["type"] for txn_result in txn_results] diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index 2db90f36..99409b36 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.account import get_dispenser_account @@ -115,6 +116,10 @@ def _fund_using_transfer( return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt) +@deprecated( + "Use `algorand.account.ensure_funded()`, `algorand.account.ensure_funded_from_environment()`, " + "or `algorand.account.ensure_funded_from_testnet_dispenser_api()` instead" +) def ensure_funded( client: AlgodClient, parameters: EnsureBalanceParameters, diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index 6b59cd4c..28de779f 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -__all__ = ["TransferParameters", "transfer", "TransferAssetParameters", "transfer_asset"] +__all__ = ["TransferAssetParameters", "TransferParameters", "transfer", "transfer_asset"] logger = logging.getLogger(__name__) diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index d98a875a..9da21ca1 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -5,6 +5,7 @@ from algosdk.account import address_from_private_key from algosdk.mnemonic import from_private_key, to_private_key from algosdk.util import algos_to_microalgos +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet @@ -30,13 +31,17 @@ _DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 +@deprecated( + "Use `algorand.account.from_mnemonic()` instead. Example: " "`account = algorand.account.from_mnemonic(mnemonic)`" +) def get_account_from_mnemonic(mnemonic: str) -> Account: """Convert a mnemonic (25 word passphrase) into an Account""" private_key = to_private_key(mnemonic) - address = address_from_private_key(private_key) + address = str(address_from_private_key(private_key)) return Account(private_key=private_key, address=address) +@deprecated("Use `algorand.account.from_kmd()` instead. Example: " "`account = algorand.account.from_kmd(name)`") def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: """Creates a wallet with specified name""" wallet_id = kmd_client.create_wallet(name, "")["id"] @@ -50,6 +55,10 @@ def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " + "`account = algorand.account.from_kmd(name, fund_with=AlgoAmount.from_algo(1000))`" +) def get_or_create_kmd_wallet_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: @@ -90,6 +99,10 @@ def _is_default_account(account: dict[str, Any]) -> bool: return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " + "`account = algorand.account.from_kmd('unencrypted-default-wallet', lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000)`" +) def get_localnet_default_account(client: "AlgodClient") -> Account: """Returns the default Account in a LocalNet instance""" if not is_localnet(client): @@ -102,6 +115,10 @@ def get_localnet_default_account(client: "AlgodClient") -> Account: return account +@deprecated( + "Use `algorand.account.dispenser_from_environment()` or `algorand.account.localnet_dispenser()` instead. " + "Example: `dispenser = algorand.account.dispenser_from_environment()`" +) def get_dispenser_account(client: "AlgodClient") -> Account: """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" if is_localnet(client): @@ -109,6 +126,9 @@ def get_dispenser_account(client: "AlgodClient") -> Account: return get_account(client, "DISPENSER") +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " "`account = algorand.account.from_kmd(name, predicate)`" +) def get_kmd_wallet_account( client: "AlgodClient", kmd_client: "KMDClient", @@ -142,6 +162,11 @@ def get_kmd_wallet_account( return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated( + "Use `algorand.account.from_environment()` or `algorand.account.from_kmd()` or `algorand.account.random()` instead. " + "Example: " + "`account = algorand.account.from_environment('ACCOUNT', AlgoAmount.from_algo(1000))`" +) def get_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index a52639d1..0334c832 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -27,6 +27,7 @@ from algosdk.constants import APP_PAGE_MAX_SIZE from algosdk.logic import get_application_address from algosdk.source_map import SourceMap +from typing_extensions import deprecated import algokit_utils._legacy_v2.application_specification as au_spec import algokit_utils._legacy_v2.deploy as au_deploy @@ -83,6 +84,16 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) +@deprecated( + "Use AppClient from algokit_utils.applications instead. Example:\n" + "```python\n" + "from algokit_utils.clients import AlgorandClient\n" + "from algokit_utils.models.application import Arc56Contract\n" + "algorand_client = AlgorandClient.from_environment()\n" + "app_client = AppClient.from_network(app_spec=Arc56Contract.from_json(app_spec_json), " + "algorand=algorand_client, app_id=123)\n" + "```" +) class ApplicationClient: """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" @@ -239,7 +250,7 @@ def prepare( ) return new_client - def _prepare( # noqa: PLR0913 + def _prepare( self, target: "ApplicationClient", *, @@ -913,9 +924,9 @@ def _check_app_id(self) -> None: if self.app_id == 0: raise Exception( "ApplicationClient is not associated with an app instance, to resolve either:\n" - "1.) provide an app_id on construction OR\n" - "2.) provide a creator address so an app can be searched for OR\n" - "3.) create an app first using create or deploy methods" + "1.provide an app_id on construction OR\n" + "2.provide a creator address so an app can be searched for OR\n" + "3.create an app first using create or deploy methods" ) def _resolve_method( @@ -1254,6 +1265,10 @@ def _try_convert_to_logic_error( return None +@deprecated( + "The execute_atc_with_logic_error function is deprecated; use AppClient's error handling and TransactionComposer's " + "send method for equivalent functionality and improved error management." +) def execute_atc_with_logic_error( atc: AtomicTransactionComposer, algod_client: "AlgodClient", diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py index 865dece5..5b034929 100644 --- a/src/algokit_utils/_legacy_v2/application_specification.py +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -8,16 +8,17 @@ from algosdk.abi import Contract from algosdk.abi.method import MethodDict from algosdk.transaction import StateSchema +from typing_extensions import deprecated __all__ = [ + "AppSpecStateDict", + "ApplicationSpecification", "CallConfig", "DefaultArgumentDict", "DefaultArgumentType", "MethodConfigDict", - "OnCompleteActionName", "MethodHints", - "ApplicationSpecification", - "AppSpecStateDict", + "OnCompleteActionName", ] @@ -136,6 +137,10 @@ def _decode_state_schema(data: dict[str, int]) -> StateSchema: ) +@deprecated( + "The ApplicationSpecification class is deprecated. Use Arc56Contract and the TransactionComposer and AppClient " + "classes for modern application development." +) @dataclasses.dataclass(kw_only=True) class ApplicationSpecification: """ARC-0032 application specification diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 2f71cbf8..409523c9 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner from algosdk.constants import TX_GROUP_LIMIT from algosdk.transaction import AssetTransferTxn +from typing_extensions import deprecated if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -68,6 +69,10 @@ def _ensure_asset_balance_conditions( raise ValueError(error_message) +@deprecated( + "Use TransactionComposer.add_asset_opt_in() or AlgorandClient.asset.opt_in() instead. " + "Example: composer.add_asset_opt_in(AssetOptInParams(sender=account.address, asset_id=123))" +) def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, @@ -116,6 +121,10 @@ def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) return result +@deprecated( + "Use TransactionComposer.add_asset_opt_out() or AlgorandClient.asset.opt_out() instead. " + "Example: composer.add_asset_opt_out(AssetOptOutParams(sender=account.address, asset_id=123, creator=creator_address))" +) def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index ed0bd0e5..6a73ba45 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -7,11 +7,11 @@ from enum import Enum from typing import TYPE_CHECKING, TypeAlias, TypedDict +import algosdk from algosdk import transaction from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner -from algosdk.logic import get_application_address from algosdk.transaction import StateSchema -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._legacy_v2.application_specification import ( ApplicationSpecification, @@ -36,26 +36,26 @@ __all__ = [ - "UPDATABLE_TEMPLATE_NAME", "DELETABLE_TEMPLATE_NAME", "NOTE_PREFIX", + "UPDATABLE_TEMPLATE_NAME", "ABICallArgs", - "ABICreateCallArgs", "ABICallArgsDict", + "ABICreateCallArgs", "ABICreateCallArgsDict", - "DeploymentFailedError", - "AppReference", "AppDeployMetaData", - "AppMetaData", "AppLookup", + "AppMetaData", + "AppReference", "DeployCallArgs", - "DeployCreateCallArgs", "DeployCallArgsDict", + "DeployCreateCallArgs", "DeployCreateCallArgsDict", - "Deployer", "DeployResponse", - "OnUpdate", + "Deployer", + "DeploymentFailedError", "OnSchemaBreak", + "OnUpdate", "OperationPerformed", "TemplateValueDict", "TemplateValueMapping", @@ -175,6 +175,7 @@ def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: return None +@deprecated("Deprecated") def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified creator that have a transaction note containing {py:class}`AppDeployMetaData` @@ -222,7 +223,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - if create_metadata and create_metadata.name: apps[create_metadata.name] = AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=create_metadata, created_round=app_created_at_round, **(update_metadata or create_metadata).__dict__, @@ -255,7 +256,8 @@ class AppChanges: schema_change_description: str | None -def check_for_app_changes( # noqa: PLR0913 +@deprecated("Deprecated") +def check_for_app_changes( algod_client: "AlgodClient", *, new_approval: bytes, @@ -412,7 +414,7 @@ def check_template_variables(approval_program: str, template_values: TemplateVal logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") -@deprecated(reason="Use `AppManager.replace_template_variables` instead", version="3.0.0") +@deprecated("Use `AppManager.replace_template_variables` instead") def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: """Replaces `TMPL_*` variables in `program` with `template_values` @@ -809,7 +811,7 @@ def _create_metadata( ) -> AppMetaData: return AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=original_metadata or app_spec_note, created_round=created_round, updated_round=updated_round or created_round, diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py index a365a3c1..a556d90f 100644 --- a/src/algokit_utils/_legacy_v2/logic_error.py +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -2,6 +2,8 @@ from copy import copy from typing import TYPE_CHECKING, TypedDict +from typing_extensions import deprecated + from algokit_utils._legacy_v2.models import SimulationTrace if TYPE_CHECKING: @@ -37,8 +39,9 @@ def parse_logic_error( } +@deprecated("Use algokit_utils.models.error.LogicError instead") class LogicError(Exception): - def __init__( # noqa: PLR0913 + def __init__( self, *, logic_error_str: str, @@ -74,9 +77,9 @@ def trace(self, lines: int = 5) -> str: return """ Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map""" + 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2. Set approval_source_map from a previously compiled approval program OR + 3. Import a previously exported source map using import_source_map""" program_lines = copy(self.lines) program_lines[self.line_no] += "\t\t<-- Error" diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index d20bed83..7887cb60 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -9,7 +9,7 @@ SimulateAtomicTransactionResponse, TransactionSigner, ) -from deprecated import deprecated +from typing_extensions import deprecated # Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) @@ -185,17 +185,17 @@ class CreateCallParametersDict(OnCompleteCallParametersDict, total=False): # Pre 1.3.1 backwards compatibility -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class RawTransactionParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class CommonCallParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParametersDict instead", version="1.3.1") +@deprecated("Use TransactionParametersDict instead") class CommonCallParametersDict(TransactionParametersDict): """Deprecated, use TransactionParametersDict instead""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index b1bcc2cb..4d1341b9 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -6,19 +6,20 @@ from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +from typing_extensions import deprecated __all__ = [ "AlgoClientConfig", + "AlgoClientConfigs", "get_algod_client", "get_algonode_config", "get_default_localnet_config", "get_indexer_client", + "get_kmd_client", "get_kmd_client_from_algod_client", "is_localnet", "is_mainnet", "is_testnet", - "AlgoClientConfigs", - "get_kmd_client", ] @@ -40,12 +41,14 @@ class AlgoClientConfigs: kmd_config: AlgoClientConfig | None +@deprecated("Use AlgorandClient.client.algod") def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: """Returns the client configuration to point to the default LocalNet""" port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) +@deprecated("Use AlgorandClient.client.test_net() or AlgorandClient.main_net() instead") def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str ) -> AlgoClientConfig: @@ -56,6 +59,7 @@ def get_algonode_config( ) +@deprecated("Use AlgorandClient.client.from_environment() instead. Example: client = AlgorandClient.from_environment()") def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment @@ -65,6 +69,7 @@ def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: return AlgodClient(config.token, config.server, headers) +@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment @@ -73,6 +78,7 @@ def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: return KMDClient(config.token, config.server) +@deprecated("Use AlgorandClient.client.from_environment().indexer instead") def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. @@ -82,24 +88,28 @@ def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: return IndexerClient(config.token, config.server, headers) +@deprecated("Use AlgorandClient.client.is_local_net() instead") def is_localnet(client: AlgodClient) -> bool: """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" params = client.suggested_params() return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] +@deprecated("Use AlgorandClient.client.is_main_net() instead") def is_mainnet(client: AlgodClient) -> bool: """Returns True if client genesis is `mainnet-v1`""" params = client.suggested_params() return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] +@deprecated("Use AlgorandClient.client.is_test_net() instead") def is_testnet(client: AlgodClient) -> bool: """Returns True if client genesis is `testnet-v1`""" params = client.suggested_params() return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] +@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index d4d95d19..d997a211 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -1,19 +1,44 @@ +import os from collections.abc import Callable from dataclasses import dataclass from typing import Any -from algosdk.account import generate_account +from algosdk import mnemonic from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.mnemonic import to_private_key +from algosdk.transaction import SuggestedParams from typing_extensions import Self -from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.clients.dispenser_api_client import DispenserAssetName, TestNetDispenserApiClient +from algokit_utils.config import config +from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + PaymentParams, + SendAtomicTransactionComposerResults, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import SendSingleTransactionResult +logger = config.logger -@dataclass -class AddressAndSigner: - address: str - signer: TransactionSigner + +@dataclass(frozen=True, kw_only=True) +class _CommonEnsureFundedParams: + transaction_id: str + amount_funded: AlgoAmount + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedResponse(SendSingleTransactionResult, _CommonEnsureFundedParams): + pass + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedFromTestnetDispenserApiResponse(_CommonEnsureFundedParams): + pass class AccountManager: @@ -26,14 +51,15 @@ def __init__(self, client_manager: ClientManager): :param client_manager: The ClientManager client to use for algod and kmd clients """ self._client_manager = client_manager - self._accounts = dict[str, TransactionSigner]() + self._kmd_account_manager = KmdAccountManager(client_manager) + self._accounts = dict[str, Account]() self._default_signer: TransactionSigner | None = None def set_default_signer(self, signer: TransactionSigner) -> Self: """ Sets the default signer to use if no other signer is specified. - :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount` + :param signer: The signer to use :return: The `AccountManager` so method calls can be chained """ self._default_signer = signer @@ -47,10 +73,17 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: :param signer: The signer to sign transactions with for the given sender :return: The AccountCreator instance for method chaining """ - self._accounts[sender] = signer + if isinstance(signer, AccountTransactionSigner): + self._accounts[sender] = Account(private_key=signer.private_key) return self - def get_signer(self, sender: str) -> TransactionSigner: + def get_account(self, sender: str) -> Account: + account = self._accounts.get(sender) + if not account: + raise ValueError(f"No account found for address {sender}") + return account + + def get_signer(self, sender: str | Account) -> TransactionSigner: """ Returns the `TransactionSigner` for the given sender address. @@ -59,82 +92,400 @@ def get_signer(self, sender: str) -> TransactionSigner: :param sender: The sender address :return: The `TransactionSigner` or throws an error if not found """ - signer = self._accounts.get(sender, None) or self._default_signer + account = self._accounts.get(self._get_address(sender)) + signer = account.signer if account else self._default_signer if not signer: raise ValueError(f"No signer found for address {sender}") return signer - def get_information(self, sender: str) -> dict[str, Any]: + def get_information(self, sender: str | Account) -> dict[str, Any]: """ Returns the given sender account's current status, balance and spendable amounts. - Example: - address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" - account_info = account.get_information(address) - - `Response data schema details `_ - :param sender: The address of the sender/account to look up :return: The account information """ - info = self._client_manager.algod.account_info(sender) + info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) return info - def get_asset_information(self, sender: str, asset_id: int) -> dict[str, Any]: - info = self._client_manager.algod.account_asset_info(sender, asset_id) - assert isinstance(info, dict) - return info + def from_mnemonic(self, mnemonic: str) -> Account: + private_key = to_private_key(mnemonic) + account = Account(private_key=private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) + return account + + def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Account: + account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") + + if account_mnemonic: + private_key = mnemonic.to_private_key(account_mnemonic) + account = Account(private_key=private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) + return account + + if self._client_manager.is_local_net(): + kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with) + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account + + raise ValueError(f"Missing environment variable {name.upper()}_MNEMONIC when looking for account {name}") def from_kmd( - self, - name: str, - predicate: Callable[[dict[str, Any]], bool] | None = None, - ) -> AddressAndSigner: - account = get_kmd_wallet_account( - name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd - ) - if not account: + self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None + ) -> Account: + kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender) + if not kmd_account: raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}") - self.set_signer(account.address, account.signer) - return AddressAndSigner(address=account.address, signer=account.signer) + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account - def random(self) -> AddressAndSigner: + def rekeyed(self, sender: Account | str, account: Account) -> Account: + sender_address = sender.address if isinstance(sender, Account) else sender + self._accounts[sender_address] = account + return Account(address=sender_address, private_key=account.private_key) + + def rekey_account( # noqa: PLR0913 + self, + account: str | Account, + rekey_to: str | Account, + *, + # Common transaction parameters + signer: TransactionSigner | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + suppress_log: bool | None = None, + ) -> SendAtomicTransactionComposerResults: + """Rekey an account to a new address. + + Args: + account: The account to rekey + rekey_to: The address or account to rekey to + signer: Optional transaction signer + note: Optional transaction note + lease: Optional transaction lease + static_fee: Optional static fee + extra_fee: Optional extra fee + max_fee: Optional max fee + validity_window: Optional validity window + first_valid_round: Optional first valid round + last_valid_round: Optional last valid round + suppress_log: Optional flag to suppress logging + + Returns: + The transaction result """ - Tracks and returns a new, random Algorand account with secret key loaded. + sender_address = self._get_address(account) + rekey_address = self._get_address(rekey_to) + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=sender_address, + receiver=sender_address, + amount=AlgoAmount.from_micro_algo(0), + rekey_to=rekey_address, + signer=signer, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + suppress_log=suppress_log, + ) + ) + .send() + ) + + # If rekey_to is a signing account, set it as the signer for this account + if isinstance(rekey_to, Account): + self.rekeyed(account, rekey_to) + + if not suppress_log: + logger.info(f"Rekeyed {account} to {rekey_to} via transaction {result.tx_ids[-1]}") + + return result - Example: - account = account.random() + def random(self) -> Account: + """ + Tracks and returns a new, random Algorand account. :return: The account """ - (sk, addr) = generate_account() - signer = AccountTransactionSigner(sk) + account = Account.new_account() + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=account.private_key)) + return account + + def localnet_dispenser(self) -> Account: + kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account + + def dispenser_from_environment(self) -> Account: + name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") + if name: + return self.from_environment(DISPENSER_ACCOUNT_NAME) + return self.localnet_dispenser() + + def ensure_funded( # noqa: PLR0913 + self, + account_to_fund: str | Account, + dispenser_account: str | Account, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + # Sender params + max_rounds_to_wait: int | None = None, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + # Common txn params + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + ) -> EnsureFundedResponse | None: + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self._get_address(dispenser_account) + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=dispenser_account, + receiver=account_to_fund, + amount=amount_funded, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + ) + .send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + ) + ) - self.set_signer(addr, signer) + return EnsureFundedResponse( + returns=result.returns, + transactions=result.transactions, + confirmations=result.confirmations, + tx_ids=result.tx_ids, + group_id=result.group_id, + transaction_id=result.tx_ids[0], + confirmation=result.confirmations[0], + transaction=result.transactions[0], + amount_funded=amount_funded, + ) + + def ensure_funded_from_environment( # noqa: PLR0913 + self, + account_to_fund: str | Account, + min_spending_balance: AlgoAmount, + *, # Force remaining params to be keyword-only + min_funding_increment: AlgoAmount | None = None, + # SendParams + max_rounds_to_wait: int | None = None, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + # Common transaction params (omitting sender) + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + ) -> EnsureFundedResponse | None: + """Ensure an account is funded from a dispenser account configured in environment. + + Args: + account_to_fund: Address of account to fund + min_spending_balance: Minimum spending balance to ensure + min_funding_increment: Optional minimum funding increment + max_rounds_to_wait: Optional maximum rounds to wait for transaction + suppress_log: Optional flag to suppress logging + populate_app_call_resources: Optional flag to populate app call resources + signer: Optional transaction signer + rekey_to: Optional rekey address + note: Optional transaction note + lease: Optional transaction lease + static_fee: Optional static fee + extra_fee: Optional extra fee + max_fee: Optional maximum fee + validity_window: Optional validity window + first_valid_round: Optional first valid round + last_valid_round: Optional last valid round + + Returns: + EnsureFundedResponse if funding was needed, None otherwise + """ + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self.dispenser_from_environment() + + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=dispenser_account.address, + receiver=account_to_fund, + amount=amount_funded, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + ) + .send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + ) + ) - return AddressAndSigner(address=addr, signer=signer) + return EnsureFundedResponse( + returns=result.returns, + transactions=result.transactions, + confirmations=result.confirmations, + tx_ids=result.tx_ids, + group_id=result.group_id, + transaction_id=result.tx_ids[0], + confirmation=result.confirmations[0], + transaction=result.transactions[0], + amount_funded=amount_funded, + ) - def dispenser(self) -> AddressAndSigner: + def ensure_funded_from_testnet_dispenser_api( + self, + account_to_fund: str | Account, + dispenser_client: TestNetDispenserApiClient, + min_spending_balance: AlgoAmount, + *, # Force remaining params to be keyword-only + min_funding_increment: AlgoAmount | None = None, + ) -> EnsureFundedFromTestnetDispenserApiResponse | None: + """Ensure an account is funded using the TestNet Dispenser API. + + Args: + account_to_fund: Address of account to fund + dispenser_client: Instance of TestNetDispenserApiClient to use for funding + min_spending_balance: Minimum spending balance to ensure + min_funding_increment: Optional minimum funding increment + + Returns: + EnsureFundedResponse if funding was needed, None otherwise + + Raises: + ValueError: If attempting to fund on non-TestNet network """ - Returns an account (with private key loaded) that can act as a dispenser. + account_to_fund = self._get_address(account_to_fund) - Example: - account = account.dispenser() + if not self._client_manager.is_test_net(): + raise ValueError("Attempt to fund using TestNet dispenser API on non TestNet network.") - If running on LocalNet then it will return the default dispenser account automatically, - otherwise it will load the account mnemonic stored in os.environ['DISPENSER_MNEMONIC']. + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) - :return: The account - """ - acct = get_dispenser_account(self._client_manager.algod) + if not amount_funded: + return None - self.set_signer(acct.address, acct.signer) + result = dispenser_client.fund( + address=account_to_fund, + amount=amount_funded.micro_algo, + asset_id=DispenserAssetName.ALGO, + ) + + return EnsureFundedFromTestnetDispenserApiResponse( + transaction_id=result.tx_id, + amount_funded=AlgoAmount.from_micro_algo(result.amount), + ) + + def _get_address(self, sender: str | Account) -> str: + return sender.address if isinstance(sender, Account) else sender + + def _get_composer(self, get_suggested_params: Callable[[], SuggestedParams] | None = None) -> TransactionComposer: + if get_suggested_params is None: + + def _get_suggested_params() -> SuggestedParams: + return self._client_manager.algod.suggested_params() - return AddressAndSigner(address=acct.address, signer=acct.signer) + get_suggested_params = _get_suggested_params + + return TransactionComposer( + algod=self._client_manager.algod, get_signer=self.get_signer, get_suggested_params=get_suggested_params + ) + + def _calculate_fund_amount( + self, + min_spending_balance: int, + current_spending_balance: int, + min_funding_increment: int, + ) -> int | None: + if min_spending_balance > current_spending_balance: + min_fund_amount = min_spending_balance - current_spending_balance + return max(min_fund_amount, min_funding_increment) + return None + + def _get_ensure_funded_amount( + self, + sender: str, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + ) -> AlgoAmount | None: + account_info = self.get_information(sender) + current_spending_balance = account_info["amount"] - account_info["min-balance"] + + min_increment = min_funding_increment.micro_algo if min_funding_increment else 0 + amount_funded = self._calculate_fund_amount( + min_spending_balance.micro_algo, current_spending_balance, min_increment + ) - def localnet_dispenser(self) -> AddressAndSigner: - acct = get_localnet_default_account(self._client_manager.algod) - self.set_signer(acct.address, acct.signer) - return AddressAndSigner(address=acct.address, signer=acct.signer) + return AlgoAmount.from_micro_algo(amount_funded) if amount_funded is not None else None diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py new file mode 100644 index 00000000..6ac08c2d --- /dev/null +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -0,0 +1,190 @@ +from collections.abc import Callable +from typing import Any, cast + +from algosdk.kmd import KMDClient + +from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.config import config +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer + +logger = config.logger + + +class KmdAccount(Account): + """Account retrieved from KMD with signing capabilities, extending base Account""" + + def __init__(self, private_key: str, address: str | None = None) -> None: + """Initialize KMD account with private key and optional address override + + Args: + private_key: Base64 encoded private key + address: Optional address override (for rekeyed accounts) + """ + super().__init__(private_key=private_key, address=address or "") + + +class KmdAccountManager: + """Provides abstractions over KMD that makes it easier to get and manage accounts.""" + + _kmd: KMDClient | None + + def __init__(self, client_manager: ClientManager) -> None: + """Create a new KMD manager. + + Args: + client_manager: ClientManager to use for account management + """ + self._client_manager = client_manager + try: + self._kmd = client_manager.kmd + except ValueError: + self._kmd = None + + def kmd(self) -> KMDClient: + """Get the KMD client, initializing it if needed. + + Returns: + KMDClient: The initialized KMD client + + Raises: + Exception: If KMD is not configured + """ + if self._kmd is None: + if self._client_manager.is_local_net(): + kmd_config = ClientManager.get_config_from_environment_or_localnet() + self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config) + return self._kmd + raise Exception("Attempt to use KMD client with no KMD configured") + return self._kmd + + def get_wallet_account( + self, + wallet_name: str, + predicate: Callable[[dict[str, Any]], bool] | None = None, + sender: str | None = None, + ) -> KmdAccount | None: + """Returns an Algorand signing account with private key loaded from the given KMD wallet. + + Args: + wallet_name: The name of the wallet to retrieve an account from + predicate: Optional filter to use to find the account (otherwise returns a random account from the wallet) + sender: Optional sender address to use this signer for (aka a rekeyed account) + + Returns: + Optional[KmdAccount]: The signing account or None if no matching wallet or account was found + + Example: + ```python + # Get default funded account in a LocalNet + default_dispenser = kmd_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 + ) + ``` + """ + kmd_client = self.kmd() + wallets = kmd_client.list_wallets() + wallet = next((w for w in wallets if w["name"] == wallet_name), None) + if not wallet: + return None + + wallet_id = wallet["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + addresses = kmd_client.list_keys(wallet_handle) + + matched_address = None + if predicate: + for address in addresses: + account_info = self._client_manager.algod.account_info(address) + if predicate(cast(dict[str, Any], account_info)): + matched_address = address + break + else: + matched_address = next(iter(addresses), None) + + if not matched_address: + return None + + private_key = kmd_client.export_key(wallet_handle, "", matched_address) + return KmdAccount(private_key=private_key, address=sender) + + def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount: + """Gets or creates a funded account in a KMD wallet of the given name. + + This is useful to get idempotent accounts from LocalNet without having to specify the private key + (which will change when resetting the LocalNet). + + Args: + name: The name of the wallet to retrieve / create + fund_with: The number of Algos to fund the account with when created (default: 1000) + + Returns: + KmdAccount: An Algorand account with private key loaded + + Example: + ```python + # Idempotently get (if exists) or create (if doesn't exist) an account by name using KMD + # if creating it then fund it with 2 ALGO from the default dispenser account + new_account = kmd_manager.get_or_create_wallet_account("account1", 2) + # This will return the same account as above since the name matches + existing_account = kmd_manager.get_or_create_wallet_account("account1") + ``` + """ + existing = self.get_wallet_account(name) + if existing: + return existing + + kmd_client = self.kmd() + wallet_id = kmd_client.create_wallet(name, "")["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + kmd_client.generate_key(wallet_handle) + + account = self.get_wallet_account(name) + assert account is not None + + logger.info( + f"LocalNet account '{name}' doesn't yet exist; created account {account.address} " + f"with keys stored in KMD and funding with {fund_with} ALGO" + ) + + dispenser = self.get_localnet_dispenser_account() + TransactionComposer( + algod=self._client_manager.algod, + get_signer=lambda _: dispenser.signer, + get_suggested_params=self._client_manager.algod.suggested_params, + ).add_payment( + PaymentParams( + sender=dispenser.address, + receiver=account.address, + amount=fund_with or AlgoAmount.from_algo(1000), + ) + ).send() + return account + + def get_localnet_dispenser_account(self) -> KmdAccount: + """Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + + Returns: + KmdAccount: The default LocalNet dispenser account + + Raises: + Exception: If not running against LocalNet or dispenser account not found + + Example: + ```python + dispenser = kmd_manager.get_localnet_dispenser_account() + ``` + """ + if not self._client_manager.is_local_net(): + raise Exception("Can't get LocalNet dispenser account from non LocalNet network") + + dispenser = self.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000, # noqa: PLR2004 + ) + if not dispenser: + raise Exception("Error retrieving LocalNet dispenser account; couldn't find the default account in KMD") + + return dispenser diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py new file mode 100644 index 00000000..4c71d171 --- /dev/null +++ b/src/algokit_utils/applications/app_client.py @@ -0,0 +1,1396 @@ +from __future__ import annotations + +import base64 +import copy +import json +import os +from dataclasses import dataclass, fields +from typing import TYPE_CHECKING, Any, Protocol, TypeVar + +import algosdk +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_manager import BoxABIValue, BoxName, BoxValue +from algokit_utils.applications.utils import ( + get_abi_decoded_value, + get_abi_encoded_value, + get_abi_tuple_from_abi_struct, + get_arc56_method, +) +from algokit_utils.errors.logic_error import LogicError, parse_logic_error +from algokit_utils.models.application import ( + AppState, + Arc56Contract, + CompiledTeal, + ProgramSourceInfo, + SourceInfoDetail, + StorageKey, + StorageMap, +) +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, + AppDeleteMethodCall, + AppMethodCallTransactionArgument, + AppUpdateMethodCall, + AppUpdateParams, + BuiltTransactions, + PaymentParams, +) +from algokit_utils.transactions.transaction_sender import SendAppTransactionResult, SendSingleTransactionResult + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.atomic_transaction_composer import TransactionSigner + + from algokit_utils.applications.app_manager import ( + AppManager, + BoxIdentifier, + BoxReference, + TealTemplateParams, + ) + from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue + from algokit_utils.models.amount import AlgoAmount + from algokit_utils.protocols.application import AlgorandClientProtocol + from algokit_utils.transactions.transaction_composer import TransactionComposer + +# TEAL opcodes for constant blocks +BYTE_CBLOCK = 38 # bytecblock opcode +INT_CBLOCK = 32 # intcblock opcode + +T = TypeVar("T") # For generic return type in _handle_call_errors + + +def get_constant_block_offset(program: bytes) -> int: # noqa: C901 + """Calculate the offset after constant blocks in TEAL program. + + Args: + program: The compiled TEAL program bytes + + Returns: + The maximum offset after bytecblock/intcblock operations + """ + bytes_list = list(program) + program_size = len(bytes_list) + + # Remove version byte + bytes_list.pop(0) + + # Track offsets + bytecblock_offset: int | None = None + intcblock_offset: int | None = None + + while bytes_list: + # Get current byte + byte = bytes_list.pop(0) + + # Check if byte is a constant block opcode + if byte in (BYTE_CBLOCK, INT_CBLOCK): + is_bytecblock = byte == BYTE_CBLOCK + + # Get number of values in constant block + if not bytes_list: + break + values_remaining = bytes_list.pop(0) + + # Process each value in the block + for _ in range(values_remaining): + if is_bytecblock: + # For bytecblock, next byte is length of element + if not bytes_list: + break + length = bytes_list.pop(0) + # Remove the bytes for this element + bytes_list = bytes_list[length:] + else: + # For intcblock, read until we find end of uvarint (MSB not set) + while bytes_list: + byte = bytes_list.pop(0) + if not (byte & 0x80): # Check if MSB is not set + break + + # Update appropriate offset + if is_bytecblock: + bytecblock_offset = program_size - len(bytes_list) - 1 + else: + intcblock_offset = program_size - len(bytes_list) - 1 + + # If next byte isn't a constant block opcode, we're done + if not bytes_list or bytes_list[0] not in (BYTE_CBLOCK, INT_CBLOCK): + break + + # Return maximum offset + return max(bytecblock_offset or 0, intcblock_offset or 0) + + +@dataclass(kw_only=True, frozen=True) +class AppClientCompilationParams: + deploy_time_params: TealTemplateParams | None = None + updatable: bool | None = None + deletable: bool | None = None + + +@dataclass(kw_only=True, frozen=True) +class ExposedLogicErrorDetails: + is_clear_state_program: bool = False + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + program: bytes | None = None + approval_source_info: ProgramSourceInfo | None = None + clear_source_info: ProgramSourceInfo | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientParams: + """Full parameters for creating an app client""" + + app_spec: ( + Arc56Contract | ApplicationSpecification | str + ) # Using string quotes since these types may be defined elsewhere + algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere + app_id: int + app_name: str | None = None + default_sender: str | bytes | None = None # Address can be string or bytes + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientCompilationResult: + approval_program: bytes + clear_state_program: bytes + compiled_approval: CompiledTeal | None = None + compiled_clear: CompiledTeal | None = None + + +@dataclass(kw_only=True, frozen=True) +class CommonTxnParams: + sender: str + signer: TransactionSigner | None = None + rekey_to: str | None = None + note: bytes | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + + +@dataclass(kw_only=True) +class FundAppAccountParams: + sender: str | None = None + signer: TransactionSigner | None = None + rekey_to: str | None = None + note: bytes | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + amount: AlgoAmount + close_remainder_to: str | None = None + max_rounds_to_wait: int | None = None + suppress_log: bool | None = None + populate_app_call_resources: bool | None = None + on_complete: algosdk.transaction.OnComplete | None = None + + +@dataclass(kw_only=True) +class AppClientCallParams: + method: str | None = None # If calling ABI method, name or signature + args: list | None = None # Arguments to pass to the method + boxes: list | None = None # Box references to load + accounts: list[str] | None = None # Account addresses to load + apps: list[int] | None = None # App IDs to load + assets: list[int] | None = None # Asset IDs to load + lease: (str | bytes) | None = None # Optional lease + sender: str | None = None # Optional sender account + note: (bytes | dict | str) | None = None # Transaction note + send_params: dict | None = None # Parameters to control transaction sending + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallParams: + method: str + args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + extra_fee: AlgoAmount | None = None + first_valid_round: int | None = None + lease: bytes | None = None + max_fee: AlgoAmount | None = None + note: bytes | None = None + rekey_to: str | None = None + sender: str | None = None + signer: TransactionSigner | None = None + static_fee: AlgoAmount | None = None + validity_window: int | None = None + last_valid_round: int | None = None + on_complete: algosdk.transaction.OnComplete | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithCompilationParams(AppClientMethodCallParams, AppClientCompilationParams): + """Combined parameters for method calls with compilation""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithSendParams(AppClientMethodCallParams, SendParams): + """Combined parameters for method calls with send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithCompilationAndSendParams( + AppClientMethodCallParams, AppClientCompilationParams, SendParams +): + """Combined parameters for method calls with compilation and send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallParams: + signer: TransactionSigner | None = None + rekey_to: str | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + sender: str | None = None + note: bytes | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + + +@dataclass(kw_only=True, frozen=True) +class CallOnComplete: + on_complete: algosdk.transaction.OnComplete + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCompilationParams(AppClientBareCallParams, AppClientCompilationParams): + """Combined parameters for bare calls with compilation""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithSendParams(AppClientBareCallParams, SendParams): + """Combined parameters for bare calls with send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, AppClientCompilationParams, SendParams): + """Combined parameters for bare calls with compilation and send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, CallOnComplete): + """Combined parameters for bare calls with an OnComplete value""" + + +@dataclass(kw_only=True, frozen=True) +class ResolveAppClientByNetwork: + app_spec: Arc56Contract | ApplicationSpecification | str + algorand: AlgorandClientProtocol + app_name: str | None = None + default_sender: str | bytes | None = None + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppSourceMaps: + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +class _AppClientStateMethodsProtocol(Protocol): + def get_all(self) -> dict[str, Any]: ... + + def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: ... + + def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: ... # noqa: ANN401 + + def get_map(self, map_name: str) -> dict[str, ABIValue]: ... + + +class _AppClientStateMethods(_AppClientStateMethodsProtocol): + def __init__( + self, + *, + get_all: Callable[[], dict[str, Any]], + get_value: Callable[[str, dict[str, AppState] | None], ABIValue | None], + get_map_value: Callable[[str, bytes | Any, dict[str, AppState] | None], Any], + get_map: Callable[[str], dict[str, ABIValue]], + ) -> None: + self._get_all = get_all + self._get_value = get_value + self._get_map_value = get_map_value + self._get_map = get_map + + def get_all(self) -> dict[str, Any]: + return self._get_all() + + def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: + return self._get_value(name, app_state) + + def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 + return self._get_map_value(map_name, key, app_state) + + def get_map(self, map_name: str) -> dict[str, ABIValue]: + return self._get_map(map_name) + + +class _AppClientStateAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def local_state(self, address: str) -> _AppClientStateMethodsProtocol: + """Methods to access local state for the current app for a given address""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address), + key_getter=lambda: self._app_spec.state.keys.get("local", {}), + map_getter=lambda: self._app_spec.state.maps.get("local", {}), + ) + + @property + def global_state(self) -> _AppClientStateMethodsProtocol: + """Methods to access global state for the current app""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_global_state(self._app_id), + key_getter=lambda: self._app_spec.state.keys.get("global", {}), + map_getter=lambda: self._app_spec.state.maps.get("global", {}), + ) + + # @property + # def box(self) -> AppClientStateMethods: + # """Methods to access box storage for the current app""" + # return self._get_state_methods( + # state_getter=lambda: self._algorand.app.get_box_state(self._app_id), + # key_getter=lambda: self._app_spec.state.keys.get("box", {}), + # map_getter=lambda: self._app_spec.state.maps.get("box", {}), + # ) + + def _get_state_methods( # noqa: C901 + self, + state_getter: Callable[[], dict[str, AppState]], + key_getter: Callable[[], dict[str, StorageKey]], + map_getter: Callable[[], dict[str, StorageMap]], + ) -> _AppClientStateMethodsProtocol: + def get_all() -> dict[str, Any]: + state = state_getter() + keys = key_getter() + return {key: get_value(key, state) for key in keys} + + def get_value(name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: + state = app_state or state_getter() + key_info = key_getter()[name] + value = next((s for s in state.values() if s.key_base64 == key_info.key), None) + + if value and value.value_raw: + return get_abi_decoded_value(value.value_raw, key_info.value_type, self._app_spec.structs) + + return None + + def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 + state = app_state or state_getter() + metadata = map_getter()[map_name] + + prefix = bytes(metadata.prefix or "", "base64") + encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) + full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") + value = next((s for s in state.values() if s.key_base64 == full_key), None) + if value and value.value_raw: + return get_abi_decoded_value(value.value_raw, metadata.value_type, self._app_spec.structs) + return None + + def get_map(map_name: str) -> dict[str, ABIValue]: + state = state_getter() + metadata = map_getter()[map_name] + + prefix = metadata.prefix or "" + + prefixed_state = {k: v for k, v in state.items() if k.startswith(prefix)} + + decoded_map = {} + + for key_encoded, value in prefixed_state.items(): + key_bytes = key_encoded[len(prefix) :] + try: + decoded_key = get_abi_decoded_value(key_bytes, metadata.key_type, self._app_spec.structs) + except Exception as e: + raise ValueError(f"Failed to decode key {key_encoded}") from e + + try: + if value and value.value_raw: + decoded_value = get_abi_decoded_value( + value.value_raw, metadata.value_type, self._app_spec.structs + ) + else: + decoded_value = get_abi_decoded_value(value.value, metadata.value_type, self._app_spec.structs) + except Exception as e: + raise ValueError(f"Failed to decode value {value}") from e + + decoded_map[str(decoded_key)] = decoded_value + + return decoded_map + + return _AppClientStateMethods( + get_all=get_all, + get_value=get_value, + get_map_value=get_map_value, + get_map=get_map, + ) + + def get_local_state(self, address: str) -> dict[str, AppState]: + return self._algorand.app.get_local_state(self._app_id, address) + + def get_global_state(self) -> dict[str, AppState]: + return self._algorand.app.get_global_state(self._app_id) + + +class _AppClientBareParamsAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def _get_bare_params( + self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete + ) -> dict[str, Any]: + """Get bare parameters for application calls. + + Args: + params: The parameters to process + on_complete: The OnComplete value for the transaction + + Returns: + The processed parameters with defaults filled in + """ + params = params or {} + sender = self._client._get_sender(params.get("sender")) + return { + **params, + "app_id": self._app_id, + "sender": sender, + "signer": self._client._get_signer(params.get("sender"), params.get("signer")), + "on_complete": on_complete, + } + + def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> AppUpdateParams: + call_params: AppUpdateParams = AppUpdateParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.UpdateApplicationOC) + ) + return call_params + + def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.OptInOC)) + return call_params + + def delete(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__, OnComplete.DeleteApplicationOC) + ) + return call_params + + def clear_state(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.ClearStateOC)) + return call_params + + def close_out(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.CloseOutOC)) + return call_params + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.NoOpOC)) + return call_params + + +class _AppClientMethodCallParamsAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_params_accessor = _AppClientBareParamsAccessor(client) + + @property + def bare(self) -> _AppClientBareParamsAccessor: + return self._bare_params_accessor + + def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: + def random_note() -> bytes: + return base64.b64encode(os.urandom(16)) + + return PaymentParams( + sender=self._client._get_sender(params.sender), + signer=self._client._get_signer(params.sender, params.signer), + receiver=self._client.app_address, + amount=params.amount, + rekey_to=params.rekey_to, + note=params.note or random_note(), + lease=params.lease, + static_fee=params.static_fee, + extra_fee=params.extra_fee, + max_fee=params.max_fee, + validity_window=params.validity_window, + first_valid_round=params.first_valid_round, + last_valid_round=params.last_valid_round, + close_remainder_to=params.close_remainder_to, + ) + + def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) + return AppCallMethodCall(**input_params) + + def call(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) + return AppCallMethodCall(**input_params) + + def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + input_params = self._get_abi_params( + params.__dict__, on_complete=algosdk.transaction.OnComplete.DeleteApplicationOC + ) + return AppDeleteMethodCall(**input_params) + + def update( + self, params: AppClientMethodCallParams | AppClientMethodCallWithCompilationAndSendParams + ) -> AppUpdateMethodCall: + compile_params = ( + self._client.compile( + app_spec=self._client.app_spec, + app_manager=self._algorand.app, + deploy_time_params=params.deploy_time_params, + updatable=params.updatable, + deletable=params.deletable, + ).__dict__ + if isinstance(params, AppClientMethodCallWithCompilationAndSendParams) + else {} + ) + + input_params = { + **self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.UpdateApplicationOC), + **compile_params, + } + # Filter input_params to include only fields valid for AppUpdateMethodCall + app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCall)} + filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields} + return AppUpdateMethodCall(**filtered_input_params) + + def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.CloseOutOC) + return AppCallMethodCall(**input_params) + + def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + input_params = copy.deepcopy(params) + + input_params["app_id"] = self._app_id + input_params["on_complete"] = on_complete + + input_params["sender"] = self._client._get_sender(params["sender"]) + input_params["signer"] = self._client._get_signer(params["sender"], params["signer"]) + + if params.get("method"): + input_params["method"] = get_arc56_method(params["method"], self._app_spec) + if params.get("args"): + input_params["args"] = self._client._get_abi_args_with_default_values( + method_name_or_signature=params["method"], + args=params["args"], + sender=self._client._get_sender(input_params["sender"]), + ) + + return input_params + + +class _AppClientBareCreateTransactionMethods: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + + def update(self, params: AppClientBareCallWithCompilationAndSendParams) -> Transaction: + return self._algorand.create_transaction.app_update(self._client.params.bare.update(params)) + + def opt_in(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.opt_in(params)) + + def delete(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.delete(params)) + + def clear_state(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.clear_state(params)) + + def close_out(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.close_out(params)) + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.call(params)) + + +class _AppClientMethodCallTransactionCreator: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_create_transaction_methods = _AppClientBareCreateTransactionMethods(client) + + @property + def bare(self) -> _AppClientBareCreateTransactionMethods: + return self._bare_create_transaction_methods + + def fund_app_account(self, params: FundAppAccountParams) -> Transaction: + return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params)) + + def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params)) + + def update(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params)) + + def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params)) + + def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params)) + + def call(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params)) + + +class _AppClientBareSendAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def update( + self, + params: AppClientBareCallWithCompilationAndSendParams, + ) -> SendAppTransactionResult: + """Send an application update transaction. + + Args: + params: The parameters for the update call + compilation: Optional compilation parameters + max_rounds_to_wait: The maximum number of rounds to wait for confirmation + suppress_log: Whether to suppress log output + populate_app_call_resources: Whether to populate app call resources + + Returns: + The result of sending the transaction + """ + compiled = self._client.compile_and_persist_sourcemaps( + params.deploy_time_params, params.updatable, params.deletable + ) + bare_params = self._client.params.bare.update(params) + bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) + bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) + call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params)) + return SendAppTransactionResult(**{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}) + + def opt_in(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.opt_in(params)) + ) + + def delete(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.delete(params)) + ) + + def clear_state(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.clear_state(params)) + ) + + def close_out(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.close_out(params)) + ) + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.call(params)) + ) + + +class _AppClientSendAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_send_accessor = _AppClientBareSendAccessor(client) + + @property + def bare(self) -> _AppClientBareSendAccessor: + return self._bare_send_accessor + + def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.payment(self._client.params.fund_app_account(params)) + ) + + def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)) + ) + + def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)) + ) + + def update(self, params: AppClientMethodCallWithCompilationAndSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)) + ) + + def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)) + ) + + def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + is_read_only_call = params.on_complete == algosdk.transaction.OnComplete.NoOpOC or ( + not params.on_complete and get_arc56_method(params.method, self._app_spec).method.readonly + ) + + if is_read_only_call: + method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( + self._client.params.call(params) + ) + + simulate_response = self._client._handle_call_errors( + lambda: method_call_to_simulate.simulate( + allow_unnamed_resources=params.populate_app_call_resources or True, + skip_signatures=True, + allow_more_logs=True, + allow_empty_signatures=True, + extra_opcode_budget=None, + exec_trace_config=None, + round=None, + fix_signers=None, # TODO: double check on whether algosdk py even has this param + ) + ) + + return SendAppTransactionResult( + tx_ids=simulate_response.tx_ids, + transactions=simulate_response.transactions, + transaction=simulate_response.transactions[-1], + confirmation=simulate_response.confirmations[-1] if simulate_response.confirmations else b"", + confirmations=simulate_response.confirmations, + group_id=simulate_response.group_id or "", + returns=simulate_response.returns, + return_value=simulate_response.returns[-1], + ) + + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call_method_call(self._client.params.call(params)) + ) + + +class AppClient: + def __init__(self, params: AppClientParams) -> None: + self._app_id = params.app_id + self._app_spec = self.normalise_app_spec(params.app_spec) + self._algorand = params.algorand + self._app_address = algosdk.logic.get_application_address(self._app_id) + self._app_name = params.app_name or self._app_spec.name + self._default_sender = params.default_sender + self._default_signer = params.default_signer + self._approval_source_map = params.approval_source_map + self._clear_source_map = params.clear_source_map + self._state_accessor = _AppClientStateAccessor(self) + self._params_accessor = _AppClientMethodCallParamsAccessor(self) + self._send_accessor = _AppClientSendAccessor(self) + self._create_transaction_accessor = _AppClientMethodCallTransactionCreator(self) + + @property + def app_id(self) -> int: + return self._app_id + + @property + def app_address(self) -> str: + return self._app_address + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def state(self) -> _AppClientStateAccessor: + return self._state_accessor + + @property + def params(self) -> _AppClientMethodCallParamsAccessor: + return self._params_accessor + + @property + def send(self) -> _AppClientSendAccessor: + return self._send_accessor + + @property + def create_transaction(self) -> _AppClientMethodCallTransactionCreator: + return self._create_transaction_accessor + + @staticmethod + def normalise_app_spec(app_spec: Arc56Contract | ApplicationSpecification | str) -> Arc56Contract: + if isinstance(app_spec, str): + spec = json.loads(app_spec) + if "hints" in spec: + spec = ApplicationSpecification.from_json(app_spec) + else: + spec = app_spec + + if isinstance(spec, Arc56Contract): + return spec + + elif isinstance(spec, ApplicationSpecification): + # Convert ARC-32 to ARC-56 + from algokit_utils.applications.utils import arc32_to_arc56 + + return arc32_to_arc56(spec) + elif isinstance(spec, dict): + # normalize field names to lowercase to python camel + return Arc56Contract.from_json(spec) + else: + raise ValueError("Invalid app spec format") + + @staticmethod + def from_network( + app_spec: Arc56Contract | ApplicationSpecification | str, + algorand: AlgorandClientProtocol, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + network = algorand.client.network() + app_spec = AppClient.normalise_app_spec(app_spec) + network_names = [network.genesis_hash] + + if network.is_local_net: + network_names.append("localnet") + if network.is_main_net: + network_names.append("mainnet") + if network.is_test_net: + network_names.append("testnet") + + available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else [] + network_index = next((i for i, n in enumerate(available_app_spec_networks) if n in network_names), None) + + if network_index is None: + raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec") + + app_id = app_spec.networks[available_app_spec_networks[network_index]]["app_id"] # type: ignore[index] + + return AppClient( + AppClientParams( + app_id=app_id, + app_spec=app_spec, + algorand=algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + + @staticmethod + def compile( + app_spec: Arc56Contract, + app_manager: AppManager, + deploy_time_params: TealTemplateParams | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + ) -> AppClientCompilationResult: + def is_base64(s: str) -> bool: + try: + return base64.b64encode(base64.b64decode(s)).decode() == s + except Exception: + return False + + if not app_spec.source: + if not app_spec.byte_code or not app_spec.byte_code.get("approval") or not app_spec.byte_code.get("clear"): + raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code") + + return AppClientCompilationResult( + approval_program=base64.b64decode(app_spec.byte_code.get("approval", "")), + clear_state_program=base64.b64decode(app_spec.byte_code.get("clear", "")), + ) + + approval_source = app_spec.source.get("approval", "") + approval_template: str = ( + base64.b64decode(approval_source).decode("utf-8") if is_base64(approval_source) else approval_source + ) + compiled_approval = app_manager.compile_teal_template( + approval_template, + template_params=deploy_time_params, + deployment_metadata=( + {"updatable": updatable or False, "deletable": deletable or False} + if updatable is not None or deletable is not None + else None + ), + ) + + clear_source = app_spec.source.get("clear", "") + clear_template: str = ( + base64.b64decode(clear_source).decode("utf-8") if is_base64(clear_source) else clear_source + ) + compiled_clear = app_manager.compile_teal_template( + clear_template, + template_params=deploy_time_params, + ) + + # TODO: Add invocation of persisting sourcemaps + return AppClientCompilationResult( + approval_program=compiled_approval.compiled_base64_to_bytes, + compiled_approval=compiled_approval, + clear_state_program=compiled_clear.compiled_base64_to_bytes, + compiled_clear=compiled_clear, + ) + + @staticmethod + def expose_logic_error_static( # noqa: C901 + e: Exception, app_spec: Arc56Contract, details: ExposedLogicErrorDetails + ) -> Exception: + """Takes an error that may include a logic error and re-exposes it with source info.""" + source_map = details.clear_source_map if details.is_clear_state_program else details.approval_source_map + + error_details = parse_logic_error(str(e)) + if not error_details: + return e + + # The PC value to find in the ARC56 SourceInfo + arc56_pc = error_details["pc"] + + program_source_info = ( + details.clear_source_info if details.is_clear_state_program else details.approval_source_info + ) + + # The offset to apply to the PC if using the cblocks pc offset method + cblocks_offset = 0 + + # If the program uses cblocks offset, then we need to adjust the PC accordingly + if program_source_info and program_source_info.pc_offset_method == "cblocks": + if not details.program: + raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset") + + cblocks_offset = get_constant_block_offset(details.program) + arc56_pc = error_details["pc"] - cblocks_offset + + # Find the source info for this PC and get the error message + source_info = None + if program_source_info and program_source_info.source_info: + source_info = next( + (s for s in program_source_info.source_info if isinstance(s, SourceInfoDetail) and arc56_pc in s.pc), + None, + ) + error_message = source_info.error_message if source_info else None + + # If we have the source we can display the TEAL in the error message + if hasattr(app_spec, "source"): + program_source = ( + (app_spec.source.get("clear") if details.is_clear_state_program else app_spec.source.get("approval")) + if app_spec.source + else None + ) + custom_get_line_for_pc = None + + def get_line_for_pc(input_pc: int) -> int | None: + if not program_source_info: + return None + teal = [line.teal for line in program_source_info.source_info if input_pc - cblocks_offset in line.pc] + return teal[0] if teal else None + + if not source_map: + custom_get_line_for_pc = get_line_for_pc + + if program_source: + e = LogicError( + logic_error_str=str(e), + program=program_source, + source_map=source_map, + transaction_id=error_details["transaction_id"], + message=error_details["message"], + pc=error_details["pc"], + logic_error=e, + get_line_for_pc=custom_get_line_for_pc, + traces=None, + ) + + if error_message: + import re + + app_id = re.search(r"(?<=app=)\d+", str(e)) + tx_id = re.search(r"(?<=transaction )\S+(?=:)", str(e)) + error = Exception( + f"Runtime error when executing {app_spec.name} " + f"(appId: {app_id.group() if app_id else ''}) in transaction " + f"{tx_id.group() if tx_id else ''}: {error_message}" + ) + error.__cause__ = e + return error + + return e + + # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' + def compile_and_persist_sourcemaps( + self, + deploy_time_params: TealTemplateParams | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + ) -> AppClientCompilationResult: + result = AppClient.compile(self._app_spec, self._algorand.app, deploy_time_params, updatable, deletable) + + if result.compiled_approval: + self._approval_source_map = result.compiled_approval.source_map + if result.compiled_clear: + self._clear_source_map = result.compiled_clear.source_map + + return result + + def clone( + self, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient( + AppClientParams( + app_id=self._app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ) + ) + + def export_source_maps(self) -> AppSourceMaps: + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "you need to call create, update, or deploy first" + ) + + return AppSourceMaps( + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + ) + + def import_source_maps(self, source_maps: AppSourceMaps) -> None: + if not source_maps.approval_source_map: + raise ValueError("Approval source map is required") + if not source_maps.clear_source_map: + raise ValueError("Clear source map is required") + + if not isinstance(source_maps.approval_source_map, dict | SourceMap): + raise ValueError( + "Approval source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`" + ) + if not isinstance(source_maps.clear_source_map, dict | SourceMap): + raise ValueError( + "Clear source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`" + ) + + self._approval_source_map = ( + SourceMap(source_map=source_maps.approval_source_map) + if isinstance(source_maps.approval_source_map, dict) + else source_maps.approval_source_map + ) + self._clear_source_map = ( + SourceMap(source_map=source_maps.clear_source_map) + if isinstance(source_maps.clear_source_map, dict) + else source_maps.clear_source_map + ) + + def get_local_state(self, address: str) -> dict[str, AppState]: + return self._state_accessor.get_local_state(address) + + def get_global_state(self) -> dict[str, AppState]: + return self._state_accessor.get_global_state() + + def get_box_names(self) -> list[BoxName]: + return self._algorand.app.get_box_names(self._app_id) + + def get_box_value(self, name: BoxIdentifier) -> bytes: + return self._algorand.app.get_box_value(self._app_id, name) + + def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type) + + def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]: + names = self.get_box_names() + if filter_func: + names = [name for name in names if filter_func(name)] + + # Get values for filtered names + values = self._algorand.app.get_box_values(self.app_id, [name.name_raw for name in names]) + + # Return list of BoxValue objects + return [BoxValue(name=name, value=values[i]) for i, name in enumerate(names)] + + def get_box_values_from_abi_type( + self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None + ) -> list[BoxABIValue]: + # Get box names and apply filter if provided + names = self.get_box_names() + if filter_func: + names = [name for name in names if filter_func(name)] + + # Get values for filtered names and decode them + values = self._algorand.app.get_box_values_from_abi_type( + self.app_id, [name.name_raw for name in names], abi_type + ) + + # Return list of BoxABIValue objects + return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] + + def new_group(self) -> TransactionComposer: + return self._algorand.new_group() + + def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + return self.send.fund_app_account(params) + + def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT001, FBT002 + """Takes an error that may include a logic error from a call to the current app and re-exposes the + error to include source code information via the source map and ARC-56 spec. + + Args: + e: The error to parse + is_clear_state_program: Whether the code was running the clear state program (defaults to approval program) + + Returns: + The new error, or if there was no logic error or source map then the wrapped error with source details + """ + + # Get source info based on program type + source_info = None + if hasattr(self._app_spec, "source_info") and self._app_spec.source_info: + source_info = ( + self._app_spec.source_info.get("clear") + if is_clear_state_program + else self._app_spec.source_info.get("approval") + ) + + pc_offset_method = source_info.pc_offset_method if source_info else None + + program: bytes | None = None + if pc_offset_method == "cblocks": + # TODO: Cache this if we deploy the app and it's not updateable + app_info = self._algorand.app.get_by_id(self.app_id) + program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program + + return AppClient.expose_logic_error_static( + e, + self._app_spec, + ExposedLogicErrorDetails( + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=program, + approval_source_info=( + self._app_spec.source_info.get("approval") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + clear_source_info=( + self._app_spec.source_info.get("clear") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + ), + ) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + """Make the given call and catch any errors, augmenting with debugging information before re-throwing.""" + try: + return call() + except Exception as e: + raise self.expose_logic_error(e=e) from None + + def _get_sender(self, sender: str | None) -> str: + if not sender and not self._default_sender: + raise Exception( + f"No sender provided and no default sender present in app client for call to app {self.app_name}" + ) + return sender or self._default_sender # type: ignore[return-value] + + def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: + return signer or self._default_signer if sender else None + + def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + """Get bare parameters for application calls. + + Args: + params: The parameters to process + on_complete: The OnComplete value for the transaction + + Returns: + The processed parameters with defaults filled in + """ + sender = self._get_sender(params.get("sender")) + return { + **params, + "app_id": self._app_id, + "sender": sender, + "signer": self._get_signer(params.get("sender"), params.get("signer")), + "on_complete": on_complete, + } + + def _get_abi_args_with_default_values( # noqa: C901, PLR0912 + self, + *, + method_name_or_signature: str, + args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, + sender: str, + ) -> list[Any]: + """Get ABI args with default values filled in. + + Args: + method_name_or_signature: Method name or ABI signature + args: Optional list of argument values + sender: Sender address + + Returns: + List of argument values with defaults filled in + + Raises: + ValueError: If required argument is missing or default value lookup fails + """ + method = get_arc56_method(method_name_or_signature, self._app_spec) + result = [] + + for i, method_arg in enumerate(method.arc56_args): + # Get provided arg value if any + arg_value = args[i] if args and i < len(args) else None + + if arg_value is not None: + # Convert struct to tuple if needed + if method_arg.struct and isinstance(arg_value, dict): + arg_value = get_abi_tuple_from_abi_struct( + arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs + ) + result.append(arg_value) + continue + + # Handle default value if arg not provided + default_value = method_arg.default_value + if default_value: + match default_value.source: + case "literal": + value_raw = base64.b64decode(default_value.data) + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) + + case "method": + # Get method return value + default_method = get_arc56_method(default_value.data, self._app_spec) + empty_args = [None] * len(default_method.args) + call_result = self._algorand.send.app_call_method_call( + AppCallMethodCall( + app_id=self._app_id, + method=algosdk.abi.Method.from_signature(default_value.data), + args=empty_args, + sender=sender, + ) + ) + + if not call_result.return_value: + raise ValueError("Default value method call did not return a value") + + if isinstance(call_result.return_value, dict): + # Convert struct return value to tuple + result.append( + get_abi_tuple_from_abi_struct( + call_result.return_value, + self._app_spec.structs[str(default_method.arc56_returns.type)], + self._app_spec.structs, + ) + ) + else: + result.append(call_result.return_value.return_value) + + case "local" | "global": + # Get state value + state = ( + self.get_global_state() + if default_value.source == "global" + else self.get_local_state(sender) + ) + value = next((s for s in state.values() if s.key_base64 == default_value.data), None) + if not value: + raise ValueError( + f"Key '{default_value.data}' not found in {default_value.source} " + f"storage for argument {method_arg.name or f'arg{i+1}'}" + ) + + if value.value_raw: + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(value.value_raw, value_type, self._app_spec.structs)) + else: + result.append(value.value) + + case "box": + # Get box value + box_name = base64.b64decode(default_value.data) + box_value = self._algorand.app.get_box_value(self._app_id, box_name) + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(box_value, value_type, self._app_spec.structs)) + + elif not algosdk.abi.is_abi_transaction_type(method_arg.type): + # Error if required non-txn arg missing + raise ValueError( + f"No value provided for required argument " + f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}" + ) + + return result + + def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + sender = self._get_sender(params.get("sender")) + method = get_arc56_method(params["method"], self._app_spec) + args = self._get_abi_args_with_default_values( + method_name_or_signature=params["method"], args=params.get("args"), sender=sender + ) + return { + **params, + "appId": self._app_id, + "sender": sender, + "signer": self._get_signer(params.get("sender"), params.get("signer")), + "method": method, + "onComplete": on_complete, + "args": args, + } diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py new file mode 100644 index 00000000..357d808c --- /dev/null +++ b/src/algokit_utils/applications/app_deployer.py @@ -0,0 +1,602 @@ +import base64 +import dataclasses +import json +from dataclasses import dataclass +from typing import Literal + +import algosdk +from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner +from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete +from algosdk.v2client.indexer import IndexerClient + +from algokit_utils._legacy_v2.deploy import ( + AppDeployMetaData, + AppLookup, + AppMetaData, + OnSchemaBreak, + OnUpdate, + OperationPerformed, +) +from algokit_utils.applications.app_manager import AppManager, BoxReference, TealTemplateParams +from algokit_utils.config import config +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppDeleteParams, + AppUpdateMethodCall, + AppUpdateParams, +) +from algokit_utils.transactions.transaction_sender import ( + AlgorandClientTransactionSender, +) + +APP_DEPLOY_NOTE_DAPP = "algokit_deployer" + +logger = config.logger + + +@dataclass(kw_only=True) +class DeployAppUpdateParams: + """Parameters for an update transaction in app deployment""" + + sender: str + on_complete: OnComplete = OnComplete.UpdateApplicationOC + signer: TransactionSigner | None = None + args: list[bytes] | None = None + note: bytes | None = None + lease: bytes | None = None + rekey_to: str | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(kw_only=True) +class DeployAppDeleteParams: + """Parameters for a delete transaction in app deployment""" + + sender: str + on_complete: OnComplete = OnComplete.DeleteApplicationOC + signer: TransactionSigner | None = None + note: bytes | None = None + lease: bytes | None = None + rekey_to: str | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(kw_only=True) +class AppDeployParams: + """Parameters for deploying an app""" + + metadata: AppDeployMetaData + deploy_time_params: TealTemplateParams | None = None + on_schema_break: Literal["replace", "fail", "append"] | OnSchemaBreak = OnSchemaBreak.Fail + on_update: Literal["update", "replace", "fail", "append"] | OnUpdate = OnUpdate.Fail + create_params: AppCreateParams | AppCreateMethodCall + update_params: DeployAppUpdateParams | AppUpdateMethodCall + delete_params: DeployAppDeleteParams | AppDeleteMethodCall + existing_deployments: AppLookup | None = None + ignore_cache: bool = False + max_fee: int | None = None + max_rounds_to_wait: int | None = None + suppress_log: bool = False + + +@dataclass(kw_only=True, frozen=True) +class ConfirmedTransactionResult: + transaction: TransactionWrapper + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppDeployResult: + operation_performed: OperationPerformed + + # Common fields from AppMetadata + name: str + version: str + created_round: int + updated_round: int + deleted: bool + created_metadata: dict + deletable: bool | None = None + updatable: bool | None = None + + app_id: int | None = None + app_address: str | None = None + transaction: TransactionWrapper | None = None + tx_id: str | None = None + transactions: list[TransactionWrapper] | None = None + tx_ids: list[str] | None = None + confirmation: algosdk.v2client.algod.AlgodResponseType | None = None + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + compiled_approval: dict | None = None + compiled_clear: dict | None = None + return_value: ABIResult | None = None + delete_return_value: ABIResult | None = None + delete_result: ConfirmedTransactionResult | None = None + + +class AppDeployer: + """Manages deployment and deployment metadata of applications""" + + def __init__( + self, + app_manager: AppManager, + transaction_sender: AlgorandClientTransactionSender, + indexer: IndexerClient | None = None, + ): + self._app_manager = app_manager + self._transaction_sender = transaction_sender + self._indexer = indexer + self._app_lookups: dict[str, AppLookup] = {} + + def _create_deploy_note(self, metadata: AppDeployMetaData) -> bytes: + note = { + "dapp_name": APP_DEPLOY_NOTE_DAPP, + "format": "j", + "data": metadata.__dict__, + } + return json.dumps(note).encode() + + def deploy(self, deployment: AppDeployParams) -> AppDeployResult: + # Create new instances with updated notes + logger.info( + f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " + f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and " + f"{len(deployment.create_params.clear_state_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}", + suppress_log=deployment.suppress_log, + ) + note = self._create_deploy_note(deployment.metadata) + create_params = dataclasses.replace(deployment.create_params, note=note) + update_params = dataclasses.replace(deployment.update_params, note=note) + + deployment = dataclasses.replace( + deployment, + create_params=create_params, + update_params=update_params, + ) + + # Validate inputs + if ( + deployment.existing_deployments + and deployment.existing_deployments.creator != deployment.create_params.sender + ): + raise ValueError( + f"Received invalid existingDeployments value for creator " + f"{deployment.existing_deployments.creator} when attempting to deploy " + f"for creator {deployment.create_params.sender}" + ) + + if not deployment.existing_deployments and not self._indexer: + raise ValueError( + "Didn't receive an indexer client when this AppManager was created, " + "but also didn't receive an existingDeployments cache - one of them must be provided" + ) + + # Compile code if needed + approval_program = deployment.create_params.approval_program + clear_program = deployment.create_params.clear_state_program + + if isinstance(approval_program, str): + compiled_approval = self._app_manager.compile_teal_template( + approval_program, + deployment.deploy_time_params, + deployment.metadata.__dict__, + ) + approval_program = compiled_approval.compiled_base64_to_bytes + + if isinstance(clear_program, str): + compiled_clear = self._app_manager.compile_teal_template( + clear_program, + deployment.deploy_time_params, + ) + clear_program = compiled_clear.compiled_base64_to_bytes + + # Get existing app metadata + apps = deployment.existing_deployments or self.get_creator_apps_by_name( + creator_address=deployment.create_params.sender, + ignore_cache=deployment.ignore_cache, + ) + + existing_app = apps.apps.get(deployment.metadata.name) + if not existing_app or existing_app.deleted: + return self._create_app( + deployment=deployment, + approval_program=approval_program, + clear_program=clear_program, + ) + + # Check for changes + existing_app_record = self._app_manager.get_by_id(existing_app.app_id) + + existing_approval = base64.b64encode(existing_app_record.approval_program).decode() + existing_clear = base64.b64encode(existing_app_record.clear_state_program).decode() + + new_approval = base64.b64encode(approval_program).decode() + new_clear = base64.b64encode(clear_program).decode() + + is_update = new_approval != existing_approval or new_clear != existing_clear + is_schema_break = ( + existing_app_record.local_ints + < (deployment.create_params.schema.get("local_ints", 0) if deployment.create_params.schema else 0) + or existing_app_record.global_ints + < (deployment.create_params.schema.get("global_ints", 0) if deployment.create_params.schema else 0) + or existing_app_record.local_byte_slices + < (deployment.create_params.schema.get("local_byte_slices", 0) if deployment.create_params.schema else 0) + or existing_app_record.global_byte_slices + < (deployment.create_params.schema.get("global_byte_slices", 0) if deployment.create_params.schema else 0) + ) + + if is_schema_break: + logger.warning( + f"Detected a breaking app schema change in app {existing_app.app_id}:", + extra={ + "from": { + "global_ints": existing_app_record.global_ints, + "global_byte_slices": existing_app_record.global_byte_slices, + "local_ints": existing_app_record.local_ints, + "local_byte_slices": existing_app_record.local_byte_slices, + }, + "to": deployment.create_params.schema, + }, + suppress_log=deployment.suppress_log, + ) + + return self._handle_schema_break( + deployment=deployment, + existing_app=existing_app, + approval_program=approval_program, + clear_program=clear_program, + ) + + if is_update: + return self._handle_update( + deployment=deployment, + existing_app=existing_app, + approval_program=approval_program, + clear_program=clear_program, + ) + + return AppDeployResult( + **existing_app.__dict__, + operation_performed=OperationPerformed.Nothing, + app_id=existing_app.app_id, + app_address=existing_app.app_address, + ) + + def _create_app( + self, + deployment: AppDeployParams, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Create a new application""" + + if isinstance(deployment.create_params, AppCreateMethodCall): + result = self._transaction_sender.app_create_method_call( + AppCreateMethodCall( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + result = self._transaction_sender.app_create( + AppCreateParams( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + app_metadata = AppMetaData( + app_id=result.app_id, + app_address=get_application_address(result.app_id), + **deployment.metadata.__dict__, + created_metadata=deployment.metadata, + created_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + app_metadata_dict = app_metadata.__dict__ + app_metadata_dict["operation_performed"] = OperationPerformed.Create + app_metadata_dict["app_id"] = result.app_id + app_metadata_dict["app_address"] = get_application_address(result.app_id) + + return AppDeployResult( + **app_metadata_dict, + tx_id=result.tx_id, + tx_ids=result.tx_ids, + transaction=result.transaction, + transactions=result.transactions, + confirmation=result.confirmation, + confirmations=result.confirmations, + return_value=result.return_value, + ) + + def _handle_schema_break( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): + raise ValueError( + "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp" + ) + + if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app") + + def _handle_update( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_update in (OnUpdate.Fail, "fail"): + raise ValueError( + "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." + ) + + if deployment.on_update in (OnUpdate.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if deployment.on_update in (OnUpdate.UpdateApp, "update"): + if existing_app.updatable: + return self._update_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app") + + if deployment.on_update in (OnUpdate.ReplaceApp, "replace"): + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app") + + raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") + + def _replace_app( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + composer = self._transaction_sender.new_group() + + # Add create transaction + if isinstance(deployment.create_params, AppCreateMethodCall): + composer.add_app_create_method_call( + AppCreateMethodCall( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + composer.add_app_create( + AppCreateParams( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + # Add delete transaction + if isinstance(deployment.delete_params, AppDeleteMethodCall): + delete_call_params = AppDeleteMethodCall( + **{ + **deployment.delete_params.__dict__, + "app_id": existing_app.app_id, + } + ) + composer.add_app_delete_method_call(delete_call_params) + else: + delete_params = AppDeleteParams( + **{ + **deployment.delete_params.__dict__, + "app_id": existing_app.app_id, + } + ) + composer.add_app_delete(delete_params) + + result = composer.send() + + app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] + app_metadata = AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + **deployment.metadata.__dict__, + created_metadata=deployment.metadata, + created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + deleted=False, + ) + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + app_metadata_dict = app_metadata.__dict__ + app_metadata_dict["operation_performed"] = OperationPerformed.Replace + app_metadata_dict["app_id"] = app_id + app_metadata_dict["app_address"] = get_application_address(app_id) + + # Extract return_value and delete_return_value from ABIResult + return_value = result.returns[0] if result.returns and isinstance(result.returns[0], ABIResult) else None + delete_return_value = ( + result.returns[-1] if len(result.returns) > 1 and isinstance(result.returns[-1], ABIResult) else None + ) + + return AppDeployResult( + **app_metadata_dict, + tx_id=result.tx_ids[0], + tx_ids=result.tx_ids, + transaction=result.transactions[0], + transactions=result.transactions, + confirmation=result.confirmations[0], + confirmations=result.confirmations, + return_value=return_value, + delete_return_value=delete_return_value, + delete_result=ConfirmedTransactionResult( + transaction=result.transactions[-1], + confirmation=result.confirmations[-1], + ), + ) + + def _update_app( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Update an existing application""" + + if isinstance(deployment.update_params, AppUpdateMethodCall): + result = self._transaction_sender.app_update_method_call( + AppUpdateMethodCall( + **{ + **deployment.update_params.__dict__, + "app_id": existing_app.app_id, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + result = self._transaction_sender.app_update( + AppUpdateParams( + **{ + **deployment.update_params.__dict__, + "app_id": existing_app.app_id, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + app_metadata = AppMetaData( + app_id=existing_app.app_id, + app_address=existing_app.app_address, + created_metadata=existing_app.created_metadata, + created_round=existing_app.created_round, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + **deployment.metadata.__dict__, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + return AppDeployResult( + **app_metadata.__dict__, + operation_performed=OperationPerformed.Update, + transaction=result.transaction, + transactions=result.transactions, + confirmation=result.confirmation, + confirmations=result.confirmations, + return_value=result.return_value, + ) + + def _update_app_lookup(self, sender: str, app_metadata: AppMetaData) -> None: + """Update the app lookup cache""" + + lookup = self._app_lookups.get(sender) + if not lookup: + self._app_lookups[sender] = AppLookup( + creator=sender, + apps={app_metadata.name: app_metadata}, + ) + else: + lookup.apps[app_metadata.name] = app_metadata + + def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> AppLookup: + """Get apps created by an account""" + + if not ignore_cache and creator_address in self._app_lookups: + return self._app_lookups[creator_address] + + if not self._indexer: + raise ValueError( + "Didn't receive an indexer client when this AppManager was created, " + "but received a call to get_creator_apps" + ) + + app_lookup: dict[str, AppMetaData] = {} + + # Get all apps created by account + created_apps = self._indexer.search_applications(creator=creator_address) + + for app in created_apps["applications"]: + app_id = app["id"] + + # Get creation transaction + creation_txns = self._indexer.search_transactions( + application_id=app_id, + min_round=app["created-at-round"], + address=creator_address, + address_role="sender", + note_prefix=base64.b64encode(APP_DEPLOY_NOTE_DAPP.encode()), + limit=1, + ) + + if not creation_txns["transactions"]: + continue + + creation_txn = creation_txns["transactions"][0] + + try: + note = base64.b64decode(creation_txn["note"]).decode() + if not note.startswith(f"{APP_DEPLOY_NOTE_DAPP}:j"): + continue + + metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :]) + + if metadata.get("name"): + app_lookup[metadata["name"]] = AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + created_metadata=metadata, + created_round=creation_txn["confirmed-round"], + **metadata, + updated_round=creation_txn["confirmed-round"], + deleted=app.get("deleted", False), + ) + except Exception as e: + logger.warning( + f"Error processing app {app_id} for creator {creator_address}: {e}", + ) + continue + + lookup = AppLookup(creator=creator_address, apps=app_lookup) + self._app_lookups[creator_address] = lookup + return lookup diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py new file mode 100644 index 00000000..2c8c3a94 --- /dev/null +++ b/src/algokit_utils/applications/app_factory.py @@ -0,0 +1,690 @@ +import base64 +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, TypeGuard, TypeVar + +import algosdk +from algosdk import transaction +from algosdk.abi import Method +from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.deploy import AppDeployMetaData, AppLookup, OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_client import ( + AppClient, + AppClientBareCallParams, + AppClientCompilationParams, + AppClientCompilationResult, + AppClientMethodCallParams, + AppClientParams, + AppSourceMaps, + ExposedLogicErrorDetails, +) +from algokit_utils.applications.app_deployer import ( + AppDeployParams, + ConfirmedTransactionResult, + DeployAppDeleteParams, + DeployAppUpdateParams, +) +from algokit_utils.applications.app_manager import TealTemplateParams +from algokit_utils.applications.utils import ( + get_abi_decoded_value, + get_abi_tuple_from_abi_struct, + get_arc56_method, + get_arc56_return_value, +) +from algokit_utils.models.abi import ABIStruct, ABIValue +from algokit_utils.models.application import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + Arc56Contract, + Arc56Method, + CompiledTeal, + MethodArg, +) +from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.application import AlgorandClientProtocol +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppUpdateMethodCall, + BuiltTransactions, +) +from algokit_utils.transactions.transaction_sender import SendAppCreateTransactionResult, SendAppTransactionResult + +T = TypeVar("T") + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryParams: + algorand: AlgorandClientProtocol + app_spec: Arc56Contract | ApplicationSpecification | str + app_name: str | None = None + default_sender: str | bytes | None = None + default_signer: TransactionSigner | None = None + version: str | None = None + updatable: bool | None = None + deletable: bool | None = None + deploy_time_params: TealTemplateParams | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateParams(AppClientBareCallParams, AppClientCompilationParams): + on_complete: transaction.OnComplete | None = None + schema: dict[str, int] | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateWithSendParams(AppFactoryCreateParams, SendParams): + pass + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompilationParams): + on_complete: transaction.OnComplete | None = None + schema: dict[str, int] | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, SendParams): + pass + + +@dataclass(frozen=True, kw_only=True) +class AppFactoryCreateResult(SendAppTransactionResult): + """Result from creating an application via AppFactory""" + + app_id: int + """The ID of the created application""" + app_address: str + """The address of the created application""" + compiled_approval: CompiledTeal | None = None + """The compiled approval program if source was provided""" + compiled_clear: CompiledTeal | None = None + """The compiled clear program if source was provided""" + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryDeployResult: + """Represents the result object from app deployment""" + + app_address: str + app_id: int + approval_program: bytes # Uint8Array + clear_state_program: bytes # Uint8Array + compiled_approval: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + compiled_clear: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + created_metadata: dict # {name: str, version: str, updatable: bool, deletable: bool} + created_round: int + deletable: bool + deleted: bool + delete_return_value: ABIValue | ABIStruct | None = None + delete_result: ConfirmedTransactionResult | None = None + group_id: str | None = None + name: str + operation_performed: OperationPerformed + return_value: ABIValue | ABIStruct | None = None + returns: list[Any] | None = None + transaction: TransactionWrapper + transactions: list[TransactionWrapper] + tx_id: str + tx_ids: list[str] + updatable: bool + updated_round: int + version: str + + +class _AppFactoryBareParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParams: + create_args = {} + if params: + create_args = {**params.__dict__.copy()} + del create_args["schema"] + del create_args["sender"] + del create_args["on_complete"] + del create_args["deploy_time_params"] + del create_args["updatable"] + del create_args["deletable"] + compiled = self._factory.compile(params) + create_args["approval_program"] = compiled.approval_program + create_args["clear_state_program"] = compiled.clear_state_program + + return AppCreateParams( + **create_args, + schema=(params.schema if params else None) + or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + }, + sender=self._factory._get_sender(params.sender if params else None), + on_complete=(params.on_complete if params else None) or OnComplete.NoOpOC, + ) + + def deploy_update(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.UpdateApplicationOC, + } + + def deploy_delete(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.DeleteApplicationOC, + } + + +class _AppFactoryParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareParamsAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareParamsAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCall: + compiled = self._factory.compile(params) + params_dict = params.__dict__ + params_dict["schema"] = params.schema or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + } + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = params.on_complete or OnComplete.NoOpOC + del params_dict["deploy_time_params"] + del params_dict["updatable"] + del params_dict["deletable"] + return AppCreateMethodCall( + **params_dict, + app_id=0, + approval_program=compiled.approval_program, + clear_state_program=compiled.clear_state_program, + ) + + def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCall: + params_dict = params.__dict__.copy() + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = OnComplete.UpdateApplicationOC + return AppUpdateMethodCall(**params_dict, app_id=0, approval_program="", clear_state_program="") + + def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + params_dict = params.__dict__.copy() + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = OnComplete.DeleteApplicationOC + return AppDeleteMethodCall(**params_dict, app_id=0) + + +class _AppFactoryBareCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + + def create(self, params: AppFactoryCreateParams | None = None) -> Transaction: + return self._factory._algorand.create_transaction.app_create(self._factory.params.bare.create(params)) + + +class _AppFactoryCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareCreateTransactionAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareCreateTransactionAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> BuiltTransactions: + return self._factory._algorand.create_transaction.app_create_method_call(self._factory.params.create(params)) + + +class _AppFactoryBareSendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateWithSendParams | None = None) -> tuple[AppClient, AppFactoryCreateResult]: + updatable = params.updatable if params and params.updatable is not None else self._factory._updatable + deletable = params.deletable if params and params.deletable is not None else self._factory._deletable + deploy_time_params = ( + params.deploy_time_params + if params and params.deploy_time_params is not None + else self._factory._deploy_time_params + ) + + compiled = self._factory.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + create_args = {} + if params: + create_args = {**params.__dict__} + del create_args["max_rounds_to_wait"] + del create_args["suppress_log"] + del create_args["populate_app_call_resources"] + + create_args["updatable"] = updatable + create_args["deletable"] = deletable + create_args["deploy_time_params"] = deploy_time_params + + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create( + self._factory.params.bare.create(AppFactoryCreateParams(**create_args)) + ) + ).__dict__ + + result["compiled_approval"] = compiled.compiled_approval + result["compiled_clear"] = compiled.compiled_clear + + return ( + self._factory.get_app_client_by_id( + app_id=result["app_id"], + ), + AppFactoryCreateResult(**result), + ) + + +class _AppFactorySendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + self._bare = _AppFactoryBareSendAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareSendAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, SendAppCreateTransactionResult]: + updatable = params.updatable if params.updatable is not None else self._factory._updatable + deletable = params.deletable if params.deletable is not None else self._factory._deletable + deploy_time_params = ( + params.deploy_time_params if params.deploy_time_params is not None else self._factory._deploy_time_params + ) + + compiled = self._factory.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + create_params_dict = params.__dict__.copy() + create_params_dict["updatable"] = updatable + create_params_dict["deletable"] = deletable + create_params_dict["deploy_time_params"] = deploy_time_params + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create_method_call( + self._factory.params.create(AppFactoryCreateMethodCallParams(**create_params_dict)) + ) + ) + + return ( + self._factory.get_app_client_by_id( + app_id=result.app_id, + ), + SendAppCreateTransactionResult( + **{ + **result.__dict__, + **( + {"compiled_approval": compiled.compiled_approval, "compiled_clear": compiled.compiled_clear} + if compiled + else {} + ), + } + ), + ) + + +class AppFactory: + def __init__(self, params: AppFactoryParams) -> None: + self._app_spec = AppClient.normalise_app_spec(params.app_spec) + self._app_name = params.app_name or self._app_spec.name + self._algorand = params.algorand + self._version = params.version or "1.0" + self._default_sender = params.default_sender + self._default_signer = params.default_signer + self._deploy_time_params = params.deploy_time_params + self._updatable = params.updatable + self._deletable = params.deletable + self._approval_source_map: SourceMap | None = None + self._clear_source_map: SourceMap | None = None + self._params_accessor = _AppFactoryParamsAccessor(self) + self._send_accessor = _AppFactorySendAccessor(self) + self._create_transaction_accessor = _AppFactoryCreateTransactionAccessor(self) + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def algorand(self) -> AlgorandClientProtocol: + return self._algorand + + @property + def params(self) -> _AppFactoryParamsAccessor: + return self._params_accessor + + @property + def send(self) -> _AppFactorySendAccessor: + return self._send_accessor + + @property + def create_transaction(self) -> _AppFactoryCreateTransactionAccessor: + return self._create_transaction_accessor + + def deploy( # noqa: PLR0913 + self, + *, + deploy_time_params: TealTemplateParams | None = None, + on_update: OnUpdate = OnUpdate.Fail, + on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + create_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + existing_deployments: AppLookup | None = None, + ignore_cache: bool = False, + updatable: bool | None = None, + deletable: bool | None = None, + app_name: str | None = None, + max_rounds_to_wait: int | None = None, # noqa: ARG002 TODO: revisit + suppress_log: bool = False, # noqa: ARG002 TODO: revisit + populate_app_call_resources: bool = False, # noqa: ARG002 TODO: revisit + ) -> tuple[AppClient, AppFactoryDeployResult]: + updatable = ( + updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") + ) + deletable = ( + deletable if deletable is not None else self._deletable or self._get_deploy_time_control("deletable") + ) + deploy_time_params = deploy_time_params if deploy_time_params is not None else self._deploy_time_params + + compiled = self.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + def _is_method_call_params( + params: AppClientMethodCallParams | AppClientBareCallParams | None, + ) -> TypeGuard[AppClientMethodCallParams]: + return params is not None and hasattr(params, "method") + + update_args: DeployAppUpdateParams | AppUpdateMethodCall + if _is_method_call_params(update_params): + update_args = self.params.deploy_update(update_params) + else: + update_args = DeployAppUpdateParams( + **self.params.bare.deploy_update( + update_params if isinstance(update_params, AppClientBareCallParams) else None + ) + ) + + delete_args: DeployAppDeleteParams | AppDeleteMethodCall + if _is_method_call_params(delete_params): + delete_args = self.params.deploy_delete(delete_params) + else: + delete_args = DeployAppDeleteParams( + **self.params.bare.deploy_delete( + delete_params if isinstance(delete_params, AppClientBareCallParams) else None + ) + ) + + app_deploy_params = AppDeployParams( + deploy_time_params=deploy_time_params, + on_schema_break=on_schema_break, + on_update=on_update, + existing_deployments=existing_deployments, + ignore_cache=ignore_cache, + create_params=( + self.params.create( + AppFactoryCreateMethodCallParams( + **create_params.__dict__, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + if create_params and hasattr(create_params, "method") + else self.params.bare.create( + AppFactoryCreateParams( + **create_params.__dict__ if create_params else {}, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + ), + update_params=update_args, + delete_params=delete_args, + metadata=AppDeployMetaData( + name=app_name or self._app_name, + version=self._version, + updatable=updatable, + deletable=deletable, + ), + ) + deploy_result = self._algorand.app_deployer.deploy(app_deploy_params) + + app_client = self.get_app_client_by_id( + app_id=deploy_result.app_id or 0, + app_name=app_name, + default_sender=self._default_sender, + default_signer=self._default_signer, + ) + + result = {**deploy_result.__dict__, **(compiled.__dict__ if compiled else {})} + + if "return_value" in result: + if result["operation_performed"] == OperationPerformed.Update: + if update_params and isinstance(update_params, AppClientMethodCallParams): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(update_params.method, self._app_spec), + self._app_spec.structs, + ) + elif create_params and isinstance(create_params, AppClientMethodCallParams): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(create_params.method, self._app_spec), + self._app_spec.structs, + ) + + if "delete_return_value" in result and delete_params and isinstance(delete_params, AppClientMethodCallParams): + result["delete_return_value"] = get_arc56_return_value( + result["delete_return_value"], + get_arc56_method(delete_params.method, self._app_spec), + self._app_spec.structs, + ) + + return app_client, AppFactoryDeployResult(**result) + + def get_app_client_by_id( + self, + app_id: int, + app_name: str | None = None, + default_sender: str | bytes | None = None, # Address can be string or bytes + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient( + AppClientParams( + app_id=app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ) + ) + + def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT002 FBT001 TODO: revisit + return AppClient.expose_logic_error_static( + e, + self._app_spec, + ExposedLogicErrorDetails( + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=None, + approval_source_info=( + self._app_spec.source_info.get("approval") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + clear_source_info=( + self._app_spec.source_info.get("clear") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + ), + ) + + def export_source_maps(self) -> AppSourceMaps: + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "you need to call create, update, or deploy first" + ) + return AppSourceMaps( + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + ) + + def import_source_maps(self, source_maps: AppSourceMaps) -> None: + self._approval_source_map = source_maps.approval_source_map + self._clear_source_map = source_maps.clear_source_map + + def compile(self, compilation: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + result = AppClient.compile( + self._app_spec, + self._algorand.app, + deploy_time_params=compilation.deploy_time_params if compilation else None, + updatable=compilation.updatable if compilation else None, + deletable=compilation.deletable if compilation else None, + ) + + if result.compiled_approval: + self._approval_source_map = result.compiled_approval.source_map + if result.compiled_clear: + self._clear_source_map = result.compiled_clear.source_map + + return result + + def _get_deploy_time_control(self, control: str) -> bool | None: + approval = ( + self._app_spec.source["approval"] if self._app_spec.source and "approval" in self._app_spec.source else None + ) + + template_name = UPDATABLE_TEMPLATE_NAME if control == "updatable" else DELETABLE_TEMPLATE_NAME + if not approval or template_name not in approval: + return None + + on_complete = "UpdateApplication" if control == "updatable" else "DeleteApplication" + return on_complete in self._app_spec.bare_actions.get("call", []) or any( + on_complete in m.actions.call for m in self._app_spec.methods if m.actions and m.actions.call + ) + + def _get_sender(self, sender: str | bytes | None) -> str: + if not sender and not self._default_sender: + raise Exception( + f"No sender provided and no default sender present in app client for call to app {self._app_name}" + ) + return str(sender or self._default_sender) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + try: + return call() + except Exception as e: + raise self.expose_logic_error(e) from None + + def _parse_method_call_return(self, result: SendAppTransactionResult, method: Method) -> SendAppTransactionResult: + return SendAppTransactionResult( + **{ + **result.__dict__, + "return_value": get_arc56_return_value(result.return_value, method, self._app_spec.structs) + if isinstance(result.return_value, ABIResult) + else None, + } + ) + + def _get_create_abi_args_with_default_values( + self, + method_name_or_signature: str | Arc56Method, + args: list[Any] | None, + ) -> list[Any]: + method = ( + get_arc56_method(method_name_or_signature, self._app_spec) + if isinstance(method_name_or_signature, str) + else method_name_or_signature + ) + result = [] + + def _has_struct(arg: Any) -> TypeGuard[MethodArg]: # noqa: ANN401 + return hasattr(arg, "struct") + + for i, method_arg in enumerate(method.args): + arg = method_arg + arg_value = args[i] if args and i < len(args) else None + + if arg_value is not None: + if _has_struct(arg) and arg.struct and isinstance(arg_value, dict): + arg_value = get_abi_tuple_from_abi_struct( + arg_value, + self._app_spec.structs[arg.struct], + self._app_spec.structs, + ) + result.append(arg_value) + continue + + default_value = getattr(arg, "default_value", None) + if default_value: + if default_value.source == "literal": + value_raw = base64.b64decode(default_value.data) + value_type = default_value.type or str(arg.type) + result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) + else: + raise ValueError( + f"Can't provide default value for {default_value.source} for a contract creation call" + ) + else: + raise ValueError( + f"No value provided for required argument " + f"{arg.name or f'arg{i+1}'} in call to method {method.name}" + ) + + return result diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index 307d5e0f..e282ede7 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -2,33 +2,44 @@ from collections.abc import Mapping from dataclasses import dataclass from enum import IntEnum -from typing import Any, TypeAlias +from typing import Any, TypeAlias, cast import algosdk import algosdk.atomic_transaction_composer import algosdk.box_reference -from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.atomic_transaction_composer import ABIResult, AccountTransactionSigner +from algosdk.box_reference import BoxReference as AlgosdkBoxReference from algosdk.logic import get_application_address +from algosdk.source_map import SourceMap from algosdk.v2client import algod -from algokit_utils.models.abi import ABIValue -from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.models.abi import ABIType, ABIValue +from algokit_utils.models.application import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + AppInformation, + AppState, + CompiledTeal, +) -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class BoxName: name: str name_raw: bytes name_base64: str -@dataclass(frozen=True) -class AppState: - key_raw: bytes - key_base64: str - value_raw: bytes | None - value_base64: str | None - value: str | int +@dataclass(kw_only=True, frozen=True) +class BoxValue: + name: BoxName + value: bytes + + +@dataclass(kw_only=True, frozen=True) +class BoxABIValue: + name: BoxName + value: ABIValue class DataTypeFlag(IntEnum): @@ -39,31 +50,17 @@ class DataTypeFlag(IntEnum): TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] -@dataclass(frozen=True) -class AppInformation: - app_id: int - app_address: str - approval_program: bytes - clear_state_program: bytes - creator: str - global_state: dict[str, AppState] - local_ints: int - local_byte_slices: int - global_ints: int - global_byte_slices: int - extra_program_pages: int | None +BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner -@dataclass(frozen=True) -class CompiledTeal: - teal: str - compiled: bytes - compiled_hash: str - compiled_base64_to_bytes: bytes - source_map: dict | None +class BoxReference(AlgosdkBoxReference): + def __init__(self, app_id: int, name: bytes): + super().__init__(app_index=app_id, name=name) - -BoxIdentifier = str | bytes | AccountTransactionSigner + def __eq__(self, other: object) -> bool: + if isinstance(other, (BoxReference | AlgosdkBoxReference)): + return self.app_index == other.app_index and self.name == other.name + return False def _is_valid_token_character(char: str) -> bool: @@ -134,7 +131,7 @@ def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: result: list[str] = [] match_count = 0 - token = f"TMPL_{template_variable}" + token = f"TMPL_{template_variable}" if not template_variable.startswith("TMPL_") else template_variable token_idx_offset = len(value) - len(token) for line in program_lines: comment_idx = _find_unquoted_string(line, "//") @@ -173,7 +170,7 @@ def compile_teal(self, teal_code: str) -> CompiledTeal: compiled=compiled["result"], compiled_hash=compiled["hash"], compiled_base64_to_bytes=base64.b64decode(compiled["result"]), - source_map=compiled.get("sourcemap"), + source_map=SourceMap(compiled.get("sourcemap", {})), ) self._compilation_results[teal_code] = result return result @@ -182,7 +179,7 @@ def compile_teal_template( self, teal_template_code: str, template_params: TealTemplateParams | None = None, - deployment_metadata: dict[str, bool] | None = None, + deployment_metadata: Mapping[str, bool] | None = None, ) -> CompiledTeal: teal_code = AppManager.strip_teal_comments(teal_template_code) teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) @@ -237,41 +234,51 @@ def get_box_names(self, app_id: int) -> list[BoxName]: ] def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: - name = b"" - if isinstance(box_name, str): - name = box_name.encode("utf-8") - elif isinstance(box_name, bytes): - name = box_name - elif isinstance(box_name, AccountTransactionSigner): - name = algosdk.encoding.decode_address(algosdk.account.address_from_private_key(box_name.private_key)) - else: - raise ValueError(f"Invalid box identifier type: {type(box_name)}") - + name = AppManager.get_box_reference(box_name)[1] box_result = self._algod.application_box_by_name(app_id, name) assert isinstance(box_result, dict) - return base64.b64decode(box_result["value"]) + return bytes(box_result["value"], "utf-8") def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: return [self.get_box_value(app_id, box_name) for box_name in box_names] - def get_box_value_from_abi_type( - self, app_id: int, box_name: BoxIdentifier, abi_type: algosdk.abi.ABIType - ) -> ABIValue: + def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_type: ABIType) -> ABIValue: value = self.get_box_value(app_id, box_name) try: - return abi_type.decode(value) # type: ignore[no-any-return] + parse_to_tuple = isinstance(abi_type, algosdk.abi.TupleType) + decoded_value = abi_type.decode(base64.b64decode(value)) + return tuple(decoded_value) if parse_to_tuple else decoded_value except Exception as e: raise ValueError(f"Failed to decode box value {value.decode('utf-8')} with ABI type {abi_type}") from e def get_box_values_from_abi_type( - self, app_id: int, box_names: list[BoxIdentifier], abi_type: algosdk.abi.ABIType + self, app_id: int, box_names: list[BoxIdentifier], abi_type: ABIType ) -> list[ABIValue]: return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] + @staticmethod + def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes]: + if isinstance(box_id, (BoxReference | AlgosdkBoxReference)): + return box_id.app_index, box_id.name + + name = b"" + if isinstance(box_id, str): + name = box_id.encode("utf-8") + elif isinstance(box_id, bytes): + name = box_id + elif isinstance(box_id, AccountTransactionSigner): + name = cast( + bytes, algosdk.encoding.decode_address(algosdk.account.address_from_private_key(box_id.private_key)) + ) + else: + raise ValueError(f"Invalid box identifier type: {type(box_id)}") + + return 0, name + @staticmethod def get_abi_return( confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None - ) -> ABIValue | None: + ) -> ABIResult | None: """Get the ABI return value from a transaction confirmation.""" if not method: return None @@ -287,7 +294,7 @@ def get_abi_return( if not abi_result: return None - return abi_result.return_value # type: ignore[no-any-return] + return abi_result @staticmethod def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: @@ -346,7 +353,7 @@ def replace_template_variables(program: str, template_values: TealTemplateParams return "\n".join(program_lines) @staticmethod - def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: dict[str, bool]) -> str: + def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: Mapping[str, bool]) -> str: if params.get("updatable") is not None: if UPDATABLE_TEMPLATE_NAME not in teal_template_code: raise ValueError( diff --git a/src/algokit_utils/applications/utils.py b/src/algokit_utils/applications/utils.py new file mode 100644 index 00000000..05bc4650 --- /dev/null +++ b/src/algokit_utils/applications/utils.py @@ -0,0 +1,428 @@ +import base64 +from typing import Any, Literal, TypeVar + +from algosdk.abi import Method as AlgorandABIMethod +from algosdk.abi import TupleType +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + AppSpecStateDict, + DefaultArgumentDict, + MethodConfigDict, + MethodHints, +) +from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue +from algokit_utils.models.application import ( + ABIArgumentType, + ABITypeAlias, + Arc56Contract, + Arc56ContractState, + Arc56Method, + CallConfig, + DefaultValue, + Method, + MethodActions, + MethodArg, + MethodReturns, + OnCompleteAction, + StorageKey, + StructField, + StructName, +) + +T = TypeVar("T", bound=ABIValue | bytes | ABIStruct | None) + + +def get_arc56_method(method_name_or_signature: str, app_spec: Arc56Contract) -> Arc56Method: + if "(" not in method_name_or_signature: + # Filter by method name + methods = [m for m in app_spec.methods if m.name == method_name_or_signature] + if not methods: + raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") + if len(methods) > 1: + signatures = [AlgorandABIMethod.undictify(m.__dict__).get_signature() for m in app_spec.methods] + raise ValueError( + f"Received a call to method {method_name_or_signature} in contract {app_spec.name}, " + f"but this resolved to multiple methods; please pass in an ABI signature instead: " + f"{', '.join(signatures)}" + ) + method = methods[0] + else: + # Find by signature + method = None + for m in app_spec.methods: + abi_method = AlgorandABIMethod.undictify(m.to_dict()) + if abi_method.get_signature() == method_name_or_signature: + method = m + break + + if method is None: + raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") + + return Arc56Method(method) + + +def get_arc56_return_value( + return_value: ABIResult | None, + method: Method | AlgorandABIMethod, + structs: dict[str, list[StructField]], +) -> ABIValue | ABIStruct | None: + """Checks for decode errors on the return value and maps it to the specified type. + + Args: + return_value: The smart contract response + method: The method that was called + structs: The struct fields from the app spec + + Returns: + The smart contract response with an updated return value + + Raises: + ValueError: If there is a decode error + """ + + # Get method returns info + if isinstance(method, AlgorandABIMethod): + type_str = method.returns.type + struct = None # AlgorandABIMethod doesn't have struct info + else: + type_str = method.returns.type + struct = method.returns.struct + + # Handle void/undefined returns + if type_str == "void" or return_value is None: + return None + + # Handle decode errors + if return_value.decode_error: + raise ValueError(return_value.decode_error) + + # Get raw return value + raw_value = return_value.raw_value + + # Handle AVM types + if type_str == "AVMBytes": + return raw_value + if type_str == "AVMString" and raw_value: + return raw_value.decode("utf-8") + if type_str == "AVMUint64" and raw_value: + return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return] + + # Handle structs + if struct and struct in structs: + return_tuple = return_value.return_value + return get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs) + + # Return as-is + return return_value.return_value # type: ignore[no-any-return] + + +def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: ANN401, PLR0911 + if isinstance(value, (bytes | bytearray)): + return value + if type_str == "AVMUint64": + return ABIType.from_string("uint64").encode(value) + if type_str in ("AVMBytes", "AVMString"): + if isinstance(value, str): + return value.encode("utf-8") + if not isinstance(value, (bytes | bytearray)): + raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}") + return value + if type_str in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs) + if isinstance(value, (list | tuple)): + return tuple_type.encode(value) # type: ignore[arg-type] + else: + tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs) + return tuple_type.encode(tuple_values) + else: + abi_type = ABIType.from_string(type_str) + return abi_type.encode(value) + + +def get_abi_decoded_value( + value: bytes | int | str, type_str: str | ABITypeAlias | ABIArgumentType, structs: dict[str, list[StructField]] +) -> ABIValue: + type_value = str(type_str) + + if type_value == "AVMBytes" or not isinstance(value, bytes): + return value + if type_value == "AVMString": + return value.decode("utf-8") + if type_value == "AVMUint64": + return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return] + if type_value in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs) + decoded_tuple = tuple_type.decode(value) + return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs) + return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return] + + +def get_abi_tuple_from_abi_struct( + struct_value: dict[str, Any], + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> list[Any]: + result = [] + for field in struct_fields: + key = field.name + if key not in struct_value: + raise ValueError(f"Missing value for field '{key}'") + value = struct_value[key] + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_tuple_from_abi_struct(value, field_type, structs) + result.append(value) + return result + + +def get_abi_tuple_type_from_abi_struct_definition( + struct_def: list[StructField], structs: dict[str, list[StructField]] +) -> TupleType: + types = [] + for field in struct_def: + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs)) + else: + types.append(ABIType.from_string(field_type)) # type: ignore[arg-type] + elif isinstance(field_type, list): + types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs)) + else: + raise ValueError(f"Invalid field type: {field_type}") + return TupleType(types) + + +def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> dict[str, Any]: + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + +def arc32_to_arc56(app_spec: ApplicationSpecification) -> Arc56Contract: # noqa: C901 + """ + Convert ARC-32 application specification to ARC-56 contract format. + + Args: + app_spec: ARC-32 application specification + + Returns: + ARC-56 contract specification + """ + + def convert_structs() -> dict[StructName, list[StructField]]: + structs: dict[StructName, list[StructField]] = {} + for hint in app_spec.hints.values(): + if not hint.structs: + continue + for struct in hint.structs.values(): + fields = [ + StructField( + name=name, + type=type_, + ) + for name, type_ in struct["elements"] + ] + structs[struct["name"]] = fields + return structs + + def get_hint(method: AlgorandABIMethod) -> MethodHints | None: + sig = method.get_signature() + return app_spec.hints.get(sig) + + def get_default_value( + type: str | ABIType, # noqa: A002 TODO: revisit + default_arg: DefaultArgumentDict, + ) -> DefaultValue | None: + if not default_arg or default_arg["source"] == "abi-method": + return None + + source_map = { + "constant": "literal", + "global-state": "global", + "local-state": "local", + } + + data = default_arg["data"] + if isinstance(data, str): + data = base64.b64encode(data.encode()).decode() + elif isinstance(data, bytes): + data = base64.b64encode(data).decode() + else: + data = str(data) + + return DefaultValue( + data=data, + type="AVMString" if type == "string" else str(type), + source=source_map.get(default_arg["source"], "literal"), # type: ignore[arg-type] + ) + + def convert_method(method: AlgorandABIMethod) -> Method: + hint = get_hint(method) + + args: list[MethodArg] = [] + for arg in method.args: + if not arg.name: + continue + struct_name = None + if hint and hint.structs and arg.name in hint.structs: + struct_name = hint.structs[arg.name].get("name") + + default_value = None + if hint and hint.default_arguments and arg.name in hint.default_arguments: + default_value = get_default_value(str(arg.type), hint.default_arguments[arg.name]) + + method_arg = MethodArg( + type=arg.type, # type: ignore[arg-type] + struct=struct_name, + name=arg.name, + desc=arg.desc, + default_value=default_value, + ) + args.append(method_arg) + + method_returns = MethodReturns( + type=str(method.returns.type), + struct=hint.structs.get("output", {}).get("name") if hint and hint.structs else None, # type: ignore[call-overload] + desc=method.returns.desc, + ) + + method_actions = MethodActions( + create=convert_actions(hint.call_config, "CREATE") if hint and hint.call_config else [], # type: ignore # noqa: PGH003 + call=convert_actions(hint.call_config, "CALL") if hint and hint.call_config else [], + ) + + return Method( + name=method.name, + desc=method.desc, + args=args, + returns=method_returns, + actions=method_actions, + readonly=hint.read_only if hint else False, + events=[], + recommendations=None, + ) + + def convert_storage_keys(schema_dict: AppSpecStateDict) -> dict[str, StorageKey]: + return { + name: StorageKey( + desc=spec.get("descr"), + key_type=spec["type"], + value_type="AVMUint64" if spec["type"] == "uint64" else "AVMBytes", + key=base64.b64encode(spec["key"].encode()).decode(), + ) + for name, spec in schema_dict.get("declared", {}).items() + } + + def convert_actions( + call_config: CallConfig | MethodConfigDict, action_type: Literal["CREATE", "CALL"] + ) -> list[OnCompleteAction | Literal["NoOp", "OptIn", "DeleteApplication"]]: + """ + Converts method configuration into a list of on-complete action literals. + + Args: + call_config (CallConfig | MethodConfigDict): Configuration dictionary or CallConfig object for method + actions. + action_type (Literal["CREATE", "CALL"]): The type of action to convert. + + Returns: + List[OnCompleteAction]: A list of on-complete action literals. + """ + config_action_map = { + "no_op": "NoOp", + "opt_in": "OptIn", + "close_out": "CloseOut", + "clear_state": "ClearState", + "update_application": "UpdateApplication", + "delete_application": "DeleteApplication", + } + + def get_action_value(key: str) -> str | None: + if isinstance(call_config, dict): + config_value = call_config.get(key) # type: ignore[call-overload] + # Handle legacy CallConfig enum + return config_value.name if hasattr(config_value, "name") else config_value # type: ignore[no-any-return] + # Handle new CallConfig dataclass + return getattr(call_config, key, None) + + return [action for key, action in config_action_map.items() if get_action_value(key) in ("ALL", action_type)] # type: ignore # noqa: PGH003 + + # Convert structs + structs = convert_structs() + + # Get schema information from app_spec + global_schema = app_spec.schema.get("global", {}) + local_schema = app_spec.schema.get("local", {}) + + state = Arc56ContractState( + schemas={ + "global": { + "ints": int(app_spec.global_state_schema.num_uints) if app_spec.global_state_schema.num_uints else 0, + "bytes": int(app_spec.global_state_schema.num_byte_slices) + if app_spec.global_state_schema.num_byte_slices + else 0, + }, + "local": { + "ints": int(app_spec.local_state_schema.num_uints) if app_spec.local_state_schema.num_uints else 0, + "bytes": int(app_spec.local_state_schema.num_byte_slices) + if app_spec.local_state_schema.num_byte_slices + else 0, + }, + }, + keys={ + "global": convert_storage_keys(global_schema), + "local": convert_storage_keys(local_schema), + "box": {}, + }, + maps={ + "global": {}, + "local": {}, + "box": {}, + }, + ) + + contract_source = { + "approval": app_spec.approval_program, + "clear": app_spec.clear_program, + } + + bare_actions = { + "create": convert_actions(app_spec.bare_call_config, "CREATE"), + "call": convert_actions(app_spec.bare_call_config, "CALL"), + } + + return Arc56Contract( + arcs=[], + name=app_spec.contract.name, + desc=app_spec.contract.desc, + structs=structs, + methods=[convert_method(m) for m in app_spec.contract.methods], + state=state, + source=contract_source, + bare_actions=bare_actions, + byte_code=None, + compiler_info=None, + events=None, + networks=None, + scratch_variables=None, + source_info=None, + template_variables=None, + ) diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index ee642dac..18184715 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -14,7 +14,7 @@ ) -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AccountAssetInformation: """Information about an account's holding of a particular asset.""" @@ -28,7 +28,7 @@ class AccountAssetInformation: """The round this information was retrieved at.""" -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetInformation: """Information about an asset.""" @@ -66,7 +66,7 @@ class AssetInformation: """32-byte hash of some metadata that is relevant to the asset and/or asset holders.""" -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class BulkAssetOptInOutResult: """Individual result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.""" diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index f679c95e..eb4ef73e 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -7,17 +7,11 @@ from typing_extensions import Self from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client, -) +from algokit_utils.models.network import AlgoClientConfigs from algokit_utils.transactions.transaction_composer import ( AppCallParams, AppMethodCallParams, @@ -36,16 +30,16 @@ __all__ = [ "AlgorandClient", - "AssetCreateParams", - "AssetOptInParams", + "AppCallParams", "AppMethodCallParams", - "PaymentParams", - "AssetFreezeParams", "AssetConfigParams", + "AssetCreateParams", "AssetDestroyParams", - "AppCallParams", - "OnlineKeyRegistrationParams", + "AssetFreezeParams", + "AssetOptInParams", "AssetTransferParams", + "OnlineKeyRegistrationParams", + "PaymentParams", ] @@ -53,7 +47,7 @@ class AlgorandClient: """A client that brokers easy access to Algorand functionality.""" def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): - self._client_manager: ClientManager = ClientManager(config) + self._client_manager: ClientManager = ClientManager(clients_or_configs=config, algorand_client=self) self._account_manager: AccountManager = AccountManager(self._client_manager) self._asset_manager: AssetManager = AssetManager(self._client_manager.algod, lambda: self.new_group()) self._app_manager: AppManager = AppManager(self._client_manager.algod) @@ -63,6 +57,9 @@ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): app_manager=self._app_manager, algod_client=self._client_manager.algod, ) + self._app_deployer: AppDeployer = AppDeployer( + self._app_manager, self._transaction_sender, self._client_manager.indexer_if_present + ) self._transaction_creator = AlgorandClientTransactionCreator( new_group=lambda: self.new_group(), ) @@ -163,10 +160,14 @@ def asset(self) -> AssetManager: return self._asset_manager @property - def app_deployer(self) -> AppManager: - """Get or create applications.""" + def app(self) -> AppManager: return self._app_manager + @property + def app_deployer(self) -> AppDeployer: + """Get or create applications.""" + return self._app_deployer + @property def send(self) -> AlgorandClientTransactionSender: """Methods for sending a transaction and waiting for confirmation""" @@ -192,9 +193,9 @@ def default_local_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_default_localnet_config("algod"), - indexer_config=get_default_localnet_config("indexer"), - kmd_config=get_default_localnet_config("kmd"), + algod_config=ClientManager.get_default_local_net_config("algod"), + indexer_config=ClientManager.get_default_local_net_config("indexer"), + kmd_config=ClientManager.get_default_local_net_config("kmd"), ) ) @@ -207,8 +208,8 @@ def test_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_algonode_config("testnet", "algod", ""), - indexer_config=get_algonode_config("testnet", "indexer", ""), + algod_config=ClientManager.get_algonode_config("testnet", "algod"), + indexer_config=ClientManager.get_algonode_config("testnet", "indexer"), kmd_config=None, ) ) @@ -222,8 +223,8 @@ def main_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_algonode_config("mainnet", "algod", ""), - indexer_config=get_algonode_config("mainnet", "indexer", ""), + algod_config=ClientManager.get_algonode_config("mainnet", "algod"), + indexer_config=ClientManager.get_algonode_config("mainnet", "indexer"), kmd_config=None, ) ) @@ -249,13 +250,7 @@ def from_environment() -> "AlgorandClient": :return: The `AlgorandClient` """ - return AlgorandClient( - AlgoSdkClients( - algod=get_algod_client(), - kmd=get_kmd_client(), - indexer=get_indexer_client(), - ) - ) + return AlgorandClient(ClientManager.get_config_from_environment_or_localnet()) @staticmethod def from_config(config: AlgoClientConfigs) -> "AlgorandClient": diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 16108520..ece39c6e 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,15 +1,22 @@ +import os +from dataclasses import dataclass +from typing import Literal +from urllib import parse + import algosdk +from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +# from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils.applications.app_manager import TealTemplateParams from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_indexer_client, - get_kmd_client, -) +from algokit_utils.models.application import Arc56Contract +from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs +from algokit_utils.protocols.application import AlgorandClientProtocol class AlgoSdkClients: @@ -24,21 +31,48 @@ def __init__( self.kmd = kmd +@dataclass(kw_only=True, frozen=True) +class NetworkDetail: + is_test_net: bool + is_main_net: bool + is_local_net: bool + genesis_id: str + genesis_hash: str + + +def genesis_id_is_localnet(genesis_id: str) -> bool: + return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + + +def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: + server = os.getenv(f"{environment_prefix}_SERVER") + if server is None: + raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") + port = os.getenv(f"{environment_prefix}_PORT") + if port: + parsed = parse.urlparse(server) + server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() + return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) + + class ClientManager: - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients): + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClientProtocol): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs elif isinstance(clients_or_configs, AlgoClientConfigs): _clients = AlgoSdkClients( - algod=get_algod_client(clients_or_configs.algod_config), - indexer=get_indexer_client(clients_or_configs.indexer_config) + algod=ClientManager.get_algod_client(clients_or_configs.algod_config), + indexer=ClientManager.get_indexer_client(clients_or_configs.indexer_config) if clients_or_configs.indexer_config else None, - kmd=get_kmd_client(clients_or_configs.kmd_config) if clients_or_configs.kmd_config else None, + kmd=ClientManager.get_kmd_client(clients_or_configs.kmd_config) + if clients_or_configs.kmd_config + else None, ) self._algod = _clients.algod self._indexer = _clients.indexer self._kmd = _clients.kmd + self._algorand = algorand_client @property def algod(self) -> AlgodClient: @@ -52,6 +86,10 @@ def indexer(self) -> IndexerClient: raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured") return self._indexer + @property + def indexer_if_present(self) -> IndexerClient | None: + return self._indexer + @property def kmd(self) -> KMDClient: """Returns an algosdk KMD API client or raises an error if it's not been provided.""" @@ -59,6 +97,25 @@ def kmd(self) -> KMDClient: raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured") return self._kmd + def network(self) -> NetworkDetail: + sp = self._algod.suggested_params() # TODO: cache it + return NetworkDetail( + is_test_net=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], + is_main_net=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], + is_local_net=ClientManager.genesis_id_is_local_net(str(sp.gen)), + genesis_id=str(sp.gen), + genesis_hash=sp.gh, + ) + + def is_local_net(self) -> bool: + return self.network().is_local_net + + def is_test_net(self) -> bool: + return self.network().is_test_net + + def is_main_net(self) -> bool: + return self.network().is_main_net + def get_testnet_dispenser( self, auth_token: str | None = None, request_timeout: int | None = None ) -> TestNetDispenserApiClient: @@ -66,3 +123,175 @@ def get_testnet_dispenser( return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout) return TestNetDispenserApiClient(auth_token=auth_token) + + def get_app_factory( + self, + app_spec: Arc56Contract | ApplicationSpecification | str, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + version: str | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + deploy_time_params: TealTemplateParams | None = None, + ) -> AppFactory: + if not self._algorand: + raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") + + return AppFactory( + AppFactoryParams( + algorand=self._algorand, + app_spec=app_spec, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + version=version, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + + @staticmethod + def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: + """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment + + If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" + config = config or _get_config_from_environment("ALGOD") + headers = {"X-Algo-API-Token": config.token or ""} + return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers) + + @staticmethod + def get_algod_client_from_environment() -> AlgodClient: + return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) + + @staticmethod + def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment + + If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" + config = config or _get_config_from_environment("KMD") + return KMDClient(config.token, config.server) + + @staticmethod + def get_kmd_client_from_environment() -> KMDClient: + return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) + + @staticmethod + def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: + """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. + + If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and + `INDEXER_TOKEN`""" + config = config or _get_config_from_environment("INDEXER") + headers = {"X-Indexer-API-Token": config.token} + return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers) + + @staticmethod + def get_indexer_client_from_environment() -> IndexerClient: + return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) + + @staticmethod + def genesis_id_is_local_net(genesis_id: str) -> bool: + return genesis_id_is_localnet(genesis_id) + + @staticmethod + def get_config_from_environment_or_localnet() -> AlgoClientConfigs: + """Retrieve client configuration from environment variables or fallback to localnet defaults. + + If ALGOD_SERVER is set in environment variables, it will use environment configuration, + otherwise it will use default localnet configuration. + + Returns: + AlgoClientConfigs: Configuration for algod, indexer, and optionally kmd + """ + algod_server = os.getenv("ALGOD_SERVER") + + if algod_server: + # Use environment configuration + algod_config = ClientManager.get_algod_config_from_environment() + + # Only include indexer if INDEXER_SERVER is set + indexer_config = ( + ClientManager.get_indexer_config_from_environment() if os.getenv("INDEXER_SERVER") else None + ) + + # Include KMD config only for local networks (not mainnet/testnet) + kmd_config = ( + ClientManager.get_kmd_config_from_environment() + if not any(net in algod_server.lower() for net in ["mainnet", "testnet"]) + else None + ) + else: + # Use localnet defaults + algod_config = ClientManager.get_default_local_net_config("algod") + indexer_config = ClientManager.get_default_local_net_config("indexer") + kmd_config = ClientManager.get_default_local_net_config("kmd") + + return AlgoClientConfigs( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config, + ) + + @staticmethod + def get_default_local_net_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: + port = ( + config_or_port + if isinstance(config_or_port, int) + else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port] + ) + + return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) + + @staticmethod + def get_algod_config_from_environment() -> AlgoClientConfig: + """Retrieve the algod configuration from environment variables. + + Expects ALGOD_SERVER to be defined in environment variables. + ALGOD_PORT and ALGOD_TOKEN are optional. + + Raises: + ValueError: If ALGOD_SERVER environment variable is not set + """ + return _get_config_from_environment("ALGOD") + + @staticmethod + def get_indexer_config_from_environment() -> AlgoClientConfig: + """Retrieve the indexer configuration from environment variables. + + Expects INDEXER_SERVER to be defined in environment variables. + INDEXER_PORT and INDEXER_TOKEN are optional. + + Raises: + ValueError: If INDEXER_SERVER environment variable is not set + """ + return _get_config_from_environment("INDEXER") + + @staticmethod + def get_kmd_config_from_environment() -> AlgoClientConfig: + """Retrieve the kmd configuration from environment variables. + + Expects KMD_SERVER to be defined in environment variables. + KMD_PORT and KMD_TOKEN are optional. + """ + return _get_config_from_environment("KMD") + + @staticmethod + def get_algonode_config( + network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"] + ) -> AlgoClientConfig: + """Returns the Algorand configuration to point to the free tier of the AlgoNode service. + + Args: + network: Which network to connect to - TestNet or MainNet + config: Which algod config to return - Algod or Indexer + + Returns: + AlgoClientConfig: Configuration for the specified network and service + """ + service_type = "api" if config == "algod" else "idx" + return AlgoClientConfig( + server=f"https://{network}-{service_type}.algonode.cloud", + port=443, + ) diff --git a/src/algokit_utils/clients/dispenser_api_client.py b/src/algokit_utils/clients/dispenser_api_client.py index 66593e80..b8a3ef78 100644 --- a/src/algokit_utils/clients/dispenser_api_client.py +++ b/src/algokit_utils/clients/dispenser_api_client.py @@ -1,12 +1,13 @@ import contextlib import enum -import logging import os from dataclasses import dataclass import httpx -logger = logging.getLogger(__name__) +from algokit_utils.config import config + +logger = config.logger class DispenserApiConfig: diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index 55850fd0..f76704ce 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -2,6 +2,7 @@ import os from collections.abc import Callable from pathlib import Path +from typing import Any logger = logging.getLogger(__name__) @@ -10,6 +11,50 @@ ALGOKIT_CONFIG_FILENAME = ".algokit.toml" +class AlgoKitLogger: + def __init__(self) -> None: + self._logger = logging.getLogger("algokit") + self._setup_logger() + + def _setup_logger(self) -> None: + formatter = logging.Formatter("%(levelname)s: %(message)s") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + self._logger.addHandler(handler) + self._logger.setLevel(logging.INFO) + + def _get_logger(self, *, suppress_log: bool = False) -> logging.Logger: + if suppress_log: + null_logger = logging.getLogger("null") + null_logger.addHandler(logging.NullHandler()) + return null_logger + return self._logger + + def error(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an error message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).error(message, *args, **kwargs) + + def exception(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an exception message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).exception(message, *args, **kwargs) + + def warning(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a warning message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).warning(message, *args, **kwargs) + + def info(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an info message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).info(message, *args, **kwargs) + + def debug(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a debug message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs) + + def verbose(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a verbose message (maps to debug), optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs) + + class UpdatableConfig: """Class to manage and update configuration settings for the AlgoKit project. @@ -19,26 +64,33 @@ class UpdatableConfig: trace_all (bool): Indicates whether to trace all operations. trace_buffer_size_mb (int): The size of the trace buffer in megabytes. max_search_depth (int): The maximum depth to search for a specific file. + populate_app_call_resources (bool): Indicates whether to populate app call resources. """ def __init__(self) -> None: + self._logger = AlgoKitLogger() self._debug: bool = False self._project_root: Path | None = None self._trace_all: bool = False self._trace_buffer_size_mb: int | float = 256 # megabytes self._max_search_depth: int = 10 + self._populate_app_call_resources: bool = False self._configure_project_root() def _configure_project_root(self) -> None: """Configures the project root by searching for a specific file within a depth limit.""" current_path = Path(__file__).resolve() for _ in range(self._max_search_depth): - logger.debug(f"Searching in: {current_path}") + self.logger.debug(f"Searching in: {current_path}") if (current_path / ALGOKIT_CONFIG_FILENAME).exists(): self._project_root = current_path break current_path = current_path.parent + @property + def logger(self) -> AlgoKitLogger: + return self._logger + @property def debug(self) -> bool: """Returns the debug status.""" @@ -59,6 +111,10 @@ def trace_buffer_size_mb(self) -> int | float: """Returns the size of the trace buffer in megabytes.""" return self._trace_buffer_size_mb + @property + def populate_app_call_resource(self) -> bool: + return self._populate_app_call_resources + def with_debug(self, func: Callable[[], str | None]) -> None: """Executes a function with debug mode temporarily enabled.""" original_debug = self._debug @@ -68,7 +124,7 @@ def with_debug(self, func: Callable[[], str | None]) -> None: finally: self._debug = original_debug - def configure( # noqa: PLR0913 + def configure( self, *, debug: bool, diff --git a/src/algokit_utils/errors/logic_error.py b/src/algokit_utils/errors/logic_error.py new file mode 100644 index 00000000..24fb40a0 --- /dev/null +++ b/src/algokit_utils/errors/logic_error.py @@ -0,0 +1,129 @@ +import base64 +import dataclasses +import re +from collections.abc import Callable +from copy import copy +from typing import TYPE_CHECKING, TypedDict + +from algosdk.atomic_transaction_composer import ( + SimulateAtomicTransactionResponse, +) + +if TYPE_CHECKING: + from algosdk.source_map import SourceMap as AlgoSourceMap + +__all__ = [ + "LogicError", + "parse_logic_error", +] + +LOGIC_ERROR = ( + ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" +) + +DEFAULT_BLAST_RADIUS = 5 + + +class LogicErrorData(TypedDict): + transaction_id: str + message: str + pc: int + + +@dataclasses.dataclass +class SimulationTrace: + app_budget_added: int | None + app_budget_consumed: int | None + failure_message: str | None + exec_trace: dict[str, object] + + +def parse_logic_error( + error_str: str, +) -> LogicErrorData | None: + match = re.match(LOGIC_ERROR, error_str) + if match is None: + return None + + return { + "transaction_id": match.group("transaction_id"), + "message": match.group("message"), + "pc": int(match.group("pc")), + } + + +class LogicError(Exception): + def __init__( + self, + *, + logic_error_str: str, + program: str, + source_map: "AlgoSourceMap | None", + transaction_id: str, + message: str, + pc: int, + logic_error: Exception | None = None, + traces: list[SimulationTrace] | None = None, + get_line_for_pc: Callable[[int], int | None] | None = None, + ): + self.logic_error = logic_error + self.logic_error_str = logic_error_str + try: + self.program = base64.b64decode(program).decode("utf-8") + except Exception: + self.program = program + self.source_map = source_map + self.lines = self.program.split("\n") + self.transaction_id = transaction_id + self.message = message + self.pc = pc + self.traces = traces + self.line_no = ( + self.source_map.get_line_for_pc(self.pc) + if self.source_map + else get_line_for_pc(self.pc) + if get_line_for_pc + else None + ) + + def __str__(self) -> str: + return ( + f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" + + (":" if self.line_no is None else f" and Source Line {self.line_no}:") + + f"\n{self.trace()}" + ) + + def trace(self, lines: int = 5) -> str: + if self.line_no is None: + return """ +Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the +error please provide an approval SourceMap. Either by: + 1.Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2.Set approval_source_map from a previously compiled approval program OR + 3.Import a previously exported source map using import_source_map""" + + program_lines = copy(self.lines) + program_lines[self.line_no] += "\t\t<-- Error" + lines_before = max(0, self.line_no - lines) + lines_after = min(len(program_lines), self.line_no + lines) + return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) + + +def create_simulate_traces_for_logic_error(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: + traces = [] + if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget_added = txn_group.get("app-budget-added", None) + app_budget_consumed = txn_group.get("app-budget-consumed", None) + failure_message = txn_group.get("failure-message", None) + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + traces.append( + SimulationTrace( + app_budget_added=app_budget_added, + app_budget_consumed=app_budget_consumed, + failure_message=failure_message, + exec_trace=exec_trace, + ) + ) + return traces diff --git a/src/algokit_utils/models/abi.py b/src/algokit_utils/models/abi.py index 767eed09..4e837274 100644 --- a/src/algokit_utils/models/abi.py +++ b/src/algokit_utils/models/abi.py @@ -1,4 +1,14 @@ +from typing import TypeAlias + +import algosdk + +from algokit_utils.models.application import StructField + ABIPrimitiveValue = bool | int | str | bytes | bytearray # NOTE: This is present in js-algorand-sdk, but sadly not in untyped py-algorand-sdk -ABIValue = ABIPrimitiveValue | list["ABIValue"] | dict[str, "ABIValue"] +ABIValue: TypeAlias = ABIPrimitiveValue | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] +ABIStruct: TypeAlias = dict[str, list[StructField]] + + +ABIType: TypeAlias = algosdk.abi.ABIType diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index 3014b7af..f83cc1e2 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -3,6 +3,8 @@ import algosdk from algosdk.atomic_transaction_composer import AccountTransactionSigner +DISPENSER_ACCOUNT_NAME = "DISPENSER" + @dataclasses.dataclass(kw_only=True) class Account: @@ -15,7 +17,7 @@ class Account: def __post_init__(self) -> None: if not self.address: - self.address = algosdk.account.address_from_private_key(self.private_key) + self.address = str(algosdk.account.address_from_private_key(self.private_key)) @property def public_key(self) -> bytes: diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index ac86cd3b..adb7ffae 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -121,3 +121,27 @@ def __ge__(self, other: object) -> bool: elif isinstance(other, int | Decimal): return self.amount_in_micro_algo >= int(other) raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'") + + def __sub__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: + if isinstance(other, AlgoAmount): + total_micro_algos = self.micro_algos - other.micro_algos + elif isinstance(other, (int | Decimal)): + total_micro_algos = self.micro_algos - int(other) + else: + raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") + return AlgoAmount.from_micro_algos(total_micro_algos) + + def __rsub__(self, other: int | Decimal) -> AlgoAmount: + if isinstance(other, (int | Decimal)): + total_micro_algos = int(other) - self.micro_algos + return AlgoAmount.from_micro_algos(total_micro_algos) + raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'") + + def __isub__(self, other: int | Decimal | AlgoAmount) -> Self: + if isinstance(other, AlgoAmount): + self.amount_in_micro_algo -= other.micro_algos + elif isinstance(other, (int | Decimal)): + self.amount_in_micro_algo -= int(other) + else: + raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") + return self diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py index c68e78af..6ab5d0ff 100644 --- a/src/algokit_utils/models/application.py +++ b/src/algokit_utils/models/application.py @@ -1,5 +1,469 @@ +import json +from dataclasses import asdict, dataclass, field, is_dataclass +from typing import Any, Literal, TypeAlias + +import algosdk + UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" """The name of the TEAL template variable for deploy-time immutability control.""" DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" """The name of the TEAL template variable for deploy-time permanence control.""" + + +# ===== ARCs ===== + +# Define type aliases +ABITypeAlias: TypeAlias = str +ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType +StructName: TypeAlias = str +AVMBytes = Literal["AVMBytes"] +AVMString = Literal["AVMString"] +AVMUint64 = Literal["AVMUint64"] +AVMType = AVMBytes | AVMString | AVMUint64 +OnCompleteAction = Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"] +DefaultValueSource = Literal["box", "global", "local", "literal", "method"] + + +def convert_key_to_snake_case(name: str) -> str: + import re + + return re.sub(r"(? Any: # noqa: ANN401 + if isinstance(obj, dict): + return {convert_key_to_snake_case(k): convert_keys_to_snake_case(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_keys_to_snake_case(item) for item in obj] + return obj + + +class SerializableBaseClass: + """ + A base class that provides a generic `dictify` method to convert dataclass instances + into dictionaries recursively. + """ + + def to_dict(self) -> dict[str, Any]: + def serialize(obj: Any) -> dict[str, Any] | list[Any] | Any: # noqa: ANN401 + if is_dataclass(obj) and not isinstance(obj, type): + return {k: serialize(v) for k, v in asdict(obj).items()} + elif isinstance(obj, algosdk.abi.ABIType): + return str(obj) + elif isinstance(obj, list): + return [serialize(item) for item in obj] + elif isinstance(obj, dict): + return {k: serialize(v) for k, v in obj.items()} + else: + return obj + + result = serialize(self) + if not isinstance(result, dict): + raise TypeError("Serialized object is not a dictionary.") + return result + + +@dataclass +class CallConfig: + no_op: str | None = None + opt_in: str | None = None + close_out: str | None = None + clear_state: str | None = None + update_application: str | None = None + delete_application: str | None = None + + +@dataclass(kw_only=True) +class StructField: + name: str + type: ABITypeAlias | StructName | list["StructField"] + + +@dataclass(kw_only=True) +class StorageKey: + desc: str | None + key_type: ABITypeAlias | AVMType | StructName + value_type: ABITypeAlias | AVMType | StructName + key: str # base64 encoded bytes + + +@dataclass(kw_only=True) +class StorageMap: + desc: str | None + key_type: ABITypeAlias | AVMType | StructName + value_type: ABITypeAlias | AVMType | StructName + prefix: str | None # base64-encoded prefix + + +@dataclass(kw_only=True) +class DefaultValue: + data: str + type: ABITypeAlias | AVMType | None = None + source: DefaultValueSource + + +@dataclass(kw_only=True) +class MethodArg: + type: ABITypeAlias + struct: StructName | None = None + name: str | None = None + desc: str | None = None + default_value: DefaultValue | None = None + + +@dataclass +class MethodReturns: + type: ABITypeAlias + struct: StructName | None = None + desc: str | None = None + + +@dataclass(kw_only=True) +class MethodActions: + create: list[Literal["NoOp", "OptIn", "DeleteApplication"]] + call: list[Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"]] + + +@dataclass(kw_only=True) +class BoxRecommendation: + app: int | None = None + key: str = "" + read_bytes: int = 0 + write_bytes: int = 0 + + +@dataclass(kw_only=True) +class Recommendations: + inner_transaction_count: int | None = None + boxes: list[BoxRecommendation] | None = None + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + + +@dataclass(kw_only=True) +class Method(SerializableBaseClass): + name: str + desc: str | None = None + args: list[MethodArg] = field(default_factory=list) + returns: MethodReturns = field(default_factory=lambda: MethodReturns(type="void")) + actions: MethodActions = field(default_factory=lambda: MethodActions(create=[], call=[])) + readonly: bool | None = False + events: list["Event"] | None = None + recommendations: Recommendations | None = None + + +@dataclass(kw_only=True) +class EventArg: + type: ABITypeAlias + name: str | None = None + desc: str | None = None + struct: StructName | None = None + + +@dataclass(kw_only=True) +class Event: + name: str + desc: str | None = None + args: list[EventArg] = field(default_factory=list) + + +@dataclass(kw_only=True) +class CompilerVersion: + major: int + minor: int + patch: int + commit_hash: str | None = None + + +@dataclass(kw_only=True) +class CompilerInfo: + compiler: Literal["algod", "puya"] + compiler_version: CompilerVersion + + +@dataclass +class SourceInfoDetail: + pc: list[int] + error_message: str | None = None + teal: int | None = None + source: str | None = None + + +@dataclass(kw_only=True) +class ProgramSourceInfo: + source_info: list[SourceInfoDetail] + pc_offset_method: Literal["none", "cblocks"] + + @staticmethod + def from_json(source_info: str | dict) -> "ProgramSourceInfo": + if "source_info" not in source_info: + raise ValueError("source_info is required") + source_dict: dict = json.loads(source_info) if isinstance(source_info, str) else source_info + parsed_source_dict = [SourceInfoDetail(**detail) for detail in source_dict["source_info"]] + return ProgramSourceInfo(source_info=parsed_source_dict, pc_offset_method=source_dict["pc_offset_method"]) + + +@dataclass(kw_only=True) +class Arc56ContractState: + keys: dict[str, dict[str, StorageKey]] + maps: dict[str, dict[str, StorageMap]] + schemas: dict[str, dict[str, int]] + + +@dataclass(kw_only=True) +class Arc56MethodArg: + """Represents an ARC-56 method argument with ABI type conversion.""" + + name: str | None = None + desc: str | None = None + struct: StructName | None = None + default_value: DefaultValue | None = None + type: ABIArgumentType + + @classmethod + def from_method_arg(cls, arg: MethodArg, converted_type: ABIArgumentType) -> "Arc56MethodArg": + """Create an Arc56MethodArg from a MethodArg with converted type.""" + return cls( + name=arg.name, + desc=arg.desc, + struct=arg.struct, + default_value=arg.default_value, + type=converted_type, + ) + + +@dataclass(kw_only=True) +class Arc56MethodReturnType: + """Represents an ARC-56 method return type with ABI type conversion.""" + + type: algosdk.abi.ABIType | Literal["void"] # Can be 'void' or ABIType + struct: StructName | None = None + desc: str | None = None + + +class Arc56Method(SerializableBaseClass, algosdk.abi.Method): + def __init__(self, method: Method) -> None: + # First, create the parent class with original arguments + super().__init__( + name=method.name, + args=method.args, # type: ignore[arg-type] + returns=algosdk.abi.Returns(arg_type=method.returns.type, desc=method.returns.desc), + desc=method.desc, + ) + self.method = method + + # Store our custom Arc56MethodArg list separately + + self._arc56_args = [ + Arc56MethodArg.from_method_arg( + arg, + algosdk.abi.ABIType.from_string(arg.type) + if not self._is_transaction_or_reference_type(arg.type) and isinstance(arg.type, str) + else arg.type, # type: ignore[arg-type] + ) + for arg in method.args + ] + + # Convert returns similar to TypeScript implementation, including struct support + converted_return_type: Literal["void"] | algosdk.abi.ABIType + if method.returns.type == "void": + converted_return_type = "void" + else: + converted_return_type = algosdk.abi.ABIType.from_string(str(method.returns.type)) + + self._arc56_returns = Arc56MethodReturnType( + type=converted_return_type, + struct=method.returns.struct, + desc=method.returns.desc, + ) + + def _is_transaction_or_reference_type(self, type_str: str) -> bool: + return type_str in [ + algosdk.constants.ASSETCONFIG_TXN, + algosdk.constants.PAYMENT_TXN, + algosdk.constants.KEYREG_TXN, + algosdk.constants.ASSETFREEZE_TXN, + algosdk.constants.ASSETTRANSFER_TXN, + algosdk.constants.APPCALL_TXN, + algosdk.constants.STATEPROOF_TXN, + algosdk.abi.ABIReferenceType.APPLICATION, + algosdk.abi.ABIReferenceType.ASSET, + algosdk.abi.ABIReferenceType.ACCOUNT, + ] + + @property + def arc56_args(self) -> list[Arc56MethodArg]: + """Get the ARC-56 specific argument representations.""" + return self._arc56_args + + @property + def arc56_returns(self) -> Arc56MethodReturnType: + """Get the ARC-56 specific returns type, including struct information.""" + return self._arc56_returns + + +@dataclass(kw_only=True) +class Arc56Contract(SerializableBaseClass): + arcs: list[int] + name: str + desc: str | None = None + networks: dict[str, dict[str, int]] | None = None + structs: dict[StructName, list[StructField]] = field(default_factory=dict) + methods: list[Method] = field(default_factory=list) + state: Arc56ContractState + bare_actions: dict[str, list[OnCompleteAction]] = field(default_factory=dict) + source_info: dict[str, ProgramSourceInfo] | None = None + source: dict[str, str] | None = None + byte_code: dict[str, str] | None = None + compiler_info: CompilerInfo | None = None + events: list[Event] | None = None + template_variables: dict[str, dict[str, ABITypeAlias | AVMType | StructName | str]] | None = None + scratch_variables: dict[str, dict[str, int | ABITypeAlias | AVMType | StructName]] | None = None + + @staticmethod + def from_json(application_spec: str | dict) -> "Arc56Contract": + """Convert a JSON dictionary into an Arc56Contract instance. + + Args: + json_data (dict): The JSON data representing an Arc56Contract + + Returns: + Arc56Contract: The constructed Arc56Contract instance + """ + # Convert networks if present + json_data = json.loads(application_spec) if isinstance(application_spec, str) else application_spec + json_data = convert_keys_to_snake_case(json_data) + networks = json_data.get("networks") + + # Convert structs + structs = { + name: [StructField(**field) if isinstance(field, dict) else field for field in struct_fields] + for name, struct_fields in json_data.get("structs", {}).items() + } + + # Convert methods + methods = [] + for method_data in json_data.get("methods", []): + # Convert method args + args = [MethodArg(**arg) for arg in method_data.get("args", [])] + + # Convert method returns + returns_data = method_data.get("returns", {"type": "void"}) + returns = MethodReturns(**returns_data) + + # Convert method actions + actions_data = method_data.get("actions", {"create": [], "call": []}) + actions = MethodActions(**actions_data) + + # Convert events if present + events = None + if "events" in method_data: + events = [Event(**event) for event in method_data["events"]] + + # Convert recommendations if present + recommendations = None + if "recommendations" in method_data: + recommendations = Recommendations(**method_data["recommendations"]) + + methods.append( + Method( + name=method_data["name"], + desc=method_data.get("desc"), + args=args, + returns=returns, + actions=actions, + readonly=method_data.get("readonly", False), + events=events, + recommendations=recommendations, + ) + ) + + # Convert state + state_data = json_data["state"] + state = Arc56ContractState( + keys={ + category: {name: StorageKey(**key_data) for name, key_data in keys.items()} + for category, keys in state_data.get("keys", {}).items() + }, + maps={ + category: {name: StorageMap(**map_data) for name, map_data in maps.items()} + for category, maps in state_data.get("maps", {}).items() + }, + schemas=state_data.get("schema", {}), + ) + + # Convert compiler info if present + compiler_info = None + if "compiler_info" in json_data: + compiler_version = CompilerVersion(**json_data["compiler_info"]["compiler_version"]) + compiler_info = CompilerInfo( + compiler=json_data["compiler_info"]["compiler"], compiler_version=compiler_version + ) + + # Convert events if present + events = None + if "events" in json_data: + events = [Event(**event) for event in json_data["events"]] + + source_info = {} + if "source_info" in json_data: + source_info = {key: ProgramSourceInfo.from_json(val) for key, val in json_data["source_info"].items()} + + return Arc56Contract( + arcs=json_data.get("arcs", []), + name=json_data["name"], + desc=json_data.get("desc"), + networks=networks, + structs=structs, + methods=methods, + state=state, + bare_actions=json_data.get("bare_actions", {}), + source_info=source_info, + source=json_data.get("source"), + byte_code=json_data.get("byte_code"), + compiler_info=compiler_info, + events=events, + template_variables=json_data.get("template_variables"), + scratch_variables=json_data.get("scratch_variables"), + ) + + +@dataclass(kw_only=True, frozen=True) +class AppState: + key_raw: bytes + key_base64: str + value_raw: bytes | None + value_base64: str | None + value: str | int + + +@dataclass(kw_only=True, frozen=True) +class AppInformation: + app_id: int + app_address: str + approval_program: bytes + clear_state_program: bytes + creator: str + global_state: dict[str, AppState] + local_ints: int + local_byte_slices: int + global_ints: int + global_byte_slices: int + extra_program_pages: int | None + + +@dataclass(kw_only=True, frozen=True) +class CompiledTeal: + teal: str + compiled: bytes + compiled_hash: str + compiled_base64_to_bytes: bytes + source_map: algosdk.source_map.SourceMap | None + + +@dataclass(kw_only=True, frozen=True) +class AppCompilationResult: + compiled_approval: CompiledTeal + compiled_clear: CompiledTeal diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py new file mode 100644 index 00000000..8ee897e2 --- /dev/null +++ b/src/algokit_utils/models/network.py @@ -0,0 +1,20 @@ +import dataclasses + + +@dataclasses.dataclass +class AlgoClientConfig: + """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or + {py:class}`algosdk.v2client.indexer.IndexerClient`""" + + server: str + """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + token: str | None = None + """API Token to authenticate with the service""" + port: str | int | None = None + + +@dataclasses.dataclass +class AlgoClientConfigs: + algod_config: AlgoClientConfig + indexer_config: AlgoClientConfig | None + kmd_config: AlgoClientConfig | None diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py new file mode 100644 index 00000000..ca8c0844 --- /dev/null +++ b/src/algokit_utils/models/transaction.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass(kw_only=True, frozen=True) +class SendParams: + max_rounds_to_wait: int | None = None + suppress_log: bool | None = None + populate_app_call_resources: bool | None = None diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/protocols/application.py b/src/algokit_utils/protocols/application.py new file mode 100644 index 00000000..c4782162 --- /dev/null +++ b/src/algokit_utils/protocols/application.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Protocol + +from typing_extensions import runtime_checkable + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + from algokit_utils.applications.app_deployer import AppDeployer + from algokit_utils.applications.app_manager import AppManager + from algokit_utils.clients.client_manager import ClientManager + from algokit_utils.transactions.transaction_composer import TransactionComposer + from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator + from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender + + +@dataclass +class NetworkDetails: + genesis_id: str + genesis_hash: str + network_name: str + + +@runtime_checkable +class AlgorandClientProtocol(Protocol): + @property + def app(self) -> AppManager: ... + + @property + def app_deployer(self) -> AppDeployer: ... + + @property + def send(self) -> AlgorandClientTransactionSender: ... + + @property + def create_transaction(self) -> AlgorandClientTransactionCreator: ... + + def new_group(self) -> TransactionComposer: ... + + @property + def client(self) -> ClientManager: ... + + +@runtime_checkable +class ClientManagerProtocol(Protocol): + @property + def algod(self) -> AlgodClient: ... + + @property + def indexer(self) -> IndexerClient | None: ... + + async def network(self) -> NetworkDetails: ... + + async def is_local_net(self) -> bool: ... + + async def is_test_net(self) -> bool: ... + + async def is_main_net(self) -> bool: ... diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py index 251bbf96..33edd94c 100644 --- a/src/algokit_utils/transactions/models.py +++ b/src/algokit_utils/transactions/models.py @@ -1,4 +1,6 @@ -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, TypeVar, cast + +import algosdk # Define specific types for different formats @@ -28,3 +30,51 @@ class JsonFormatArc2Note(BaseArc2Note): TransactionNoteData = str | None | int | list[Any] | dict[str, Any] TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote + +T = TypeVar("T") + + +class TransactionWrapper(algosdk.transaction.Transaction): + """Wrapper around algosdk.transaction.Transaction with optional property validators""" + + def __init__(self, transaction: algosdk.transaction.Transaction) -> None: + self._raw = transaction + + @property + def raw(self) -> algosdk.transaction.Transaction: + return self._raw + + @property + def payment(self) -> algosdk.transaction.PaymentTxn | None: + return self._return_if_type( + algosdk.transaction.PaymentTxn, + ) + + @property + def keyreg(self) -> algosdk.transaction.KeyregTxn | None: + return self._return_if_type(algosdk.transaction.KeyregTxn) + + @property + def asset_config(self) -> algosdk.transaction.AssetConfigTxn | None: + return self._return_if_type(algosdk.transaction.AssetConfigTxn) + + @property + def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn | None: + return self._return_if_type(algosdk.transaction.AssetTransferTxn) + + @property + def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn | None: + return self._return_if_type(algosdk.transaction.AssetFreezeTxn) + + @property + def application_call(self) -> algosdk.transaction.ApplicationCallTxn | None: + return self._return_if_type(algosdk.transaction.ApplicationCallTxn) + + @property + def state_proof(self) -> algosdk.transaction.StateProofTxn | None: + return self._return_if_type(algosdk.transaction.StateProofTxn) + + def _return_if_type(self, txn_type: type[T]) -> T | None: + if isinstance(self._raw, txn_type): + return cast(T, self._raw) + return None diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 77dea2e9..7d66c939 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,47 +1,52 @@ from __future__ import annotations -import logging import math from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Union import algosdk import algosdk.atomic_transaction_composer +import algosdk.v2client.models from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, TransactionSigner, TransactionWithSigner, ) from algosdk.error import AlgodHTTPError -from algosdk.transaction import OnComplete, Transaction +from algosdk.transaction import OnComplete from algosdk.v2client.algod import AlgodClient -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response from algokit_utils.applications.app_manager import AppManager from algokit_utils.config import config +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.utils import encode_lease, populate_app_call_resources if TYPE_CHECKING: from collections.abc import Callable from algosdk.abi import Method - from algosdk.box_reference import BoxReference from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.models import SimulateTraceConfig + from algokit_utils.applications.app_manager import BoxReference from algokit_utils.models.abi import ABIValue from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.models import Arc2TransactionNote -logger = logging.getLogger(__name__) +logger = config.logger -@dataclass(frozen=True) + +@dataclass(kw_only=True, frozen=True) class SenderParam: sender: str -@dataclass(frozen=True) -class CommonTxnParams: +@dataclass(kw_only=True, frozen=True) +class CommonTxnParams(SendParams): """ Common transaction parameters. @@ -72,14 +77,10 @@ class CommonTxnParams: last_valid_round: int | None = None -@dataclass(frozen=True) -class _RequiredPaymentParams: - receiver: str - amount: AlgoAmount - - -@dataclass(frozen=True) -class PaymentParams(CommonTxnParams, _RequiredPaymentParams): +@dataclass(kw_only=True, frozen=True) +class PaymentParams( + CommonTxnParams, +): """ Payment transaction parameters. @@ -88,21 +89,14 @@ class PaymentParams(CommonTxnParams, _RequiredPaymentParams): :param close_remainder_to: If given, close the sender account and send the remaining balance to this address. """ + receiver: str + amount: AlgoAmount close_remainder_to: str | None = None -@dataclass(frozen=True) -class _RequiredAssetCreateParams: - total: int - asset_name: str - unit_name: str - url: str - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetCreateParams( CommonTxnParams, - _RequiredAssetCreateParams, ): """ Asset creation parameters. @@ -123,6 +117,10 @@ class AssetCreateParams( :param metadata_hash: Hash of the metadata contained in the metadata URL. """ + total: int + asset_name: str | None = None + unit_name: str | None = None + url: str | None = None decimals: int | None = None default_frozen: bool | None = None manager: str | None = None @@ -132,15 +130,9 @@ class AssetCreateParams( metadata_hash: bytes | None = None -@dataclass(frozen=True) -class _RequiredAssetConfigParams: - asset_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetConfigParams( CommonTxnParams, - _RequiredAssetConfigParams, ): """ Asset configuration parameters. @@ -155,23 +147,16 @@ class AssetConfigParams( Clawback will be permanently disabled if undefined or an empty string. """ + asset_id: int manager: str | None = None reserve: str | None = None freeze: str | None = None clawback: str | None = None -@dataclass(frozen=True) -class _RequiredAssetFreezeParams: - asset_id: int - account: str - frozen: bool - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetFreezeParams( CommonTxnParams, - _RequiredAssetFreezeParams, ): """ Asset freeze parameters. @@ -181,16 +166,14 @@ class AssetFreezeParams( :param frozen: Whether the assets in the account should be frozen. """ - -@dataclass(frozen=True) -class _RequiredAssetDestroyParams: asset_id: int + account: str + frozen: bool -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetDestroyParams( CommonTxnParams, - _RequiredAssetDestroyParams, ): """ Asset destruction parameters. @@ -198,20 +181,12 @@ class AssetDestroyParams( :param asset_id: ID of the asset. """ - -@dataclass(frozen=True) -class _RequiredOnlineKeyRegistrationParams: - vote_key: str - selection_key: str - vote_first: int - vote_last: int - vote_key_dilution: int + asset_id: int -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class OnlineKeyRegistrationParams( CommonTxnParams, - _RequiredOnlineKeyRegistrationParams, ): """ Online key registration parameters. @@ -227,20 +202,17 @@ class OnlineKeyRegistrationParams( :param state_proof_key: The 64 byte state proof public key commitment. """ + vote_key: str + selection_key: str + vote_first: int + vote_last: int + vote_key_dilution: int state_proof_key: bytes | None = None -@dataclass(frozen=True) -class _RequiredAssetTransferParams: - asset_id: int - amount: int - receiver: str - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetTransferParams( CommonTxnParams, - _RequiredAssetTransferParams, ): """ Asset transfer parameters. @@ -252,19 +224,16 @@ class AssetTransferParams( :param close_asset_to: The account to close the asset to. """ + asset_id: int + amount: int + receiver: str clawback_target: str | None = None close_asset_to: str | None = None -@dataclass(frozen=True) -class _RequiredAssetOptInParams: - asset_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetOptInParams( CommonTxnParams, - _RequiredAssetOptInParams, ): """ Asset opt-in parameters. @@ -272,24 +241,22 @@ class AssetOptInParams( :param asset_id: ID of the asset. """ - -@dataclass(frozen=True) -class _RequiredAssetOptOutParams: asset_id: int - creator: str -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetOptOutParams( CommonTxnParams, - _RequiredAssetOptOutParams, ): """ Asset opt-out parameters. """ + asset_id: int + creator: str -@dataclass(frozen=True) + +@dataclass(kw_only=True, frozen=True) class AppCallParams(CommonTxnParams, SenderParam): """ Application call parameters. @@ -320,14 +287,8 @@ class AppCallParams(CommonTxnParams, SenderParam): box_references: list[BoxReference] | None = None -@dataclass(frozen=True) -class _RequiredAppCreateParams: - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): +@dataclass(kw_only=True, frozen=True) +class AppCreateParams(CommonTxnParams, SenderParam): """ Application create parameters. @@ -345,6 +306,8 @@ class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): :param extra_program_pages: Number of extra pages required for the programs """ + approval_program: str | bytes + clear_state_program: str | bytes schema: dict[str, int] | None = None on_complete: OnComplete | None = None args: list[bytes] | None = None @@ -355,15 +318,8 @@ class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): extra_program_pages: int | None = None -@dataclass(frozen=True) -class _RequiredAppUpdateParams: - app_id: int - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): +@dataclass(kw_only=True, frozen=True) +class AppUpdateParams(CommonTxnParams, SenderParam): """ Application update parameters. @@ -374,6 +330,9 @@ class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): teal (bytes) """ + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes args: list[bytes] | None = None account_references: list[str] | None = None app_references: list[int] | None = None @@ -382,16 +341,10 @@ class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): on_complete: OnComplete | None = None -@dataclass(frozen=True) -class _RequiredAppDeleteParams: - app_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteParams( CommonTxnParams, SenderParam, - _RequiredAppDeleteParams, ): """ Application delete parameters. @@ -400,19 +353,20 @@ class AppDeleteParams( """ app_id: int + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None on_complete: OnComplete = OnComplete.DeleteApplicationOC -@dataclass(frozen=True) -class _RequiredMethodCallParams: - app_id: int - method: Method - - -@dataclass(frozen=True) -class AppMethodCall(CommonTxnParams, SenderParam, _RequiredMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppMethodCall(CommonTxnParams, SenderParam): """Base class for ABI method calls.""" + app_id: int + method: Method args: list | None = None account_references: list[str] | None = None app_references: list[int] | None = None @@ -420,14 +374,8 @@ class AppMethodCall(CommonTxnParams, SenderParam, _RequiredMethodCallParams): box_references: list[BoxReference] | None = None -@dataclass(frozen=True) -class _RequiredAppMethodCallParams: - app_id: int - method: Method - - -@dataclass(frozen=True) -class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppMethodCallParams(CommonTxnParams, SenderParam): """ Method call parameters. @@ -437,6 +385,8 @@ class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallPa :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) """ + app_id: int + method: Method args: list[bytes] | None = None on_complete: OnComplete | None = None account_references: list[str] | None = None @@ -445,7 +395,7 @@ class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallPa box_references: list[BoxReference] | None = None -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppCallMethodCall(AppMethodCall): """Parameters for a regular ABI method call. @@ -464,14 +414,8 @@ class AppCallMethodCall(AppMethodCall): on_complete: OnComplete | None = None -@dataclass(frozen=True) -class _RequiredAppCreateMethodCallParams: - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppCreateMethodCall(AppMethodCall, _RequiredAppCreateMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppCreateMethodCall(AppMethodCall): """Parameters for an ABI method call that creates an application. :param approval_program: The program to execute for all OnCompletes other than ClearState @@ -481,20 +425,15 @@ class AppCreateMethodCall(AppMethodCall, _RequiredAppCreateMethodCallParams): :param extra_program_pages: Number of extra pages required for the programs """ + approval_program: str | bytes + clear_state_program: str | bytes schema: dict[str, int] | None = None on_complete: OnComplete | None = None extra_program_pages: int | None = None -@dataclass(frozen=True) -class _RequiredAppUpdateMethodCallParams: - app_id: int - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppUpdateMethodCall(AppMethodCall, _RequiredAppUpdateMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppUpdateMethodCall(AppMethodCall): """Parameters for an ABI method call that updates an application. :param app_id: ID of the application @@ -502,10 +441,13 @@ class AppUpdateMethodCall(AppMethodCall, _RequiredAppUpdateMethodCallParams): :param clear_state_program: The program to execute for ClearState OnComplete """ + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes on_complete: OnComplete = OnComplete.UpdateApplicationOC -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteMethodCall(AppMethodCall): """Parameters for an ABI method call that deletes an application. @@ -548,7 +490,7 @@ class AppDeleteMethodCall(AppMethodCall): ] -@dataclass +@dataclass(frozen=True) class BuiltTransactions: """ Set of transactions built by TransactionComposer. @@ -574,26 +516,27 @@ class TransactionComposerBuildResult: class SendAtomicTransactionComposerResults: """Results from sending an AtomicTransactionComposer transaction group""" - group_id: str | None + group_id: str """The group ID if this was a transaction group""" confirmations: list[algosdk.v2client.algod.AlgodResponseType] """The confirmation info for each transaction""" tx_ids: list[str] """The transaction IDs that were sent""" - transactions: list[Transaction] + transactions: list[TransactionWrapper] """The transactions that were sent""" - returns: list[Any] + returns: list[Any] | list[algosdk.atomic_transaction_composer.ABIResult] """The ABI return values from any ABI method calls""" + simulate_response: dict[str, Any] | None = None -def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 +def send_atomic_transaction_composer( # noqa: C901, PLR0912 atc: AtomicTransactionComposer, algod: AlgodClient, *, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, - suppress_log: bool = False, - populate_resources: bool | None = None, # TODO: implement/clarify # noqa: ARG001 + suppress_log: bool | None = None, + populate_resources: bool | None = None, # TODO: implement/clarify ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group @@ -615,6 +558,13 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 try: # Build transactions transactions_with_signer = atc.build_group() + + if populate_resources or ( + config.populate_app_call_resource + and any(isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer) + ): + atc = populate_app_call_resources(atc, algod) + transactions_to_send = [t.txn for t in transactions_with_signer] # Get group ID if multiple transactions @@ -652,11 +602,11 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 # Return results return SendAtomicTransactionComposerResults( - group_id=group_id, + group_id=group_id or "", confirmations=confirmations or [], tx_ids=[t.get_txid() for t in transactions_to_send], - transactions=transactions_to_send, - returns=[r.return_value for r in result.abi_results], + transactions=[TransactionWrapper(t) for t in transactions_to_send], + returns=result.abi_results, ) except AlgodHTTPError as e: @@ -720,7 +670,7 @@ class TransactionComposer: NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() - def __init__( # noqa: PLR0913 + def __init__( self, algod: AlgodClient, get_signer: Callable[[str], TransactionSigner], @@ -884,7 +834,7 @@ def build_transactions(self) -> BuiltTransactions: return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) - @deprecated(reason="Use send() instead", version="3.0.0") + @deprecated("Use send() instead") def execute( self, *, @@ -898,8 +848,8 @@ def send( self, *, max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool = False, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: group = self.build().transactions @@ -920,18 +870,78 @@ def send( except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e - def simulate(self) -> algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse: + def simulate( + self, + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, + fix_signers: bool | None = None, + ) -> SendAtomicTransactionComposerResults: + atc = AtomicTransactionComposer() if skip_signatures else self.atc + + if skip_signatures: + allow_empty_signatures = True + fix_signers = True + transactions = self.build_transactions() + for txn in transactions.transactions: + atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer.NULL_SIGNER)) + atc.method_dict = transactions.method_calls + else: + self.build() + if config.debug and config.project_root and config.trace_all: - return simulate_and_persist_response( - self.atc, + response = simulate_and_persist_response( + atc, config.project_root, self.algod, config.trace_buffer_size_mb, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) + + return SendAtomicTransactionComposerResults( + confirmations=[], # TODO: extract confirmations, + transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) - return simulate_response( - self.atc, + response = simulate_response( + atc, self.algod, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) + + confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ + "txn-results" + ] + + return SendAtomicTransactionComposerResults( + confirmations=[txn["txn-result"] for txn in confirmation_results], + transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) @staticmethod @@ -966,7 +976,7 @@ def _common_txn_build_step( suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: if params.lease: - txn.lease = params.lease + txn.lease = encode_lease(params.lease) if params.rekey_to: txn.rekey_to = params.rekey_to if params.note: @@ -1002,53 +1012,55 @@ def _build_method_call( # noqa: C901, PLR0912 arg_offset = 0 if params.args: - for i, arg in enumerate(params.args): + for _, arg in enumerate(params.args): if self._is_abi_value(arg): method_args.append(arg) continue - if algosdk.abi.is_abi_transaction_type(params.method.args[i + arg_offset].type): - match arg: - case ( - AppCreateMethodCall() - | AppCallMethodCall() - | AppUpdateMethodCall() - | AppDeleteMethodCall() - ): - temp_txn_with_signers = self._build_method_call(arg, suggested_params) - method_args.extend(temp_txn_with_signers) - arg_offset += len(temp_txn_with_signers) - 1 - continue - case AppCallParams(): - txn = self._build_app_call(arg, suggested_params) - case PaymentParams(): - txn = self._build_payment(arg, suggested_params) - case AssetOptInParams(): - txn = self._build_asset_transfer( - AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params - ) - case AssetCreateParams(): - txn = self._build_asset_create(arg, suggested_params) - case AssetConfigParams(): - txn = self._build_asset_config(arg, suggested_params) - case AssetDestroyParams(): - txn = self._build_asset_destroy(arg, suggested_params) - case AssetFreezeParams(): - txn = self._build_asset_freeze(arg, suggested_params) - case AssetTransferParams(): - txn = self._build_asset_transfer(arg, suggested_params) - case OnlineKeyRegistrationParams(): - txn = self._build_key_reg(arg, suggested_params) - case _: - raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + if isinstance(arg, TransactionWithSigner): + method_args.append(arg) + continue + if isinstance(arg, algosdk.transaction.Transaction): + # Wrap in TransactionWithSigner method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + TransactionWithSigner(txn=arg, signer=params.signer or self.get_signer(params.sender)) ) - continue + match arg: + case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): + temp_txn_with_signers = self._build_method_call(arg, suggested_params) + method_args.extend(temp_txn_with_signers) + arg_offset += len(temp_txn_with_signers) - 1 + continue + case AppCallParams(): + txn = self._build_app_call(arg, suggested_params) + case PaymentParams(): + txn = self._build_payment(arg, suggested_params) + case AssetOptInParams(): + txn = self._build_asset_transfer( + AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params + ) + case AssetCreateParams(): + txn = self._build_asset_create(arg, suggested_params) + case AssetConfigParams(): + txn = self._build_asset_config(arg, suggested_params) + case AssetDestroyParams(): + txn = self._build_asset_destroy(arg, suggested_params) + case AssetFreezeParams(): + txn = self._build_asset_freeze(arg, suggested_params) + case AssetTransferParams(): + txn = self._build_asset_transfer(arg, suggested_params) + case OnlineKeyRegistrationParams(): + txn = self._build_key_reg(arg, suggested_params) + case _: + raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + + method_args.append( + TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + ) - raise ValueError(f"Unsupported method arg: {arg!s}") + continue method_atc = AtomicTransactionComposer() @@ -1059,10 +1071,18 @@ def _build_method_call( # noqa: C901, PLR0912 sp=suggested_params, signer=params.signer or self.get_signer(params.sender), method_args=method_args, - on_complete=algosdk.transaction.OnComplete.NoOpOC, + on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC, note=params.note, lease=params.lease, - boxes=[(ref.app_index, ref.name) for ref in params.box_references] if params.box_references else None, + boxes=[AppManager.get_box_reference(ref) for ref in params.box_references] + if params.box_references + else None, + foreign_apps=params.app_references, + foreign_assets=params.asset_references, + accounts=params.account_references, + approval_program=params.approval_program if hasattr(params, "approval_program") else None, # type: ignore[arg-type] + clear_program=params.clear_state_program if hasattr(params, "clear_state_program") else None, # type: ignore[arg-type] + rekey_to=params.rekey_to, ) return self._build_atc(method_atc) @@ -1088,13 +1108,13 @@ def _build_asset_create( sp=suggested_params, total=params.total, default_frozen=params.default_frozen or False, - unit_name=params.unit_name, - asset_name=params.asset_name, + unit_name=params.unit_name or "", + asset_name=params.asset_name or "", manager=params.manager, reserve=params.reserve, freeze=params.freeze, clawback=params.clawback, - url=params.url, + url=params.url or "", metadata_hash=params.metadata_hash, decimals=params.decimals or 0, ) @@ -1103,10 +1123,10 @@ def _build_asset_create( def _build_app_call( self, - params: AppCallParams | AppUpdateParams | AppCreateParams, + params: AppCallParams | AppUpdateParams | AppCreateParams | AppDeleteParams, suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: - app_id = params.app_id if isinstance(params, AppCallParams | AppUpdateMethodCall) else None + app_id = getattr(params, "app_id", 0) approval_program = None clear_program = None @@ -1148,12 +1168,12 @@ def _build_app_call( txn = algosdk.transaction.ApplicationCreateTxn( **sdk_params, global_schema=algosdk.transaction.StateSchema( - num_uints=params.schema.get("global_uints", 0), - num_byte_slices=params.schema.get("global_byte_slices", 0), + num_uints=params.schema.get("global_ints", 0), + num_byte_slices=params.schema.get("global_bytes", 0), ), local_schema=algosdk.transaction.StateSchema( - num_uints=params.schema.get("local_uints", 0), - num_byte_slices=params.schema.get("local_byte_slices", 0), + num_uints=params.schema.get("local_ints", 0), + num_byte_slices=params.schema.get("local_bytes", 0), ), extra_pages=params.extra_program_pages or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) @@ -1239,7 +1259,7 @@ def _build_key_reg( return self._common_txn_build_step(params, txn, suggested_params) def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool: - if isinstance(x, list): + if isinstance(x, list | tuple): return len(x) == 0 or all(self._is_abi_value(item) for item in x) return isinstance(x, bool | int | float | str | bytes) @@ -1254,6 +1274,9 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 return [txn] case AtomicTransactionComposer(): return self._build_atc(txn) + case algosdk.transaction.Transaction(): + signer = self.get_signer(txn.sender) + return [TransactionWithSigner(txn=txn, signer=signer)] case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): return self._build_method_call(txn, suggested_params) @@ -1266,7 +1289,7 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 case AssetCreateParams(): asset_create = self._build_asset_create(txn, suggested_params) return [TransactionWithSigner(txn=asset_create, signer=signer)] - case AppCallParams() | AppUpdateParams() | AppCreateParams(): + case AppCallParams() | AppUpdateParams() | AppCreateParams() | AppDeleteParams(): app_call = self._build_app_call(txn, suggested_params) return [TransactionWithSigner(txn=app_call, signer=signer)] case AssetConfigParams(): diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 3050dcb7..831100d2 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,16 +1,16 @@ from collections.abc import Callable from dataclasses import dataclass -from logging import getLogger from typing import Any, TypedDict, TypeVar import algosdk import algosdk.atomic_transaction_composer -from algosdk.atomic_transaction_composer import AtomicTransactionResponse +from algosdk.atomic_transaction_composer import ABIResult, AtomicTransactionResponse from algosdk.transaction import Transaction from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager -from algokit_utils.models.abi import ABIValue +from algokit_utils.config import config +from algokit_utils.transactions.models import TransactionWrapper from algokit_utils.transactions.transaction_composer import ( AppCallMethodCall, AppCallParams, @@ -33,48 +33,45 @@ TxnParams, ) -logger = getLogger(__name__) +logger = config.logger -@dataclass +@dataclass(frozen=True, kw_only=True) class SendSingleTransactionResult: - tx_id: str # Single transaction ID (last from txIds array) - transaction: Transaction # Last transaction + transaction: TransactionWrapper # Last transaction confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation # Fields from SendAtomicTransactionComposerResults group_id: str + tx_id: str | None = None tx_ids: list[str] # Full array of transaction IDs - transactions: list[Transaction] + transactions: list[TransactionWrapper] confirmations: list[algosdk.v2client.algod.AlgodResponseType] returns: list[algosdk.atomic_transaction_composer.ABIResult] | None = None - # Fields from AssetCreateParams - asset_id: int | None = None +@dataclass(frozen=True, kw_only=True) +class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): + asset_id: int -@dataclass + +@dataclass(frozen=True) class SendAppTransactionResult(SendSingleTransactionResult): - return_value: ABIValue | None = None + return_value: ABIResult | None = None -@dataclass +@dataclass(frozen=True) class SendAppUpdateTransactionResult(SendAppTransactionResult): compiled_approval: Any | None = None compiled_clear: Any | None = None -@dataclass -class _RequiredSendAppTransactionResult: +@dataclass(frozen=True, kw_only=True) +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult): app_id: int app_address: str -@dataclass -class SendAppCreateTransactionResult(SendAppUpdateTransactionResult, _RequiredSendAppTransactionResult): - pass - - class LogConfig(TypedDict, total=False): pre_log: Callable[[TxnParams, Transaction], str] post_log: Callable[[TxnParams, AtomicTransactionResponse], str] @@ -116,11 +113,14 @@ def send_transaction(params: T) -> SendSingleTransactionResult: logger.debug(pre_log(params, transaction)) raw_result = composer.send() + raw_result_dict = raw_result.__dict__.copy() + raw_result_dict["transactions"] = raw_result.transactions + del raw_result_dict["simulate_response"] result = SendSingleTransactionResult( - **raw_result.__dict__, + **raw_result_dict, confirmation=raw_result.confirmations[-1], - transaction=raw_result.transactions[-1], + transaction=raw_result_dict["transactions"][-1], tx_id=raw_result.tx_ids[-1], ) @@ -210,7 +210,7 @@ def payment(self, params: PaymentParams) -> SendSingleTransactionResult: ), )(params) - def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult: + def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransactionResult: """Create a new Algorand Standard Asset.""" result = self._send( lambda c: c.add_asset_create, @@ -223,11 +223,10 @@ def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult ), )(params) - result = SendSingleTransactionResult( + return SendSingleAssetCreateTransactionResult( **result.__dict__, + asset_id=int(result.confirmation["asset-index"]), # type: ignore[call-overload] ) - result.asset_id = int(result.confirmation["asset-index"]) # type: ignore[call-overload] - return result def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: """Configure an existing Algorand Standard Asset.""" diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py new file mode 100644 index 00000000..216db262 --- /dev/null +++ b/src/algokit_utils/transactions/utils.py @@ -0,0 +1,302 @@ +from typing import Any, cast + +from algosdk import logic, transaction +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, EmptySigner, TransactionWithSigner +from algosdk.box_reference import BoxReference +from algosdk.error import AtomicTransactionComposerError +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.models import SimulateRequest + +# Constants +MAX_APP_CALL_ACCOUNT_REFERENCES = 4 +MAX_APP_CALL_FOREIGN_REFERENCES = 8 + + +def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: # noqa: C901, PLR0915, PLR0912 + """ + Populate application call resources based on simulation results. + """ + # Get unnamed resources from simulation + unnamed_resources = get_unnamed_app_call_resources_accessed(atc, algod) + group = atc.build_group() + + # Process transaction-level resources + for i, txn_resources in enumerate(unnamed_resources["txns"]): + if not txn_resources or not isinstance(group[i].txn, transaction.ApplicationCallTxn): + continue + + # Validate no unexpected resources + if txn_resources.get("boxes") or txn_resources.get("extraBoxRefs"): + raise ValueError("Unexpected boxes at the transaction level") + if txn_resources.get("appLocals"): + raise ValueError("Unexpected app local at the transaction level") + if txn_resources.get("assetHoldings"): + raise ValueError("Unexpected asset holding at the transaction level") + + # Update application call fields + app_txn = cast(transaction.ApplicationCallTxn, group[i].txn) + accounts = list(getattr(app_txn, "accounts", []) or []) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + boxes = list(getattr(app_txn, "boxes", []) or []) + + # Add new resources + accounts.extend(txn_resources.get("accounts", [])) + foreign_apps.extend(txn_resources.get("apps", [])) + foreign_assets.extend(txn_resources.get("assets", [])) + boxes.extend(txn_resources.get("boxes", [])) + + # Validate limits + if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES: + raise ValueError( + f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}" + ) + + total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes) + if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES: + raise ValueError( + f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}" + ) + + # Update transaction + app_txn.accounts = accounts + app_txn.foreign_apps = foreign_apps + app_txn.foreign_assets = foreign_assets + app_txn.boxes = boxes + + def populate_group_resource( # noqa: C901, PLR0915 + txns: list[TransactionWithSigner], reference: str | BoxReference | dict[str, Any] | int, ref_type: str + ) -> None: + """Helper function to populate group-level resources""" + + def is_appl_below_limit(t: TransactionWithSigner) -> bool: + if not isinstance(t.txn, transaction.ApplicationCallTxn): + return False + + app_txn = t.txn + accounts = len(app_txn.accounts or []) + assets = len(app_txn.foreign_assets or []) + apps = len(app_txn.foreign_apps or []) + boxes = len(app_txn.boxes or []) + + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Handle asset holding and app local references + if ref_type in ("assetHolding", "appLocal"): + ref_dict = cast(dict[str, Any], reference) + account = ref_dict["account"] + + # Try to find transaction with account already available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + account in (getattr(t.txn, "accounts", []) or []) + or account + in ( + logic.get_application_address(app_id) + for app_id in (getattr(t.txn, "foreign_apps", []) or []) + ) + or any(account in str(v) for v in t.txn.__dict__.values()) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + if ref_type == "assetHolding": + asset_id = ref_dict["asset"] + app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id] + else: + app_id = ref_dict["app"] + app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] + return + + # Find available transaction for the resource + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES + if ref_type == "account" + else True + ) + ), + -1, + ) + + if txn_idx == -1: + raise ValueError("No more transactions below reference limit. Add another app call to the group.") + + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + + # Add resource based on type + if ref_type == "account": + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(cast(str, reference)) + app_txn.accounts = accounts + elif ref_type == "app": + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(int(cast(str | int, reference))) + app_txn.foreign_apps = foreign_apps + elif ref_type == "box": + box_ref = cast(BoxReference, reference) + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(box_ref) + app_txn.boxes = boxes + if box_ref.app_index != 0: + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(box_ref.app_index) + app_txn.foreign_apps = foreign_apps + elif ref_type == "asset": + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(int(cast(str | int, reference))) + app_txn.foreign_assets = foreign_assets + elif ref_type == "assetHolding": + ref_dict = cast(dict[str, Any], reference) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(ref_dict["asset"]) + app_txn.foreign_assets = foreign_assets + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + elif ref_type == "appLocal": + ref_dict = cast(dict[str, Any], reference) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(ref_dict["app"]) + app_txn.foreign_apps = foreign_apps + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + + # Process group-level resources + group_resources = unnamed_resources["group"] + if group_resources: + # Handle cross-reference resources first + for app_local in group_resources.get("appLocals", []): + populate_group_resource(group, app_local, "appLocal") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != app_local["account"] + ] + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])] + + for asset_holding in group_resources.get("assetHoldings", []): + populate_group_resource(group, asset_holding, "assetHolding") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != asset_holding["account"] + ] + if "assets" in group_resources: + group_resources["assets"] = [ + asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"]) + ] + + # Handle remaining resources + for account in group_resources.get("accounts", []): + populate_group_resource(group, account, "account") + + for box in group_resources.get("boxes", []): + populate_group_resource(group, box, "box") + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box.app_index)] + + for asset in group_resources.get("assets", []): + populate_group_resource(group, asset, "asset") + + for app in group_resources.get("apps", []): + populate_group_resource(group, app, "app") + + # Handle extra box references + extra_box_refs = group_resources.get("extraBoxRefs", 0) + for _ in range(extra_box_refs): + empty_box = BoxReference(0, b"") + populate_group_resource(group, empty_box, "box") + + # Create new ATC with updated transactions + new_atc = AtomicTransactionComposer() + for txn_with_signer in group: + txn_with_signer.txn.group = None + new_atc.add_transaction(txn_with_signer) + + # Copy method calls + new_atc.method_dict = atc.method_dict.copy() + + return new_atc + + +def get_unnamed_app_call_resources_accessed(atc: AtomicTransactionComposer, algod: AlgodClient) -> dict[str, Any]: + """Get unnamed resources accessed by application calls in an atomic transaction group.""" + # Create simulation request with required flags + simulate_request = SimulateRequest( + txn_groups=[], allow_unnamed_resources=True, allow_empty_signatures=True, extra_opcode_budget=0 + ) + + # Create empty signer + null_signer = EmptySigner() + + # Clone the ATC and replace signers + empty_signer_atc = atc.clone() + for txn in empty_signer_atc.txn_list: + txn.signer = null_signer + + # Run simulation + result = empty_signer_atc.simulate(algod, simulate_request) + + # Get first group response + group_response = result.simulate_response["txn-groups"][0] + + # Check for simulation failure + if group_response.get("failure-message"): + failed_at = group_response.get("failed-at", [0])[0] + raise AtomicTransactionComposerError( + f"Error during resource population simulation in transaction {failed_at}: " + f"{group_response['failure-message']}" + ) + + # Return resources accessed at group and transaction level + return { + "group": group_response.get("unnamed-resources-accessed", {}), + "txns": [txn.get("unnamed-resources-accessed", {}) for txn in group_response.get("txn-results", [])], + } + + +MAX_LEASE_LENGTH = 32 + + +def encode_lease(lease: str | bytes | None) -> bytes | None: + if lease is None: + return None + elif isinstance(lease, bytes): + if not (1 <= len(lease) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received bytes with length {len(lease)}" + ) + if len(lease) == MAX_LEASE_LENGTH: + return lease + lease32 = bytearray(32) + lease32[: len(lease)] = lease + return bytes(lease32) + elif isinstance(lease, str): + encoded = lease.encode("utf-8") + if not (1 <= len(encoded) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received '{lease}' with length {len(lease)}" + ) + lease32 = bytearray(MAX_LEASE_LENGTH) + lease32[: len(encoded)] = encoded + return bytes(lease32) + else: + raise TypeError(f"Unknown lease type received of {type(lease)}") diff --git a/tests/accounts/__init__.py b/tests/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py new file mode 100644 index 00000000..ec56a007 --- /dev/null +++ b/tests/accounts/test_account_manager.py @@ -0,0 +1,108 @@ +import algosdk +import pytest + +from algokit_utils import Account +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.amount import AlgoAmount +from tests.conftest import get_unique_name + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +def test_new_account_is_retrieved_and_funded(algorand: AlgorandClient) -> None: + # Act + account_name = get_unique_name() + account = algorand.account.from_environment(account_name) + + # Assert + account_info = algorand.account.get_information(account.address) + assert account_info["amount"] > 0 + + +def test_same_account_is_subsequently_retrieved(algorand: AlgorandClient) -> None: + # Arrange + account_name = get_unique_name() + + # Act + account1 = algorand.account.from_environment(account_name) + account2 = algorand.account.from_environment(account_name) + + # Assert - accounts should be different objects but with same underlying keys + assert account1 is not account2 + assert account1.address == account2.address + assert account1.private_key == account2.private_key + + +def test_environment_is_used_in_preference_to_kmd(algorand: AlgorandClient, monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + account_name = get_unique_name() + account1 = algorand.account.from_environment(account_name) + + # Set up environment variable for second account + env_account_name = "TEST_ACCOUNT" + monkeypatch.setenv(f"{env_account_name}_MNEMONIC", algosdk.mnemonic.from_private_key(account1.private_key)) + + # Act + account2 = algorand.account.from_environment(env_account_name) + + # Assert - accounts should be different objects but with same underlying keys + assert account1 is not account2 + assert account1.address == account2.address + assert account1.private_key == account2.private_key + + +def test_random_account_creation(algorand: AlgorandClient) -> None: + # Act + account = algorand.account.random() + + # Assert + assert account.address + assert account.private_key + assert len(account.public_key) == 32 + + +def test_ensure_funded_from_environment(algorand: AlgorandClient) -> None: + # Arrange + account = algorand.account.random() + min_balance = AlgoAmount.from_algos(1) + + # Act + result = algorand.account.ensure_funded_from_environment( + account_to_fund=account.address, + min_spending_balance=min_balance, + ) + + # Assert + assert result is not None + assert result.amount_funded is not None + account_info = algorand.account.get_information(account.address) + assert account_info["amount"] >= min_balance.micro_algos + + +def test_get_account_information(algorand: AlgorandClient) -> None: + # Arrange + account = algorand.account.random() + + # Act + info = algorand.account.get_information(account.address) + + # Assert + assert isinstance(info, dict) + assert "amount" in info + assert "min-balance" in info + assert "address" in info + assert info["address"] == account.address diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py new file mode 100644 index 00000000..c246924a --- /dev/null +++ b/tests/applications/test_app_client.py @@ -0,0 +1,733 @@ +import base64 +import json +import random +from pathlib import Path +from typing import Any + +import algosdk +import pytest +from algosdk.atomic_transaction_composer import TransactionSigner, TransactionWithSigner + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallWithSendParams, + AppClientParams, + FundAppAccountParams, +) +from algokit_utils.applications.app_manager import AppManager, BoxReference +from algokit_utils.applications.utils import arc32_to_arc56, get_arc56_method +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.abi import ABIType +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.application import Arc56Contract +from algokit_utils.transactions.transaction_composer import AppCreateParams, PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def raw_hello_world_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = hello_world_arc32_app_spec.global_state_schema + local_schema = hello_world_arc32_app_spec.local_state_schema + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=hello_world_arc32_app_spec.approval_program, + clear_state_program=hello_world_arc32_app_spec.clear_program, + schema={ + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def raw_testing_app_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def testing_app_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, testing_app_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_arc32_app_spec.global_state_schema + local_schema = testing_app_arc32_app_spec.local_state_schema + approval = AppManager.replace_template_variables( + testing_app_arc32_app_spec.approval_program, + { + "VALUE": 1, + "UPDATABLE": 0, + "DELETABLE": 0, + }, + ) + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval, + clear_state_program=testing_app_arc32_app_spec.clear_program, + schema={ + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_spec: ApplicationSpecification, + testing_app_arc32_app_id: int, +) -> AppClient: + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_arc32_app_id, + algorand=algorand, + app_spec=testing_app_arc32_app_spec, + ) + ) + + +@pytest.fixture +def test_app_client_with_sourcemaps( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_spec: ApplicationSpecification, + testing_app_arc32_app_id: int, +) -> AppClient: + sourcemaps = json.loads( + (Path(__file__).parent.parent / "artifacts" / "testing_app" / "sources.teal.map.json").read_text() + ) + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_arc32_app_id, + algorand=algorand, + approval_source_map=algosdk.source_map.SourceMap(sourcemaps["approvalSourceMap"]), + clear_source_map=algosdk.source_map.SourceMap(sourcemaps["clearSourceMap"]), + app_spec=testing_app_arc32_app_spec, + ) + ) + + +@pytest.fixture +def testing_app_puya_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app_puya" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_puya_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, testing_app_puya_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_puya_arc32_app_spec.global_state_schema + local_schema = testing_app_puya_arc32_app_spec.local_state_schema + + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=testing_app_puya_arc32_app_spec.approval_program, + clear_state_program=testing_app_puya_arc32_app_spec.clear_program, + schema={ + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client_puya( + algorand: AlgorandClient, + funded_account: Account, + testing_app_puya_arc32_app_spec: ApplicationSpecification, + testing_app_puya_arc32_app_id: int, +) -> AppClient: + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_puya_arc32_app_id, + algorand=algorand, + app_spec=testing_app_puya_arc32_app_spec, + ) + ) + + +# TODO: add variations around arc 56 contracts too + + +def test_clone_overriding_default_sender_and_inheriting_app_name( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_default_sender = "ABC" * 55 + cloned_app_client = app_client.clone(default_sender=cloned_default_sender) + + assert app_client.app_name == "HelloWorld" + assert cloned_app_client.app_id == app_client.app_id + assert cloned_app_client.app_name == app_client.app_name + assert cloned_app_client._default_sender == cloned_default_sender # noqa: SLF001 + assert app_client._default_sender == funded_account.address # noqa: SLF001 + + +def test_clone_overriding_app_name( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_app_name = "George CLONEy" + cloned_app_client = app_client.clone(app_name=cloned_app_name) + assert app_client.app_name == hello_world_arc32_app_spec.contract.name == "HelloWorld" + assert cloned_app_client.app_name == cloned_app_name + + +def test_clone_inheriting_app_name_based_on_default_handling( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_app_name = None + cloned_app_client = app_client.clone(app_name=cloned_app_name) + assert cloned_app_client.app_name == hello_world_arc32_app_spec.contract.name == app_client.app_name + + +def test_normalise_app_spec( + raw_hello_world_arc32_app_spec: str, + hello_world_arc32_app_spec: ApplicationSpecification, +) -> None: + normalized_app_spec_from_arc32 = AppClient.normalise_app_spec(hello_world_arc32_app_spec) + assert isinstance(normalized_app_spec_from_arc32, Arc56Contract) + + normalize_app_spec_from_raw_arc32 = AppClient.normalise_app_spec(raw_hello_world_arc32_app_spec) + assert isinstance(normalize_app_spec_from_raw_arc32, Arc56Contract) + + +def test_resolve_from_network( + algorand: AlgorandClient, + hello_world_arc32_app_id: int, + hello_world_arc32_app_spec: ApplicationSpecification, +) -> None: + arc56_app_spec = arc32_to_arc56(hello_world_arc32_app_spec) + arc56_app_spec.networks = {"localnet": {"app_id": hello_world_arc32_app_id}} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + ) + + assert app_client + + +def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: + call = test_app_client.create_transaction.call( + AppClientMethodCallWithSendParams( + method="call_abi", + args=["test"], + box_references=[BoxReference(app_id=0, name=b"1")], + ) + ) + + assert isinstance(call.transactions[0], algosdk.transaction.ApplicationCallTxn) + assert call.transactions[0].boxes == [BoxReference(app_id=0, name=b"1")] + + # Test with string box reference + call2 = test_app_client.create_transaction.call( + AppClientMethodCallWithSendParams( + method="call_abi", + args=["test"], + box_references=["1"], + ) + ) + + assert isinstance(call2.transactions[0], algosdk.transaction.ApplicationCallTxn) + assert call2.transactions[0].boxes == [BoxReference(app_id=0, name=b"1")] + + +def test_construct_transaction_with_abi_encoding_including_transaction( + algorand: AlgorandClient, funded_account: Account, test_app_client: AppClient +) -> None: + # Create a payment transaction with random amount + amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + payment_txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=amount, + ) + ) + + # Call the ABI method with the payment transaction + result = test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[payment_txn, "test"], + ) + ) + + assert result.confirmation + assert len(result.transactions) == 2 + return_value = AppManager.get_abi_return( + result.confirmation, get_arc56_method("call_abi_txn", test_app_client.app_spec) + ) + expected_return = f"Sent {amount.micro_algos}. test" + assert result.return_value + assert result.return_value.return_value == expected_return + assert return_value + assert return_value.return_value == result.return_value.return_value + + +def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + # Create a payment transaction with a random amount + amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=amount, + ) + ) + + called_indexes = [] + original_signer = algorand.account.get_signer(funded_account.address) + + class IndexCapturingSigner(TransactionSigner): + def sign_transactions( + self, txn_group: list[algosdk.transaction.Transaction], indexes: list[int] + ) -> list[algosdk.transaction.GenericSignedTransaction]: + called_indexes.extend(indexes) + return original_signer.sign_transactions(txn_group, indexes) + + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[txn, "test"], + sender=funded_account.address, + signer=IndexCapturingSigner(), + ) + ) + + assert called_indexes == [0, 1] + + +def test_sign_transaction_in_group_with_different_signer_if_provided( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + # Generate a new account + test_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(10), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + # Fund the account with 1 Algo + txn = algorand.create_transaction.payment( + PaymentParams( + sender=test_account.address, + receiver=test_account.address, + amount=AlgoAmount.from_algos(random.randint(1, 5)), + ) + ) + + # Call method with transaction and signer + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[TransactionWithSigner(txn=txn, signer=test_account.signer), "test"], + ) + ) + + +def test_construct_transaction_with_abi_encoding_including_foreign_references_not_in_signature( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + test_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(10), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + result = test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_foreign_refs", + app_references=[345], + account_references=[test_account.address], + asset_references=[567], + ) + ) + + # Assuming the method returns a string matching the format below + expected_return = AppManager.get_abi_return( + result.confirmations[0], + get_arc56_method("call_abi_foreign_refs", test_app_client.app_spec), + ) + assert result.return_value + assert "App: 345, Asset: 567, Account: " in result.return_value.return_value + assert expected_return + assert expected_return.return_value == result.return_value.return_value + + +def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: + # Test global state + test_app_client.send.call( + AppClientMethodCallWithSendParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) + ) + global_state = test_app_client.get_global_state() + + assert "int1" in global_state + assert "int2" in global_state + assert "bytes1" in global_state + assert "bytes2" in global_state + assert hasattr(global_state["bytes2"], "value_raw") + assert sorted(global_state.keys()) == ["bytes1", "bytes2", "int1", "int2", "value"] + assert global_state["int1"].value == 1 + assert global_state["int2"].value == 2 + assert global_state["bytes1"].value == "asdf" + assert global_state["bytes2"].value_raw == bytes([1, 2, 3, 4]) + + # Test local state + test_app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) + test_app_client.send.call( + AppClientMethodCallWithSendParams(method="set_local", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) + ) + local_state = test_app_client.get_local_state(funded_account.address) + + assert "local_int1" in local_state + assert "local_int2" in local_state + assert "local_bytes1" in local_state + assert "local_bytes2" in local_state + assert sorted(local_state.keys()) == ["local_bytes1", "local_bytes2", "local_int1", "local_int2"] + assert local_state["local_int1"].value == 1 + assert local_state["local_int2"].value == 2 + assert local_state["local_bytes1"].value == "asdf" + assert local_state["local_bytes2"].value_raw == bytes([1, 2, 3, 4]) + + # Test box storage + box_name1 = bytes([0, 0, 0, 1]) + box_name1_base64 = base64.b64encode(box_name1).decode() + box_name2 = bytes([0, 0, 0, 2]) + box_name2_base64 = base64.b64encode(box_name2).decode() + + test_app_client.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name1, "value1"], + box_references=[box_name1], + ) + ) + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name2, "value2"], + box_references=[box_name2], + ) + ) + + box_values = test_app_client.get_box_values() + box1_value = test_app_client.get_box_value(box_name1) + + assert sorted(b.name.name_base64 for b in box_values) == sorted([box_name1_base64, box_name2_base64]) + box1 = next(b for b in box_values if b.name.name_base64 == box_name1_base64) + assert box1.value == base64.b64encode(bytes("value1", "utf-8")) + assert box1_value == box1.value + + box2 = next(b for b in box_values if b.name.name_base64 == box_name2_base64) + assert box2.value == base64.b64encode(bytes("value2", "utf-8")) + + # Legacy contract strips ABI prefix; manually encoded ABI string after + # passing algosdk's atc results in \x00\n\x00\n1234524352. + expected_value_decoded = "1234524352" + expected_value = "\x00\n" + expected_value_decoded + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name1, expected_value], + box_references=[box_name1], + ) + ) + + boxes = test_app_client.get_box_values_from_abi_type( + ABIType.from_string("string"), + lambda n: n.name_base64 == box_name1_base64, + ) + box1_abi_value = test_app_client.get_box_value_from_abi_type(box_name1, ABIType.from_string("string")) + + assert len(boxes) == 1 + assert boxes[0].value == expected_value_decoded + assert box1_abi_value == expected_value_decoded + + +@pytest.mark.parametrize( + ("box_name", "box_value", "value_type", "expected_value"), + [ + ( + "name1", + b"test_bytes", # Updated to match Bytes type + "byte[]", + [116, 101, 115, 116, 95, 98, 121, 116, 101, 115], + ), + ( + "name2", + "test_string", + "string", + "test_string", + ), + ( + "name3", # Updated to use string key + 123, + "uint32", + 123, + ), + ( + "name4", # Updated to use string key + 2**256, # Large number within uint512 range + "uint512", + 2**256, + ), + ( + "name5", # Updated to use string key + [1, 2, 3, 4], + "byte[4]", + [1, 2, 3, 4], + ), + ], +) +def test_box_methods_with_manually_encoded_abi_args( + test_app_client_puya: AppClient, + box_name: Any, # noqa: ANN401 + box_value: Any, # noqa: ANN401 + value_type: str, + expected_value: Any, # noqa: ANN401 +) -> None: + # Fund the app account + box_prefix = b"box_bytes" + + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + # Encode the box reference + box_identifier = box_prefix + ABIType.from_string("string").encode(box_name) + + # Call the method to set the box value + test_app_client_puya.send.call( + AppClientMethodCallWithSendParams( + method="set_box_bytes", + args=[box_name, ABIType.from_string(value_type).encode(box_value)], + box_references=[box_identifier], + ) + ) + + # Get and verify the box value + box_abi_value = test_app_client_puya.get_box_value_from_abi_type(box_identifier, ABIType.from_string(value_type)) + + # Convert the retrieved value to match expected type if needed + assert box_abi_value == expected_value + + +@pytest.mark.parametrize( + ("box_prefix_str", "method", "arg_value", "value_type"), + [ + ("box_str", "set_box_str", "string", "string"), + ("box_int", "set_box_int", 123, "uint32"), + ("box_int512", "set_box_int512", 2**256, "uint512"), + ("box_static", "set_box_static", [1, 2, 3, 4], "byte[4]"), + ("", "set_struct", ("box1", 123), "(string,uint64)"), + ], +) +def test_box_methods_with_arc4_returns_parametrized( + test_app_client_puya: AppClient, + box_prefix_str: str, + method: str, + arg_value: Any, # noqa: ANN401 + value_type: str, +) -> None: + """ + Test setting and retrieving box values with different data types and box prefixes. + + Args: + test_app_client_puya (AppClient): The AppClient instance for testing. + box_prefix_str (str): The string prefix for the box. + method (str): The method name to call for setting the box. + arg_value (Any): The value to set in the box. + value_type (str): The ABI type of the value. + """ + # Encode the box prefix + box_prefix = box_prefix_str.encode() + + # Fund the app account with 1 Algo + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + # Encode the box name "box1" using ABIType "string" + box_name_encoded = ABIType.from_string("string").encode("box1") + box_reference = box_prefix + box_name_encoded + + # Send the transaction to set the box value + test_app_client_puya.send.call( + AppClientMethodCallWithSendParams( + method=method, + args=["box1", arg_value], + box_references=[box_reference], + ) + ) + + # Encode the expected value using the specified ABI type + value_encoded = ABIType.from_string(value_type).encode(arg_value) + expected_value = base64.b64encode(value_encoded) + + # Retrieve the actual box value + actual_box_value = test_app_client_puya.get_box_value(box_reference) + + # Assert that the actual box value matches the expected value + assert actual_box_value == expected_value + + if method == "set_struct": + abi_decoded_boxes = test_app_client_puya.get_box_values_from_abi_type( + ABIType.from_string("(string,uint64)"), + lambda n: n.name_base64 == base64.b64encode(box_prefix + box_name_encoded).decode(), + ) + assert len(abi_decoded_boxes) == 1 + assert abi_decoded_boxes[0].value == arg_value + + +# TODO: see if needs moving into app factory tests file +def test_abi_with_default_arg_method( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_id: int, + testing_app_arc32_app_spec: ApplicationSpecification, +) -> None: + arc56_app_spec = arc32_to_arc56(testing_app_arc32_app_spec) + arc56_app_spec.networks = {"localnet": {"app_id": testing_app_arc32_app_id}} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + default_sender=funded_account.address, + default_signer=funded_account.signer, + ) + # app_client.send. + app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) + app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_local", + args=[1, 2, "banana", [1, 2, 3, 4]], + ) + ) + + method_signature = "default_value_from_local_state(string)string" + defined_value = "defined value" + + # Test with defined value + defined_value_result = app_client.send.call( + AppClientMethodCallWithSendParams(method=method_signature, args=[defined_value]) + ) + + assert defined_value_result.return_value + assert defined_value_result.return_value.return_value == "Local state, defined value" + + # Test with default value + default_value_result = app_client.send.call(AppClientMethodCallWithSendParams(method=method_signature, args=[None])) + assert default_value_result.return_value + assert default_value_result.return_value.return_value == "Local state, banana" + + +def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: + with pytest.raises(LogicError) as exc_info: + test_app_client_with_sourcemaps.send.call(AppClientMethodCallWithSendParams(method="error")) + + error = exc_info.value + assert error.pc == 885 + assert "assert failed pc=885" in str(error) + assert len(error.transaction_id) == 52 + assert error.line_no == 469 diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py new file mode 100644 index 00000000..8cf9e75a --- /dev/null +++ b/tests/applications/test_app_factory.py @@ -0,0 +1,488 @@ +from pathlib import Path + +import algosdk +import pytest +from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete + +from algokit_utils import OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallParams, + AppClientMethodCallWithCompilationAndSendParams, + AppClientMethodCallWithSendParams, + AppClientParams, +) +from algokit_utils.applications.app_factory import ( + AppFactory, + AppFactoryCreateMethodCallParams, + AppFactoryCreateWithSendParams, +) +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def app_spec() -> str: + return (Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json").read_text() + + +@pytest.fixture +def factory(algorand: AlgorandClient, funded_account: Account, app_spec: str) -> AppFactory: + """Create AppFactory fixture""" + return algorand.client.get_app_factory(app_spec=app_spec, default_sender=funded_account.address) + + +@pytest.fixture +def arc56_factory( + algorand: AlgorandClient, + funded_account: Account, +) -> AppFactory: + """Create AppFactory fixture""" + arc56_raw_spec = ( + Path(__file__).parent.parent / "artifacts" / "testing_app_arc56" / "arc56_app_spec.json" + ).read_text() + return algorand.client.get_app_factory(app_spec=arc56_raw_spec, default_sender=funded_account.address) + + +def test_create_app(factory: AppFactory) -> None: + """Test creating an app using the factory""" + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + } + ) + ) + + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + assert result.compiled_approval is not None + assert result.compiled_clear is not None + + +def test_create_app_with_constructor_deploy_time_params(algorand: AlgorandClient, app_spec: str) -> None: + """Test creating an app using the factory with constructor deploy time params""" + random_account = algorand.account.random() + dispenser_account = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + account_to_fund=random_account, + dispenser_account=dispenser_account.address, + min_spending_balance=AlgoAmount.from_algo(10), + min_funding_increment=AlgoAmount.from_algo(1), + ) + + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=random_account.address, + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + ) + + app_client, result = factory.send.bare.create() + + assert result.app_id > 0 + assert app_client.app_id == result.app_id + + +def test_create_app_with_oncomplete_overload(factory: AppFactory) -> None: + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + on_complete=OnComplete.OptInOC, + updatable=True, + deletable=True, + deploy_time_params={ + "VALUE": 1, + }, + ) + ) + + assert result.transaction.application_call + assert result.transaction.application_call.on_complete == OnComplete.OptInOC + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + + +def test_deploy_when_immutable_and_permanent(factory: AppFactory) -> None: + factory.deploy( + deletable=False, + updatable=False, + on_schema_break=OnSchemaBreak.Fail, + on_update=OnUpdate.Fail, + deploy_time_params={ + "VALUE": 1, + }, + ) + + +def test_deploy_app_create(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_create_abi(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_update(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + updatable=True, + ) + + _, updated_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.UpdateApp, + ) + + assert updated_app.operation_performed == OperationPerformed.Update + assert created_app.app_id == updated_app.app_id + assert created_app.app_address == updated_app.app_address + assert created_app.confirmation + assert created_app.updatable + assert created_app.updatable == updated_app.updatable + assert created_app.updated_round != updated_app.updated_round + assert created_app.created_round == updated_app.created_round + assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] + + +def test_deploy_app_update_abi(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + updatable=True, + ) + + _, updated_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.UpdateApp, + update_params=AppClientMethodCallParams(method="update_abi", args=["args_io"]), + ) + + assert updated_app.operation_performed == OperationPerformed.Update + assert updated_app.app_id == created_app.app_id + assert updated_app.app_address == created_app.app_address + assert updated_app.confirmation is not None + assert updated_app.created_round == created_app.created_round + assert updated_app.updated_round != updated_app.created_round + assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] + assert updated_app.transaction.application_call + assert updated_app.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC + assert updated_app.return_value == "args_io" + + +def test_deploy_app_replace(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + deletable=True, + ) + + _, replaced_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.ReplaceApp, + ) + + assert replaced_app.operation_performed == OperationPerformed.Replace + assert replaced_app.app_id > created_app.app_id + assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) + assert replaced_app.confirmation is not None + assert replaced_app.delete_result is not None + assert replaced_app.delete_result.confirmation is not None + assert len(replaced_app.transactions) == 2 + assert replaced_app.delete_result.transaction.application_call + assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id + assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + + +def test_deploy_app_replace_abi(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + deletable=True, + populate_app_call_resources=False, + ) + + _, replaced_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.ReplaceApp, + create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + delete_params=AppClientMethodCallParams(method="delete_abi", args=["arg2_io"]), + ) + + assert replaced_app.operation_performed == OperationPerformed.Replace + assert replaced_app.app_id > created_app.app_id + assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) + assert replaced_app.confirmation is not None + assert replaced_app.delete_result is not None + assert replaced_app.delete_result.confirmation is not None + assert len(replaced_app.transactions) == 2 + assert replaced_app.delete_result.transaction.application_call + assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id + assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + assert replaced_app.return_value == "arg_io" + assert replaced_app.delete_return_value == "arg2_io" + + +def test_create_then_call_app(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 1, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + call = app_client.send.call(AppClientMethodCallWithSendParams(method="call_abi", args=["test"])) + + assert call.return_value + assert call.return_value.return_value == "Hello, test" + + +def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, factory: AppFactory) -> None: + rekey_to = algorand.account.random() + + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 1, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in", rekey_to=rekey_to.address)) + + # If the rekey didn't work this will throw + rekeyed_account = algorand.account.rekeyed(funded_account.address, rekey_to) + algorand.send.payment( + PaymentParams(amount=AlgoAmount.from_algo(0), sender=rekeyed_account.address, receiver=funded_account.address) + ) + + +def test_create_app_with_abi(factory: AppFactory) -> None: + _, call_return = factory.send.create( + AppFactoryCreateMethodCallParams( + method="create_abi", + args=["string_io"], + deploy_time_params={ + "UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + ) + ) + + assert call_return.return_value + # Fix return value issues + assert call_return.return_value.return_value == "string_io" + + +def test_update_app_with_abi(factory: AppFactory) -> None: + deploy_time_params = { + "UPDATABLE": 1, + "DELETABLE": 0, + "VALUE": 1, + } + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params=deploy_time_params, + ) + ) + + call_return = app_client.send.update( + AppClientMethodCallWithCompilationAndSendParams( + method="update_abi", + args=["string_io"], + deploy_time_params=deploy_time_params, + ) + ) + + assert call_return.return_value is not None + assert call_return.return_value.return_value == "string_io" + # TODO: fix this + # assert call_return.compiled_approval is not None + + +def test_delete_app_with_abi(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 0, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + call_return = app_client.send.delete( + AppClientMethodCallWithSendParams( + method="delete_abi", + args=["string_io"], + ) + ) + + assert call_return.return_value is not None + assert call_return.return_value.return_value == "string_io" + + +def test_export_import_sourcemaps( + factory: AppFactory, + algorand: AlgorandClient, + funded_account: Account, +) -> None: + # Export source maps from original client + client, app = factory.deploy(deploy_time_params={"VALUE": 1}) + old_sourcemaps = client.export_source_maps() + + # Create new client instance + new_client = AppClient( + AppClientParams( + app_id=app.app_id, + default_sender=funded_account.address, + default_signer=funded_account.signer, + algorand=algorand, + app_spec=client.app_spec, + ) + ) + + # Test error handling before importing source maps + with pytest.raises(LogicError) as exc_info: + new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + + assert "assert failed" in exc_info.value.message + + # Import source maps into new client + new_client.import_source_maps(old_sourcemaps) + + # Test error handling after importing source maps + with pytest.raises(LogicError) as exc_info: + new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + + error = exc_info.value + assert ( + error.trace().strip() + == "// error\n\terror_7:\n\tproto 0 0\n\tintc_0 // 0\n\t// Deliberate error\n\tassert\t\t<-- Error\n\tretsub\n\t\n\t// create\n\tcreate_8:" # noqa: E501 + ) + assert error.pc == 885 + assert error.message == "assert failed pc=885" + assert len(error.transaction_id) == 52 + + +def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( + arc56_factory: AppFactory, +) -> None: + client, _ = arc56_factory.deploy( + create_params=AppClientMethodCallParams(method="createApplication"), + deploy_time_params={ + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 123, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + ) + + with pytest.raises(Exception, match="this is an error"): + client.send.call(AppClientMethodCallWithSendParams(method="throwError")) + + +def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( + arc56_factory: AppFactory, + algorand: AlgorandClient, + funded_account: Account, +) -> None: + # Deploy app with template parameters + client, result = arc56_factory.deploy( + create_params=AppClientMethodCallParams(method="createApplication"), + deploy_time_params={ + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 0, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + ) + app_id = result.app_id + + # Create new client without source map from compilation + app_client = AppClient( + AppClientParams( + app_id=app_id, + default_sender=funded_account.address, + default_signer=funded_account.signer, + algorand=algorand, + app_spec=client.app_spec, + ) + ) + + # Test error handling + with pytest.raises(LogicError) as exc_info: + app_client.send.call(AppClientMethodCallWithSendParams(method="tmpl")) + + assert ( + exc_info.value.trace().strip() + == "// tests/example-contracts/arc56_templates/templates.algo.ts:14\n\t\t// assert(this.uint64TmplVar)\n\t\tintc 1 // TMPL_uint64TmplVar\n\t\tassert\n\t\tretsub\t\t<-- Error\n\t\n\t// specificLengthTemplateVar()void\n\t*abi_route_specificLengthTemplateVar:\n\t\t// execute specificLengthTemplateVar()void" # noqa: E501 + ) diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index 8c9c1002..57313d31 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -1,16 +1,26 @@ import pytest + from algokit_utils.applications.app_manager import AppManager from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account - +from algokit_utils.models.amount import AlgoAmount from tests.conftest import check_output_stability -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account def test_template_substitution() -> None: diff --git a/tests/applications/test_utils.py b/tests/applications/test_utils.py new file mode 100644 index 00000000..4806216c --- /dev/null +++ b/tests/applications/test_utils.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from algokit_utils.applications.utils import arc32_to_arc56 +from tests.utils import load_arc32_spec + +TEST_ARC32_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + + +def test_arc32_to_arc56() -> None: + arc32_app_spec = load_arc32_spec( + TEST_ARC32_SPEC_FILE_PATH, deletable=True, updatable=True, template_values={"VERSION": 1} + ) + + arc56_app_spec = arc32_to_arc56(arc32_app_spec) + + assert arc56_app_spec diff --git a/tests/transactions/artifacts/hello_world/approval.teal b/tests/artifacts/hello_world/approval.teal similarity index 100% rename from tests/transactions/artifacts/hello_world/approval.teal rename to tests/artifacts/hello_world/approval.teal diff --git a/tests/artifacts/hello_world/arc32_app_spec.json b/tests/artifacts/hello_world/arc32_app_spec.json new file mode 100644 index 00000000..d84bc32c --- /dev/null +++ b/tests/artifacts/hello_world/arc32_app_spec.json @@ -0,0 +1,55 @@ +{ + "hints": { + "hello(string)string": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "HelloWorld", + "methods": [ + { + "name": "hello", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "readonly": false, + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} diff --git a/tests/transactions/artifacts/hello_world/clear.teal b/tests/artifacts/hello_world/clear.teal similarity index 100% rename from tests/transactions/artifacts/hello_world/clear.teal rename to tests/artifacts/hello_world/clear.teal diff --git a/tests/artifacts/legacy_hello_world/arc32_app_spec.json b/tests/artifacts/legacy_hello_world/arc32_app_spec.json new file mode 100644 index 00000000..1ddf81b2 --- /dev/null +++ b/tests/artifacts/legacy_hello_world/arc32_app_spec.json @@ -0,0 +1,378 @@ +{ + "hints": { + "version()uint64": { + "call_config": { + "no_op": "CALL" + } + }, + "readonly(uint64)void": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "set_box(byte[4],string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box(byte[4])string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box_readonly(byte[4])string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "update()void": { + "call_config": { + "update_application": "CALL" + } + }, + "update_args(string)void": { + "call_config": { + "update_application": "CALL" + } + }, + "delete()void": { + "call_config": { + "delete_application": "CALL" + } + }, + "delete_args(string)void": { + "call_config": { + "delete_application": "CALL" + } + }, + "create_opt_in()void": { + "call_config": { + "opt_in": "CREATE" + } + }, + "update_greeting(string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "create()void": { + "call_config": { + "no_op": "CREATE" + } + }, + "create_args(string)void": { + "call_config": { + "no_op": "CREATE" + } + }, + "hello(string)string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "hello_remember(string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_last()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "opt_in_args(string)void": { + "call_config": { + "opt_in": "CALL" + } + }, + "close_out()void": { + "call_config": { + "close_out": "CALL" + } + }, + "close_out_args(string)void": { + "call_config": { + "close_out": "CALL" + } + }, + "call_with_payment(pay)string": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4=" + }, + "state": { + "global": { + "num_byte_slices": 1, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": { + "greeting": { + "type": "bytes", + "key": "greeting", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "last": { + "type": "bytes", + "key": "last", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "HelloWorldApp", + "methods": [ + { + "name": "version", + "args": [], + "returns": { + "type": "uint64" + } + }, + { + "name": "readonly", + "args": [ + { + "type": "uint64", + "name": "error" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "get_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_box_readonly", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "update", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "delete", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "delete_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create_opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_greeting", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "create_args", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "hello", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "hello_remember", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_last", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "opt_in_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "close_out", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "close_out_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "call_with_payment", + "args": [ + { + "type": "pay", + "name": "payment" + } + ], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "close_out": "CALL", + "delete_application": "CALL", + "no_op": "CREATE", + "opt_in": "CALL", + "update_application": "CALL" + } +} diff --git a/tests/artifacts/testing_app/arc32_app_spec.json b/tests/artifacts/testing_app/arc32_app_spec.json new file mode 100644 index 00000000..c308fc12 --- /dev/null +++ b/tests/artifacts/testing_app/arc32_app_spec.json @@ -0,0 +1,400 @@ +{ + "hints": { + "call_abi(string)string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "call_abi_txn(pay,string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "call_abi_foreign_refs()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "set_global(uint64,uint64,string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_local(uint64,uint64,string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box(byte[4],string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "error()void": { + "call_config": { + "no_op": "CALL" + } + }, + "create_abi(string)string": { + "call_config": { + "no_op": "CREATE" + } + }, + "update_abi(string)string": { + "call_config": { + "update_application": "CALL" + } + }, + "delete_abi(string)string": { + "call_config": { + "delete_application": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "default_value(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "constant", + "data": "default value" + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_abi(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "abi-method", + "data": { + "name": "default_value", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + } + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_global_state(uint64)uint64": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "global-state", + "data": "int1" + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_local_state(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "local-state", + "data": "local_bytes1" + } + }, + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMCA1IFRNUExfVVBEQVRBQkxFIFRNUExfREVMRVRBQkxFCmJ5dGVjYmxvY2sgMHggMHgxNTFmN2M3NQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhmMTdlODBhNSAvLyAiY2FsbF9hYmkoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMxCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MGE5MmE4MWUgLy8gImNhbGxfYWJpX3R4bihwYXksc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YWQ3NTYwMmMgLy8gImNhbGxfYWJpX2ZvcmVpZ25fcmVmcygpc3RyaW5nIgo9PQpibnogbWFpbl9sMjkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGNmOGRlYSAvLyAic2V0X2dsb2JhbCh1aW50NjQsdWludDY0LHN0cmluZyxieXRlWzRdKXZvaWQiCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGNlYzI4MzRhIC8vICJzZXRfbG9jYWwodWludDY0LHVpbnQ2NCxzdHJpbmcsYnl0ZVs0XSl2b2lkIgo9PQpibnogbWFpbl9sMjcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGI0YTIzMCAvLyAic2V0X2JveChieXRlWzRdLHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0NGQwZGEwZCAvLyAiZXJyb3IoKXZvaWQiCj09CmJueiBtYWluX2wyNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDlkNTIzMDQwIC8vICJjcmVhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyNAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDNjYTVjZWI3IC8vICJ1cGRhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI3MWI0ZWU5IC8vICJkZWxldGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU3NGI1NWM4IC8vICJkZWZhdWx0X3ZhbHVlKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDQ2ZDIxMWEzIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDBjZmNiYjAwIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlKHVpbnQ2NCl1aW50NjQiCj09CmJueiBtYWluX2wxOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQwZjBiYWY4IC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDE3CmVycgptYWluX2wxNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlY2FzdGVyXzMyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVjYXN0ZXJfMzAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBvcHRpbmNhc3Rlcl8yOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWFiaWNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjM6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFiaWNhc3Rlcl8yNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYWJpY2FzdGVyXzI2CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBlcnJvcmNhc3Rlcl8yNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Ym94Y2FzdGVyXzI0CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRsb2NhbGNhc3Rlcl8yMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Z2xvYmFsY2FzdGVyXzIyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYml0eG5jYXN0ZXJfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYmljYXN0ZXJfMTkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w0MAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sMzkKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDM4CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wzNwplcnIKbWFpbl9sMzc6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGRlbGV0ZV8xMgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzg6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIHVwZGF0ZV8xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzk6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydApjYWxsc3ViIGNyZWF0ZV84CmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzgKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjYWxsX2FiaQpjYWxsYWJpXzA6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyYzIwIC8vICJIZWxsbywgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaXRvYQppdG9hXzE6CnByb3RvIDEgMQpmcmFtZV9kaWcgLTEKaW50Y18wIC8vIDAKPT0KYm56IGl0b2FfMV9sNQpmcmFtZV9kaWcgLTEKaW50Y18yIC8vIDEwCi8KaW50Y18wIC8vIDAKPgpibnogaXRvYV8xX2w0CmJ5dGVjXzAgLy8gIiIKaXRvYV8xX2wzOgpwdXNoYnl0ZXMgMHgzMDMxMzIzMzM0MzUzNjM3MzgzOSAvLyAiMDEyMzQ1Njc4OSIKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmludGNfMSAvLyAxCmV4dHJhY3QzCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gY2FsbF9hYmlfdHhuCmNhbGxhYml0eG5fMjoKcHJvdG8gMiAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NTM2NTZlNzQyMCAvLyAiU2VudCAiCmZyYW1lX2RpZyAtMgpndHhucyBBbW91bnQKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJlMjAgLy8gIi4gIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGNhbGxfYWJpX2ZvcmVpZ25fcmVmcwpjYWxsYWJpZm9yZWlnbnJlZnNfMzoKcHJvdG8gMCAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE3MDcwM2EyMCAvLyAiQXBwOiAiCnR4bmEgQXBwbGljYXRpb25zIDEKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJjMjA0MTczNzM2NTc0M2EyMCAvLyAiLCBBc3NldDogIgpjb25jYXQKdHhuYSBBc3NldHMgMApjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4MmMyMDQxNjM2MzZmNzU2ZTc0M2EyMCAvLyAiLCBBY2NvdW50OiAiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18wIC8vIDAKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4M2EgLy8gIjoiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18xIC8vIDEKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBzZXRfZ2xvYmFsCnNldGdsb2JhbF80Ogpwcm90byA0IDAKcHVzaGJ5dGVzIDB4Njk2ZTc0MzEgLy8gImludDEiCmZyYW1lX2RpZyAtNAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg2OTZlNzQzMiAvLyAiaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDYyNzk3NDY1NzMzMSAvLyAiYnl0ZXMxIgpmcmFtZV9kaWcgLTIKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcHVzaGJ5dGVzIDB4NjI3OTc0NjU3MzMyIC8vICJieXRlczIiCmZyYW1lX2RpZyAtMQphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHNldF9sb2NhbApzZXRsb2NhbF81Ogpwcm90byA0IDAKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMSAvLyAibG9jYWxfaW50MSIKZnJhbWVfZGlnIC00CmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMiAvLyAibG9jYWxfaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzEgLy8gImxvY2FsX2J5dGVzMSIKZnJhbWVfZGlnIC0yCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzIgLy8gImxvY2FsX2J5dGVzMiIKZnJhbWVfZGlnIC0xCmFwcF9sb2NhbF9wdXQKcmV0c3ViCgovLyBzZXRfYm94CnNldGJveF82Ogpwcm90byAyIDAKZnJhbWVfZGlnIC0yCmJveF9kZWwKcG9wCmZyYW1lX2RpZyAtMgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYm94X3B1dApyZXRzdWIKCi8vIGVycm9yCmVycm9yXzc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAovLyBEZWxpYmVyYXRlIGVycm9yCmFzc2VydApyZXRzdWIKCi8vIGNyZWF0ZQpjcmVhdGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGJ5dGVzIDB4NzY2MTZjNzU2NSAvLyAidmFsdWUiCnB1c2hpbnQgVE1QTF9WQUxVRSAvLyBUTVBMX1ZBTFVFCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2FiaQpjcmVhdGVhYmlfOToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB1cGRhdGUKdXBkYXRlXzEwOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKcmV0c3ViCgovLyB1cGRhdGVfYWJpCnVwZGF0ZWFiaV8xMToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8xMjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FiaQpkZWxldGVhYmlfMTM6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xNDoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gZGVmYXVsdF92YWx1ZQpkZWZhdWx0dmFsdWVfMTU6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9hYmkKZGVmYXVsdHZhbHVlZnJvbWFiaV8xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE0MjQ5MmMyMCAvLyAiQUJJLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZV8xNzoKcHJvdG8gMSAxCmludGNfMCAvLyAwCmZyYW1lX2RpZyAtMQpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUKZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTg6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDRjNmY2MzYxNmMyMDczNzQ2MTc0NjUyYzIwIC8vICJMb2NhbCBzdGF0ZSwgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gY2FsbF9hYmlfY2FzdGVyCmNhbGxhYmljYXN0ZXJfMTk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGxhYmlfMApmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBjYWxsX2FiaV90eG5fY2FzdGVyCmNhbGxhYml0eG5jYXN0ZXJfMjA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmludGNfMCAvLyAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDIKdHhuIEdyb3VwSW5kZXgKaW50Y18xIC8vIDEKLQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKY2FsbHN1YiBjYWxsYWJpdHhuXzIKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gY2FsbF9hYmlfZm9yZWlnbl9yZWZzX2Nhc3RlcgpjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjE6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmNhbGxzdWIgY2FsbGFiaWZvcmVpZ25yZWZzXzMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gc2V0X2dsb2JhbF9jYXN0ZXIKc2V0Z2xvYmFsY2FzdGVyXzIyOgpwcm90byAwIDAKaW50Y18wIC8vIDAKZHVwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKYnRvaQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpidG9pCmZyYW1lX2J1cnkgMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmZyYW1lX2J1cnkgMgp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpmcmFtZV9kaWcgMgpmcmFtZV9kaWcgMwpjYWxsc3ViIHNldGdsb2JhbF80CnJldHN1YgoKLy8gc2V0X2xvY2FsX2Nhc3RlcgpzZXRsb2NhbGNhc3Rlcl8yMzoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpmcmFtZV9idXJ5IDEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpmcmFtZV9idXJ5IDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgNApmcmFtZV9idXJ5IDMKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKZnJhbWVfZGlnIDMKY2FsbHN1YiBzZXRsb2NhbF81CnJldHN1YgoKLy8gc2V0X2JveF9jYXN0ZXIKc2V0Ym94Y2FzdGVyXzI0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKY2FsbHN1YiBzZXRib3hfNgpyZXRzdWIKCi8vIGVycm9yX2Nhc3RlcgplcnJvcmNhc3Rlcl8yNToKcHJvdG8gMCAwCmNhbGxzdWIgZXJyb3JfNwpyZXRzdWIKCi8vIGNyZWF0ZV9hYmlfY2FzdGVyCmNyZWF0ZWFiaWNhc3Rlcl8yNjoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgY3JlYXRlYWJpXzkKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gdXBkYXRlX2FiaV9jYXN0ZXIKdXBkYXRlYWJpY2FzdGVyXzI3Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiB1cGRhdGVhYmlfMTEKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVsZXRlX2FiaV9jYXN0ZXIKZGVsZXRlYWJpY2FzdGVyXzI4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWxldGVhYmlfMTMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl8yOToKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTQKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Nhc3RlcgpkZWZhdWx0dmFsdWVjYXN0ZXJfMzA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGRlZmF1bHR2YWx1ZV8xNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpXzE2CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9nbG9iYWxfc3RhdGVfY2FzdGVyCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZWNhc3Rlcl8zMjoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlXzE3CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKaXRvYgpjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVmYXVsdF92YWx1ZV9mcm9tX2xvY2FsX3N0YXRlX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMzoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1Yg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" + }, + "state": { + "global": { + "num_byte_slices": 2, + "num_uints": 3 + }, + "local": { + "num_byte_slices": 2, + "num_uints": 2 + } + }, + "schema": { + "global": { + "declared": { + "bytes1": { + "type": "bytes", + "key": "bytes1", + "descr": "" + }, + "bytes2": { + "type": "bytes", + "key": "bytes2", + "descr": "" + }, + "int1": { + "type": "uint64", + "key": "int1", + "descr": "" + }, + "int2": { + "type": "uint64", + "key": "int2", + "descr": "" + }, + "value": { + "type": "uint64", + "key": "value", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "local_bytes1": { + "type": "bytes", + "key": "local_bytes1", + "descr": "" + }, + "local_bytes2": { + "type": "bytes", + "key": "local_bytes2", + "descr": "" + }, + "local_int1": { + "type": "uint64", + "key": "local_int1", + "descr": "" + }, + "local_int2": { + "type": "uint64", + "key": "local_int2", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "TestingApp", + "methods": [ + { + "name": "call_abi", + "args": [ + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "call_abi_txn", + "args": [ + { + "type": "pay", + "name": "txn" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "call_abi_foreign_refs", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "set_global", + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_local", + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "error", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "create_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "update_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "delete_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "default_value", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "default_value_from_abi", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "default_value_from_global_state", + "args": [ + { + "type": "uint64", + "name": "arg_with_default" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "default_value_from_local_state", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "delete_application": "CALL", + "no_op": "CREATE", + "opt_in": "CREATE", + "update_application": "CALL" + } +} diff --git a/tests/artifacts/testing_app/contract.py b/tests/artifacts/testing_app/contract.py new file mode 100644 index 00000000..95159cbc --- /dev/null +++ b/tests/artifacts/testing_app/contract.py @@ -0,0 +1,185 @@ +from typing import Literal + +import beaker +import pyteal as pt +from beaker.lib.storage import BoxMapping +from pyteal.ast import CallConfig, MethodConfig + +UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" +DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" + + +class BareCallAppState: + value = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + bytes1 = beaker.GlobalStateValue(stack_type=pt.TealType.bytes) + bytes2 = beaker.GlobalStateValue(stack_type=pt.TealType.bytes) + int1 = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + int2 = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + local_bytes1 = beaker.LocalStateValue(stack_type=pt.TealType.bytes) + local_bytes2 = beaker.LocalStateValue(stack_type=pt.TealType.bytes) + local_int1 = beaker.LocalStateValue(stack_type=pt.TealType.uint64) + local_int2 = beaker.LocalStateValue(stack_type=pt.TealType.uint64) + box = BoxMapping(pt.abi.StaticBytes[Literal[4]], pt.abi.String) + + +app = beaker.Application("TestingApp", state=BareCallAppState) + + +@app.external(read_only=True) +def call_abi(value: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("Hello, "), value.get())) + + +# https://github.com/algorand/pyteal-utils/blob/main/pytealutils/strings/string.py#L63 +@pt.Subroutine(pt.TealType.bytes) +def itoa(i: pt.Expr) -> pt.Expr: + """itoa converts an integer to the ascii byte string it represents""" + return pt.If( + i == pt.Int(0), + pt.Bytes("0"), + pt.Concat( + pt.If(i / pt.Int(10) > pt.Int(0), itoa(i / pt.Int(10)), pt.Bytes("")), + pt.Extract(pt.Bytes("0123456789"), i % pt.Int(10), pt.Int(1)), + ), + ) + + +@app.external() +def call_abi_txn(txn: pt.abi.PaymentTransaction, value: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set( + pt.Concat( + pt.Bytes("Sent "), + itoa(txn.get().amount()), + pt.Bytes(". "), + value.get(), + ) + ) + + +@app.external(read_only=True) +def call_abi_foreign_refs(*, output: pt.abi.String) -> pt.Expr: + return output.set( + pt.Concat( + pt.Bytes("App: "), + itoa(pt.Txn.applications[1]), + pt.Bytes(", Asset: "), + itoa(pt.Txn.assets[0]), + pt.Bytes(", Account: "), + itoa(pt.GetByte(pt.Txn.accounts[0], pt.Int(0))), + pt.Bytes(":"), + itoa(pt.GetByte(pt.Txn.accounts[0], pt.Int(1))), + ) + ) + + +@app.external() +def set_global( + int1: pt.abi.Uint64, int2: pt.abi.Uint64, bytes1: pt.abi.String, bytes2: pt.abi.StaticBytes[Literal[4]] +) -> pt.Expr: + return pt.Seq( + app.state.int1.set(int1.get()), + app.state.int2.set(int2.get()), + app.state.bytes1.set(bytes1.get()), + app.state.bytes2.set(bytes2.get()), + ) + + +@app.external() +def set_local( + int1: pt.abi.Uint64, int2: pt.abi.Uint64, bytes1: pt.abi.String, bytes2: pt.abi.StaticBytes[Literal[4]] +) -> pt.Expr: + return pt.Seq( + app.state.local_int1.set(int1.get()), + app.state.local_int2.set(int2.get()), + app.state.local_bytes1.set(bytes1.get()), + app.state.local_bytes2.set(bytes2.get()), + ) + + +@app.external() +def set_box(name: pt.abi.StaticBytes[Literal[4]], value: pt.abi.String) -> pt.Expr: + return app.state.box[name.get()].set(value.get()) + + +@app.external() +def error() -> pt.Expr: + return pt.Assert(pt.Int(0), comment="Deliberate error") + + +@app.external( + authorize=beaker.Authorize.only_creator(), + bare=True, + method_config=MethodConfig(no_op=CallConfig.CREATE, opt_in=CallConfig.CREATE), +) +def create() -> pt.Expr: + return app.state.value.set(pt.Tmpl.Int("TMPL_VALUE")) + + +@app.create(authorize=beaker.Authorize.only_creator()) +def create_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set(input.get()) + + +@app.update(authorize=beaker.Authorize.only_creator(), bare=True) +def update() -> pt.Expr: + return pt.Assert(pt.Tmpl.Int(UPDATABLE_TEMPLATE_NAME), comment="Check app is updatable") + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return pt.Seq( + pt.Assert(pt.Tmpl.Int(UPDATABLE_TEMPLATE_NAME), comment="Check app is updatable"), output.set(input.get()) + ) + + +@app.delete(authorize=beaker.Authorize.only_creator(), bare=True) +def delete() -> pt.Expr: + return pt.Assert(pt.Tmpl.Int(DELETABLE_TEMPLATE_NAME), comment="Check app is deletable") + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return pt.Seq( + pt.Assert(pt.Tmpl.Int(DELETABLE_TEMPLATE_NAME), comment="Check app is deletable"), output.set(input.get()) + ) + + +@app.opt_in +def opt_in() -> pt.Expr: + return pt.Approve() + + +@app.external(read_only=True) +def default_value( + arg_with_default: pt.abi.String = "default value", + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(arg_with_default.get()) + + +@app.external(read_only=True) +def default_value_from_abi( + arg_with_default: pt.abi.String = default_value, + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("ABI, "), arg_with_default.get())) + + +@app.external(read_only=True) +def default_value_from_global_state( + arg_with_default: pt.abi.Uint64 = BareCallAppState.int1, + *, + output: pt.abi.Uint64, # type: ignore[assignment] +) -> pt.Expr: + return output.set(arg_with_default.get()) + + +@app.external(read_only=True) +def default_value_from_local_state( + arg_with_default: pt.abi.String = BareCallAppState.local_bytes1, + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("Local state, "), arg_with_default.get())) diff --git a/tests/artifacts/testing_app/sources.teal.map.json b/tests/artifacts/testing_app/sources.teal.map.json new file mode 100644 index 00000000..9ee43398 --- /dev/null +++ b/tests/artifacts/testing_app/sources.teal.map.json @@ -0,0 +1,22 @@ +{ + "approvalSourceMap": { + "version": 3, + "sources": [ + "" + ], + "names": [], + "mappings": ";AACA;;;;;;;;AACA;;;;;;;;AACA;;AACA;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;AACA;;;AACA;;AACA;AACA;AACA;;;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAIA;;;AACA;AACA;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AAEA;;;;;;;;;;;;AACA;;AACA;AACA;AACA;AACA;AACA;AACA;;;AAEA;;AACA;AACA;AACA;;;AACA;;;AAEA;;;AAEA;AAIA;;;AACA;AACA;;;;;;;AACA;;AACA;;AACA;;;AACA;AACA;;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;AACA;;;AACA;;;AACA;AACA;;;;;;;;;;;AACA;AACA;;;AACA;;;AACA;AACA;;;;;;;;;;;;;AACA;AACA;;;AACA;AACA;AACA;;;AACA;AACA;;;AACA;AACA;;;AACA;AACA;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;;;;;AACA;;AACA;AACA;;;;;;AACA;;AACA;AACA;;;;;;;;AACA;;AACA;;;AACA;AACA;;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;;AACA;;;;;;;;;;;;AACA;;AACA;AACA;;AACA;;;;;;;;;;;;AACA;;AACA;AACA;;AACA;;;;;;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;;;;;;;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;;AACA;AACA;AACA;;AACA;;AACA;;;AACA;AACA;AAIA;;;AACA;AAEA;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;AAIA;;;AACA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;AACA;;AACA;;AACA;AACA;AACA;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA", + "pcToLocation": {}, + "sourceAndLineToPc": {} + }, + "clearSourceMap": { + "version": 3, + "sources": [ + "" + ], + "names": [], + "mappings": ";AACA;;AACA", + "pcToLocation": {}, + "sourceAndLineToPc": {} + } +} diff --git a/tests/artifacts/testing_app_arc56/arc56_app_spec.json b/tests/artifacts/testing_app_arc56/arc56_app_spec.json new file mode 100644 index 00000000..da275d16 --- /dev/null +++ b/tests/artifacts/testing_app_arc56/arc56_app_spec.json @@ -0,0 +1,681 @@ +{ + "name": "Templates", + "desc": "", + "methods": [ + { + "name": "tmpl", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "specificLengthTemplateVar", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "throwError", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "itobTemplateVar", + "args": [], + "returns": { + "type": "byte[]" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [ + "NoOp" + ], + "call": [] + } + } + ], + "arcs": [ + 4, + 56 + ], + "structs": {}, + "state": { + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "teal": 15, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 1, + 2 + ] + }, + { + "teal": 16, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 3 + ] + }, + { + "teal": 17, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 4, + 5 + ] + }, + { + "teal": 18, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 6 + ] + }, + { + "teal": 19, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 7, + 8 + ] + }, + { + "teal": 20, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 9 + ] + }, + { + "teal": 21, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35 + ] + }, + { + "teal": 25, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "The requested action is not implemented in this contract. Are you using the correct OnComplete? Did you set your app ID?", + "pc": [ + 36 + ] + }, + { + "teal": 30, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 37, + 38, + 39 + ] + }, + { + "teal": 31, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 40 + ] + }, + { + "teal": 32, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 41 + ] + }, + { + "teal": 36, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 42, + 43, + 44 + ] + }, + { + "teal": 40, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:13", + "pc": [ + 45 + ] + }, + { + "teal": 41, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:13", + "pc": [ + 46 + ] + }, + { + "teal": 45, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:14", + "pc": [ + 47 + ] + }, + { + "teal": 46, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:14", + "pc": [ + 48 + ] + }, + { + "teal": 47, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 49 + ] + }, + { + "teal": 52, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 50, + 51, + 52 + ] + }, + { + "teal": 53, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 53 + ] + }, + { + "teal": 54, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 54 + ] + }, + { + "teal": 58, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 55, + 56, + 57 + ] + }, + { + "teal": 62, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 58 + ] + }, + { + "teal": 63, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 59 + ] + }, + { + "teal": 64, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 60 + ] + }, + { + "teal": 65, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 61 + ] + }, + { + "teal": 66, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 62 + ] + }, + { + "teal": 71, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 63, + 64, + 65 + ] + }, + { + "teal": 72, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 66 + ] + }, + { + "teal": 73, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 67 + ] + }, + { + "teal": 77, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 68, + 69, + 70 + ] + }, + { + "teal": 80, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:22", + "errorMessage": "this is an error", + "pc": [ + 71 + ] + }, + { + "teal": 81, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 72 + ] + }, + { + "teal": 86, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 73, + 74, + 75, + 76, + 77, + 78 + ] + }, + { + "teal": 89, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 79, + 80, + 81 + ] + }, + { + "teal": 90, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 82 + ] + }, + { + "teal": 91, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 83 + ] + }, + { + "teal": 92, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 84 + ] + }, + { + "teal": 93, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 85, + 86, + 87 + ] + }, + { + "teal": 94, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 88 + ] + }, + { + "teal": 95, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 89 + ] + }, + { + "teal": 96, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 90 + ] + }, + { + "teal": 97, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 91 + ] + }, + { + "teal": 98, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 92 + ] + }, + { + "teal": 99, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 93 + ] + }, + { + "teal": 103, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 94, + 95, + 96 + ] + }, + { + "teal": 107, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:26", + "pc": [ + 97 + ] + }, + { + "teal": 108, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:26", + "pc": [ + 98 + ] + }, + { + "teal": 109, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 99 + ] + }, + { + "teal": 112, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 100 + ] + }, + { + "teal": 113, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 101 + ] + }, + { + "teal": 116, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 102, + 103, + 104, + 105, + 106, + 107 + ] + }, + { + "teal": 117, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 108, + 109, + 110 + ] + }, + { + "teal": 118, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 111, + 112, + 113, + 114 + ] + }, + { + "teal": 121, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for create NoOp", + "pc": [ + 115 + ] + }, + { + "teal": 124, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 116, + 117, + 118, + 119, + 120, + 121 + ] + }, + { + "teal": 125, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 122, + 123, + 124, + 125, + 126, + 127 + ] + }, + { + "teal": 126, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 128, + 129, + 130, + 131, + 132, + 133 + ] + }, + { + "teal": 127, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 134, + 135, + 136, + 137, + 138, + 139 + ] + }, + { + "teal": 128, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 140, + 141, + 142 + ] + }, + { + "teal": 129, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152 + ] + }, + { + "teal": 132, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for call NoOp", + "pc": [ + 153 + ] + } + ], + "pcOffsetMethod": "cblocks" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCmludGNibG9jayAxIFRNUExfdWludDY0VG1wbFZhcgpieXRlY2Jsb2NrIFRNUExfYnl0ZXNUbXBsVmFyIFRNUExfYnl0ZXM2NFRtcGxWYXIgVE1QTF9ieXRlczMyVG1wbFZhcgoKLy8gVGhpcyBURUFMIHdhcyBnZW5lcmF0ZWQgYnkgVEVBTFNjcmlwdCB2MC4xMDUuMwovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKcHVzaGludCA2CioKdHhuIE9uQ29tcGxldGlvbgorCnN3aXRjaCAqY2FsbF9Ob09wICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqY3JlYXRlX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVECgoqTk9UX0lNUExFTUVOVEVEOgoJLy8gVGhlIHJlcXVlc3RlZCBhY3Rpb24gaXMgbm90IGltcGxlbWVudGVkIGluIHRoaXMgY29udHJhY3QuIEFyZSB5b3UgdXNpbmcgdGhlIGNvcnJlY3QgT25Db21wbGV0ZT8gRGlkIHlvdSBzZXQgeW91ciBhcHAgSUQ/CgllcnIKCi8vIHRtcGwoKXZvaWQKKmFiaV9yb3V0ZV90bXBsOgoJLy8gZXhlY3V0ZSB0bXBsKCl2b2lkCgljYWxsc3ViIHRtcGwKCWludGMgMCAvLyAxCglyZXR1cm4KCi8vIHRtcGwoKTogdm9pZAp0bXBsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvYXJjNTZfdGVtcGxhdGVzL3RlbXBsYXRlcy5hbGdvLnRzOjEzCgkvLyBsb2codGhpcy5ieXRlc1RtcGxWYXIpCglieXRlYyAwIC8vIFRNUExfYnl0ZXNUbXBsVmFyCglsb2cKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9hcmM1Nl90ZW1wbGF0ZXMvdGVtcGxhdGVzLmFsZ28udHM6MTQKCS8vIGFzc2VydCh0aGlzLnVpbnQ2NFRtcGxWYXIpCglpbnRjIDEgLy8gVE1QTF91aW50NjRUbXBsVmFyCglhc3NlcnQKCXJldHN1YgoKLy8gc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcigpdm9pZAoqYWJpX3JvdXRlX3NwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXI6CgkvLyBleGVjdXRlIHNwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXIoKXZvaWQKCWNhbGxzdWIgc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcgoJaW50YyAwIC8vIDEKCXJldHVybgoKLy8gc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcigpOiB2b2lkCnNwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXI6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9hcmM1Nl90ZW1wbGF0ZXMvdGVtcGxhdGVzLmFsZ28udHM6MTgKCS8vIGVkMjU1MTlWZXJpZnlCYXJlKHRoaXMuYnl0ZXNUbXBsVmFyLCB0aGlzLmJ5dGVzNjRUbXBsVmFyLCB0aGlzLmJ5dGVzMzJUbXBsVmFyKQoJYnl0ZWMgMCAvLyBUTVBMX2J5dGVzVG1wbFZhcgoJYnl0ZWMgMSAvLyBUTVBMX2J5dGVzNjRUbXBsVmFyCglieXRlYyAyIC8vIFRNUExfYnl0ZXMzMlRtcGxWYXIKCWVkMjU1MTl2ZXJpZnlfYmFyZQoJcmV0c3ViCgovLyB0aHJvd0Vycm9yKCl2b2lkCiphYmlfcm91dGVfdGhyb3dFcnJvcjoKCS8vIGV4ZWN1dGUgdGhyb3dFcnJvcigpdm9pZAoJY2FsbHN1YiB0aHJvd0Vycm9yCglpbnRjIDAgLy8gMQoJcmV0dXJuCgovLyB0aHJvd0Vycm9yKCk6IHZvaWQKdGhyb3dFcnJvcjoKCXByb3RvIDAgMAoKCS8vIHRoaXMgaXMgYW4gZXJyb3IKCWVycgoJcmV0c3ViCgovLyBpdG9iVGVtcGxhdGVWYXIoKWJ5dGVbXQoqYWJpX3JvdXRlX2l0b2JUZW1wbGF0ZVZhcjoKCS8vIFRoZSBBQkkgcmV0dXJuIHByZWZpeAoJcHVzaGJ5dGVzIDB4MTUxZjdjNzUKCgkvLyBleGVjdXRlIGl0b2JUZW1wbGF0ZVZhcigpYnl0ZVtdCgljYWxsc3ViIGl0b2JUZW1wbGF0ZVZhcgoJZHVwCglsZW4KCWl0b2IKCWV4dHJhY3QgNiAyCglzd2FwCgljb25jYXQKCWNvbmNhdAoJbG9nCglpbnRjIDAgLy8gMQoJcmV0dXJuCgovLyBpdG9iVGVtcGxhdGVWYXIoKTogYnl0ZXMKaXRvYlRlbXBsYXRlVmFyOgoJcHJvdG8gMCAxCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvYXJjNTZfdGVtcGxhdGVzL3RlbXBsYXRlcy5hbGdvLnRzOjI2CgkvLyByZXR1cm4gaXRvYih0aGlzLnVpbnQ2NFRtcGxWYXIpCglpbnRjIDEgLy8gVE1QTF91aW50NjRUbXBsVmFyCglpdG9iCglyZXRzdWIKCiphYmlfcm91dGVfY3JlYXRlQXBwbGljYXRpb246CglpbnRjIDAgLy8gMQoJcmV0dXJuCgoqY3JlYXRlX05vT3A6CglwdXNoYnl0ZXMgMHhiODQ0N2IzNiAvLyBtZXRob2QgImNyZWF0ZUFwcGxpY2F0aW9uKCl2b2lkIgoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAoJbWF0Y2ggKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbgoKCS8vIHRoaXMgY29udHJhY3QgZG9lcyBub3QgaW1wbGVtZW50IHRoZSBnaXZlbiBBQkkgbWV0aG9kIGZvciBjcmVhdGUgTm9PcAoJZXJyCgoqY2FsbF9Ob09wOgoJcHVzaGJ5dGVzIDB4OWE3MWQyYjQgLy8gbWV0aG9kICJ0bXBsKCl2b2lkIgoJcHVzaGJ5dGVzIDB4ZGY0ZDVjM2IgLy8gbWV0aG9kICJzcGVjaWZpY0xlbmd0aFRlbXBsYXRlVmFyKCl2b2lkIgoJcHVzaGJ5dGVzIDB4M2Q4NzBkODcgLy8gbWV0aG9kICJ0aHJvd0Vycm9yKCl2b2lkIgoJcHVzaGJ5dGVzIDB4YmMwYjE3MDYgLy8gbWV0aG9kICJpdG9iVGVtcGxhdGVWYXIoKWJ5dGVbXSIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfdG1wbCAqYWJpX3JvdXRlX3NwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXIgKmFiaV9yb3V0ZV90aHJvd0Vycm9yICphYmlfcm91dGVfaXRvYlRlbXBsYXRlVmFyCgoJLy8gdGhpcyBjb250cmFjdCBkb2VzIG5vdCBpbXBsZW1lbnQgdGhlIGdpdmVuIEFCSSBtZXRob2QgZm9yIGNhbGwgTm9PcAoJZXJy", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + }, + "templateVariables": { + "bytesTmplVar": { + "type": "byte[]" + }, + "uint64TmplVar": { + "type": "uint64" + }, + "bytes32TmplVar": { + "type": "byte[32]" + }, + "bytes64TmplVar": { + "type": "byte[64]" + } + }, + "scratchVariables": { + "bytesTmplVar": { + "type": "byte[]", + "slot": 200 + }, + "uint64TmplVar": { + "type": "uint64", + "slot": 201 + }, + "bytes32TmplVar": { + "type": "byte[32]", + "slot": 202 + }, + "bytes64TmplVar": { + "type": "byte[64]", + "slot": 203 + } + }, + "compilerInfo": { + "compiler": "algod", + "compilerVersion": { + "major": 3, + "minor": 26, + "patch": 0, + "commitHash": "0d10b244" + } + } +} \ No newline at end of file diff --git a/tests/artifacts/testing_app_puya/arc32_app_spec.json b/tests/artifacts/testing_app_puya/arc32_app_spec.json new file mode 100644 index 00000000..d8518906 --- /dev/null +++ b/tests/artifacts/testing_app_puya/arc32_app_spec.json @@ -0,0 +1,184 @@ +{ + "hints": { + "set_box_bytes(string,byte[])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_str(string,string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_int(string,uint32)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_int512(string,uint512)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_static(string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_struct(string,(string,uint64))void": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "value": { + "name": "DummyStruct", + "elements": [ + [ + "name", + "string" + ], + [ + "id", + "uint64" + ] + ] + } + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuYXBwcm92YWxfcHJvZ3JhbToKICAgIGludGNibG9jayAxIDAKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5fX3B1eWFfYXJjNF9yb3V0ZXJfXygpIC0+IHVpbnQ2NDoKX19wdXlhX2FyYzRfcm91dGVyX186CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICBwcm90byAwIDEKICAgIHR4biBOdW1BcHBBcmdzCiAgICBieiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgyMDJmYTczYSAweGRmN2VlYTRhIDB4MzY4OGVkMmMgMHg1ZDE3MjBkZCAweGY4MDY2NjVjIDB4ODFkMjYwZTIgLy8gbWV0aG9kICJzZXRfYm94X2J5dGVzKHN0cmluZyxieXRlW10pdm9pZCIsIG1ldGhvZCAic2V0X2JveF9zdHIoc3RyaW5nLHN0cmluZyl2b2lkIiwgbWV0aG9kICJzZXRfYm94X2ludChzdHJpbmcsdWludDMyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfaW50NTEyKHN0cmluZyx1aW50NTEyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfc3RhdGljKHN0cmluZyxieXRlWzRdKXZvaWQiLCBtZXRob2QgInNldF9zdHJ1Y3Qoc3RyaW5nLChzdHJpbmcsdWludDY0KSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9ieXRlc19yb3V0ZUAyIF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RyX3JvdXRlQDMgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2ludDUxMl9yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RhdGljX3JvdXRlQDYgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3CiAgICBpbnRjXzEgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2J5dGVzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgZXh0cmFjdCAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjAKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X2J5dGVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0cl9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0cgogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANDoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2V0X2JveF9pbnQKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfaW50NTEyX3JvdXRlQDU6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjMyCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNldF9ib3hfaW50NTEyCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0YXRpY19yb3V0ZUA2OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzYKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0YXRpYwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgc2V0X3N0cnVjdAogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTQKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDE0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgaW50Y18xIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2J5dGVzKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfYnl0ZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwLTIxCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZXRfYm94X2J5dGVzKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogQnl0ZXMpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjIKICAgIC8vIHNlbGYuYm94X2J5dGVzW25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2J5dGVzIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGR1cAogICAgYm94X2RlbAogICAgcG9wCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X3N0cihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X3N0cjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQtMjUKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfc3RyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5TdHJpbmcpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjYKICAgIC8vIHNlbGYuYm94X3N0cltuYW1lXSA9IHZhbHVlCiAgICBwdXNoYnl0ZXMgImJveF9zdHIiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZHVwCiAgICBib3hfZGVsCiAgICBwb3AKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfaW50KG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfaW50OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyOC0yOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9pbnQoc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlVJbnQzMikgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMAogICAgLy8gc2VsZi5ib3hfaW50W25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2ludCIKICAgIGZyYW1lX2RpZyAtMgogICAgY29uY2F0CiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2ludDUxMihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X2ludDUxMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzItMzMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfaW50NTEyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5VSW50NTEyKSAtPiBOb25lOgogICAgcHJvdG8gMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjM0CiAgICAvLyBzZWxmLmJveF9pbnQ1MTJbbmFtZV0gPSB2YWx1ZQogICAgcHVzaGJ5dGVzICJib3hfaW50NTEyIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfc3RhdGljKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfc3RhdGljOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNi0zOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9zdGF0aWMoCiAgICAvLyAgICAgc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlN0YXRpY0FycmF5W2FyYzQuQnl0ZSwgTGl0ZXJhbFs0XV0KICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MAogICAgLy8gc2VsZi5ib3hfc3RhdGljW25hbWVdID0gdmFsdWUuY29weSgpCiAgICBwdXNoYnl0ZXMgImJveF9zdGF0aWMiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZnJhbWVfZGlnIC0xCiAgICBib3hfcHV0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuc2V0X3N0cnVjdChuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfc3RydWN0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0Mi00MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBzZXRfc3RydWN0KHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogRHVtbXlTdHJ1Y3QpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6NDQKICAgIC8vIGFzc2VydCBuYW1lLmJ5dGVzID09IHZhbHVlLm5hbWUuYnl0ZXMsICJOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0IgogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzEgLy8gMAogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBvcC5Cb3gucHV0KG5hbWUuYnl0ZXMsIHZhbHVlLmJ5dGVzKQogICAgZnJhbWVfZGlnIC0yCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1Ygo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuY2xlYXJfc3RhdGVfcHJvZ3JhbToKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "TestPuyaBoxes", + "methods": [ + { + "name": "set_box_bytes", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[]", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_str", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_int", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint32", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_int512", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint512", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_static", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[4]", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_struct", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "(string,uint64)", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} diff --git a/tests/artifacts/testing_app_puya/contract.py b/tests/artifacts/testing_app_puya/contract.py new file mode 100644 index 00000000..7074dd6b --- /dev/null +++ b/tests/artifacts/testing_app_puya/contract.py @@ -0,0 +1,43 @@ +from typing import Literal + +from algopy import ARC4Contract, BoxMap, Bytes, arc4, op + + +class DummyStruct(arc4.Struct): + name: arc4.String + id: arc4.UInt64 + + +class TestPuyaBoxes(ARC4Contract): + def __init__(self) -> None: + self.box_bytes = BoxMap(arc4.String, Bytes) + self.box_bytes2 = BoxMap(Bytes, Bytes) + self.box_str = BoxMap(arc4.String, arc4.String) + self.box_int = BoxMap(arc4.String, arc4.UInt32) + self.box_int512 = BoxMap(arc4.String, arc4.UInt512) + self.box_static = BoxMap(arc4.String, arc4.StaticArray[arc4.Byte, Literal[4]]) + + @arc4.abimethod + def set_box_bytes(self, name: arc4.String, value: Bytes) -> None: + self.box_bytes[name] = value + + @arc4.abimethod + def set_box_str(self, name: arc4.String, value: arc4.String) -> None: + self.box_str[name] = value + + @arc4.abimethod + def set_box_int(self, name: arc4.String, value: arc4.UInt32) -> None: + self.box_int[name] = value + + @arc4.abimethod + def set_box_int512(self, name: arc4.String, value: arc4.UInt512) -> None: + self.box_int512[name] = value + + @arc4.abimethod + def set_box_static(self, name: arc4.String, value: arc4.StaticArray[arc4.Byte, Literal[4]]) -> None: + self.box_static[name] = value.copy() + + @arc4.abimethod() + def set_struct(self, name: arc4.String, value: DummyStruct) -> None: + assert name.bytes == value.name.bytes, "Name must match id of struct" + op.Box.put(name.bytes, value.bytes) diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index 61e5c255..2d5ea4e4 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -1,6 +1,7 @@ -import algosdk import pytest -from algokit_utils import Account, get_account +from algosdk.atomic_transaction_composer import AccountTransactionSigner + +from algokit_utils import Account from algokit_utils.assets.asset_manager import ( AccountAssetInformation, AssetInformation, @@ -12,26 +13,32 @@ AssetCreateParams, PaymentParams, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner -from tests.conftest import get_unique_name +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() -@pytest.fixture() -def sender(funded_account: Account) -> Account: - return funded_account - -@pytest.fixture() -def receiver(algod_client: algosdk.v2client.algod.AlgodClient) -> Account: - return get_account(algod_client, get_unique_name()) +@pytest.fixture +def sender(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def receiver(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + return new_account def test_get_by_id(algorand: AlgorandClient, sender: Account) -> None: diff --git a/tests/clients/algorand_client/__init__.py b/tests/clients/algorand_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py new file mode 100644 index 00000000..a7637f58 --- /dev/null +++ b/tests/clients/algorand_client/test_transfer.py @@ -0,0 +1,427 @@ +import httpx +import pytest +from pytest_httpx._httpx_mock import HTTPXMock + +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.clients.dispenser_api_client import DispenserApiConfig, TestNetDispenserApiClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AssetOptInParams, + AssetTransferParams, + PaymentParams, +) +from tests.conftest import generate_test_asset + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + result = algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(5), + note=b"Transfer 5 Algos", + ) + ) + + account_info = algorand.account.get_information(second_account) + + assert result.transaction.payment + assert result.transaction.payment.amt == 5_000_000 + + assert result.transaction.payment.sender == funded_account.address == result.confirmation["txn"]["txn"]["snd"] # type: ignore # noqa: PGH003 + assert account_info["amount"] == 5_000_000 + + +def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(1), + lease=b"test", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(2), + lease=b"test", + ) + ) + + +def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(1), + lease=b"\x01\x02\x03\x04", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(2), + lease=b"\x01\x02\x03\x04", + ) + ) + + +def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=1, + lease=b"test", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=2, + lease=b"test", + ) + ) + + +def test_transfer_asa_receiver_not_opted_in( + algorand: AlgorandClient, + funded_account: Account, +) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + + with pytest.raises(Exception, match="receiver error: must optin"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=1, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + +def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + with pytest.raises(Exception, match=f"asset {test_asset_id} missing from {second_account.address}"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=second_account.address, + receiver=funded_account.address, + asset_id=test_asset_id, + amount=1, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + +def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + with pytest.raises(Exception, match=f"asset 123123 missing from {funded_account.address}"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=123123, + amount=5, + note=b"Transfer asset with wrong id", + ) + ) + + +def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + second_account_info = algorand.asset.get_account_information(second_account, test_asset_id) + assert second_account_info.balance == 5 + + test_account_info = algorand.asset.get_account_information(funded_account, test_asset_id) + assert test_account_info.balance == 95 + + +def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + clawback_account = algorand.account.random() + + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + algorand.account.ensure_funded( + account_to_fund=clawback_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=clawback_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=clawback_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + clawback_from_info = algorand.asset.get_account_information(clawback_account, test_asset_id) + assert clawback_from_info.balance == 5 + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + clawback_target=clawback_account.address, + ) + ) + + second_account_info = algorand.asset.get_account_information(second_account, test_asset_id) + assert second_account_info.balance == 5 + + clawback_account_info = algorand.asset.get_account_information(clawback_account, test_asset_id) + assert clawback_account_info.balance == 0 + + test_account_info = algorand.asset.get_account_information(funded_account, test_asset_id) + assert test_account_info.balance == 95 + + +MINIMUM_BALANCE = AlgoAmount.from_micro_algos( + 100_000 +) # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance + + +def test_ensure_funded(algorand: AlgorandClient, funded_account: Account) -> None: + test_account = algorand.account.random() + response = algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + ) + assert response is not None + + to_account_info = algorand.account.get_information(test_account) + assert isinstance(to_account_info, dict) + actual_amount = to_account_info.get("amount") + assert actual_amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + + +def test_ensure_funded_uses_dispenser_by_default( + algorand: AlgorandClient, +) -> None: + second_account = algorand.account.random() + dispenser = algorand.account.dispenser_from_environment() + + result = algorand.account.ensure_funded_from_environment( + account_to_fund=second_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + assert result is not None + assert result.transaction.payment is not None + assert result.transaction.payment.sender == dispenser.address + + account_info = algorand.account.get_information(second_account) + assert account_info["amount"] == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + + +def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClient, funded_account: Account) -> None: + test_account = algorand.account.random() + response = algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_micro_algo(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + assert response is not None + + to_account_info = algorand.account.get_information(test_account) + assert isinstance(to_account_info, dict) + actual_amount = to_account_info.get("amount") + assert actual_amount == AlgoAmount.from_algos(1) + + +def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: + algorand = AlgorandClient.test_net() + account_to_fund = algorand.account.random() + monkeypatch.setenv( + "ALGOKIT_DISPENSER_ACCESS_TOKEN", + "dummy", + ) + httpx_mock.add_response( + url=f"{DispenserApiConfig.BASE_URL}/fund/0", + method="POST", + json={"amount": 1, "txID": "dummy_tx_id"}, + ) + + result = algorand.account.ensure_funded_from_testnet_dispenser_api( + account_to_fund=account_to_fund, + dispenser_client=TestNetDispenserApiClient(), + min_spending_balance=AlgoAmount.from_micro_algo(1), + ) + assert result is not None + assert result.transaction_id == "dummy_tx_id" + assert result.amount_funded == AlgoAmount.from_micro_algo(1) + + +def test_ensure_funded_testnet_api_bad_response(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: + algorand = AlgorandClient.test_net() + account_to_fund = algorand.account.random() + monkeypatch.setenv( + "ALGOKIT_DISPENSER_ACCESS_TOKEN", + "dummy", + ) + httpx_mock.add_exception( + httpx.HTTPStatusError( + "Limit exceeded", + request=httpx.Request("POST", f"{DispenserApiConfig.BASE_URL}/fund"), + response=httpx.Response( + 400, + request=httpx.Request("POST", f"{DispenserApiConfig.BASE_URL}/fund"), + json={ + "code": "fund_limit_exceeded", + "limit": 10_000_000, + "resetsAt": "2023-09-19T10:07:34.024Z", + }, + ), + ), + url=f"{DispenserApiConfig.BASE_URL}/fund/0", + method="POST", + ) + + with pytest.raises(Exception, match="fund_limit_exceeded"): + algorand.account.ensure_funded_from_testnet_dispenser_api( + account_to_fund=account_to_fund, + dispenser_client=TestNetDispenserApiClient(), + min_spending_balance=AlgoAmount.from_micro_algo(1), + ) + + +def test_rekey_works(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.account.rekey_account(funded_account, second_account, note=b"rekey") + + # This will throw if the rekey wasn't successful + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(1), + signer=second_account.signer, + ) + ) diff --git a/tests/clients/test_algorand_client.py b/tests/clients/test_algorand_client.py deleted file mode 100644 index ce0f90d5..00000000 --- a/tests/clients/test_algorand_client.py +++ /dev/null @@ -1,223 +0,0 @@ -# TODO: Update tests for latest version of algokit-utils -# import json -# from pathlib import Path - -# import pytest -# from algokit_utils import Account, ApplicationClient -# from algokit_utils.accounts.account_manager import AddressAndSigner -# from algokit_utils.clients.algorand_client import ( -# AlgorandClient, -# AppMethodCallParams, -# AssetCreateParams, -# AssetOptInParams, -# PaymentParams, -# ) -# from algosdk.abi import Contract -# from algosdk.atomic_transaction_composer import AtomicTransactionComposer - - -# @pytest.fixture() -# def algorand(funded_account: Account) -> AlgorandClient: -# client = AlgorandClient.default_local_net() -# client.set_signer(sender=funded_account.address, signer=funded_account.signer) -# return client - - -# @pytest.fixture() -# def alice(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: -# acct = algorand.account.random() -# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) -# return acct - - -# @pytest.fixture() -# def bob(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: -# acct = algorand.account.random() -# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) -# return acct - - -# @pytest.fixture() -# def app_client(algorand: AlgorandClient, alice: AddressAndSigner) -> ApplicationClient: -# client = ApplicationClient( -# algorand.client.algod, -# Path(__file__).parent / "app_algorand_client.json", -# sender=alice.address, -# signer=alice.signer, -# ) -# client.create(call_abi_method="createApplication") -# return client - - -# @pytest.fixture() -# def contract() -> Contract: -# with Path.open(Path(__file__).parent / "app_algorand_client.json") as f: -# return Contract.from_json(json.dumps(json.load(f)["contract"])) - - -# def test_send_payment(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: -# amount = 100_000 - -# alice_pre_balance = algorand.account.get_information(alice.address)["amount"] -# bob_pre_balance = algorand.account.get_information(bob.address)["amount"] -# result = algorand.send.payment(PaymentParams(sender=alice.address, receiver=bob.address, amount=amount)) -# alice_post_balance = algorand.account.get_information(alice.address)["amount"] -# bob_post_balance = algorand.account.get_information(bob.address)["amount"] - -# assert result["confirmation"] is not None -# assert alice_post_balance == alice_pre_balance - 1000 - amount -# assert bob_post_balance == bob_pre_balance + amount - - -# def test_send_asset_create(algorand: AlgorandClient, alice: AddressAndSigner) -> None: -# total = 100 - -# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) -# asset_index = result["confirmation"]["asset-index"] - -# assert asset_index > 0 - - -# def test_asset_opt_in(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: -# total = 100 - -# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) -# asset_index = result["confirmation"]["asset-index"] - -# algorand.send.asset_opt_in(AssetOptInParams(sender=bob.address, asset_id=asset_index)) - -# assert algorand.account.get_asset_information(bob.address, asset_index) is not None - - -# DO_MATH_VALUE = 3 - - -# def test_add_atc(algorand: AlgorandClient, app_client: ApplicationClient, alice: AddressAndSigner) -> None: -# atc = AtomicTransactionComposer() -# app_client.compose_call(atc, call_abi_method="doMath", a=1, b=2, operation="sum") - -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_atc(atc) -# .execute() -# ) -# assert result.abi_results[0].return_value == DO_MATH_VALUE - - -# def test_add_method_call( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("doMath"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[1, 2, "sum"], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == DO_MATH_VALUE - - -# def test_add_method_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[pay_arg], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address - - -# def test_add_method_call_with_method_call_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# hello_world_call = AppMethodCallParams( -# method=contract.get_method_by_name("helloWorld"), sender=alice.address, app_id=app_client.app_id -# ) -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("methodArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[hello_world_call], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == "Hello, World!" -# assert result.abi_results[1].return_value == app_client.app_id - - -# def test_add_method_call_with_method_call_arg_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# txn_arg_call = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg] -# ) -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("nestedTxnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[txn_arg_call], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address -# assert result.abi_results[1].return_value == app_client.app_id - - -# def test_add_method_call_with_two_method_call_args_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg_1 = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# txn_arg_call_1 = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[pay_arg_1], -# note=b"1", -# ) - -# pay_arg_2 = PaymentParams(sender=alice.address, receiver=alice.address, amount=2) -# txn_arg_call_2 = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg_2] -# ) - -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("doubleNestedTxnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[txn_arg_call_1, txn_arg_call_2], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address -# assert result.abi_results[1].return_value == alice.address -# assert result.abi_results[2].return_value == app_client.app_id diff --git a/tests/conftest.py b/tests/conftest.py index 18021c21..9499465e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,9 @@ from typing import TYPE_CHECKING from uuid import uuid4 -import algosdk.transaction import pytest +from dotenv import load_dotenv + from algokit_utils import ( DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, @@ -16,20 +17,13 @@ ApplicationSpecification, EnsureBalanceParameters, ensure_funded, - get_account, - get_algod_client, - get_indexer_client, - get_kmd_client_from_algod_client, replace_template_variables, ) -from dotenv import load_dotenv - -from legacy_v2_tests import app_client_test +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.transactions.transaction_composer import AssetCreateParams if TYPE_CHECKING: - from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient @pytest.fixture(autouse=True, scope="session") @@ -127,77 +121,30 @@ def is_opted_in(client_fixture: ApplicationClient) -> bool: return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) -@pytest.fixture(scope="session") -def algod_client() -> "AlgodClient": - return get_algod_client() - - -@pytest.fixture(scope="session") -def kmd_client(algod_client: "AlgodClient") -> "KMDClient": - return get_kmd_client_from_algod_client(algod_client) - - -@pytest.fixture(scope="session") -def indexer_client() -> "IndexerClient": - return get_indexer_client() - - -@pytest.fixture() -def creator(algod_client: "AlgodClient") -> Account: - creator_name = get_unique_name() - return get_account(algod_client, creator_name) - - -@pytest.fixture(scope="session") -def funded_account(algod_client: "AlgodClient") -> Account: - creator_name = get_unique_name() - return get_account(algod_client, creator_name) - - -@pytest.fixture(scope="session") -def app_spec() -> ApplicationSpecification: - app_spec = app_client_test.app.build() - path = Path(__file__).parent / "app_client_test.json" - path.write_text(app_spec.to_json()) - return read_spec("app_client_test.json", deletable=True, updatable=True, template_values={"VERSION": 1}) - - -def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int | None) -> int: +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None) -> int: if total is None: total = math.floor(random.random() * 100) + 20 decimals = 0 asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}" - params = algod_client.suggested_params() - - txn = algosdk.transaction.AssetConfigTxn( - sender=sender.address, - sp=params, - total=total * 10**decimals, - decimals=decimals, - default_frozen=False, - unit_name="", - asset_name=asset_name, - manager=sender.address, - reserve=sender.address, - freeze=sender.address, - clawback=sender.address, - url="https://path/to/my/asset/details", - metadata_hash=None, - note=None, - lease=None, - rekey_to=None, + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=decimals, + default_frozen=False, + unit_name="CFG", + asset_name=asset_name, + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) ) - signed_transaction = txn.sign(sender.private_key) - algod_client.send_transaction(signed_transaction) - ptx = algod_client.pending_transaction_info(txn.get_txid()) - - if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): - return ptx["asset-index"] - else: - raise ValueError("Unexpected response from pending_transaction_info") + return int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] def assure_funds(algod_client: "AlgodClient", account: Account) -> None: diff --git a/tests/test_transaction_composer.py b/tests/test_transaction_composer.py index 5ea937ec..a7096c83 100644 --- a/tests/test_transaction_composer.py +++ b/tests/test_transaction_composer.py @@ -1,6 +1,13 @@ from typing import TYPE_CHECKING import pytest +from algosdk.transaction import ( + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -13,27 +20,29 @@ SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.transaction import ( - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algokit_utils.transactions.models import Arc2TransactionNote -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient) -> Account: secondary_name = get_unique_name() return get_account(algorand.client.algod, secondary_name) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 619668e8..9217cc08 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -3,6 +3,14 @@ import algosdk import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -16,28 +24,29 @@ SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.transaction import ( - ApplicationCallTxn, - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algokit_utils.transactions.models import Arc2TransactionNote -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() + +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient) -> Account: secondary_name = get_unique_name() return get_account(algorand.client.algod, secondary_name) @@ -82,7 +91,7 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> composer.add_asset_create(params) built = composer.build_transactions() - response = composer.execute(max_rounds_to_wait=20) + response = composer.send(max_rounds_to_wait=20) created_asset = algorand.client.algod.asset_info( algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] )["params"] @@ -138,7 +147,7 @@ def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, fun assert txn.index == asset_before_config_index assert txn.manager == funded_secondary_account.address - composer.execute(max_rounds_to_wait=20) + composer.send(max_rounds_to_wait=20) updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] assert updated_asset["manager"] == funded_secondary_account.address @@ -165,7 +174,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No assert txn.sender == funded_account.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" - composer.execute(max_rounds_to_wait=20) + composer.send(max_rounds_to_wait=20) def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Account) -> None: @@ -173,8 +182,8 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, ) - approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() - clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() composer.add_app_create( AppCreateParams( sender=funded_account.address, @@ -183,7 +192,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, ) ) - response = composer.execute() + response = composer.send() app_id = algorand.client.algod.pending_transaction_info(response.tx_ids[0])["application-index"] # type: ignore[call-overload] composer = TransactionComposer( @@ -204,8 +213,8 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco assert isinstance(built.transactions[0], ApplicationCallTxn) txn = built.transactions[0] assert txn.sender == funded_account.address - response = composer.execute(max_rounds_to_wait=20) - assert response.returns[-1] == "Hello, world" + response = composer.send(max_rounds_to_wait=20) + assert response.returns[-1].return_value == "Hello, world" def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index ec84d650..3c944041 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -2,6 +2,18 @@ import algosdk import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + AssetDestroyTxn, + AssetFreezeTxn, + AssetTransferTxn, + KeyregTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -19,29 +31,26 @@ OnlineKeyRegistrationParams, PaymentParams, ) -from algosdk.transaction import ( - ApplicationCallTxn, - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - AssetDestroyTxn, - AssetFreezeTxn, - AssetTransferTxn, - KeyregTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient, funded_account: Account) -> Account: secondary_name = get_unique_name() account = get_account(algorand.client.algod, secondary_name) @@ -210,8 +219,8 @@ def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: Account) -> None: - approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() - clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() # First create the app create_result = algorand.send.app_create( @@ -222,7 +231,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, ) ) - app_id = algorand.client.algod.pending_transaction_info(create_result.tx_id)["application-index"] # type: ignore[call-overload] + app_id = algorand.client.algod.pending_transaction_info(create_result.tx_ids[0])["application-index"] # type: ignore[call-overload] # Then test creating a method call transaction result = algorand.create_transaction.app_call_method_call( diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index b8514cdb..def636fd 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -1,15 +1,18 @@ -from typing import TYPE_CHECKING, cast +from pathlib import Path from unittest.mock import MagicMock, patch +import algosdk import pytest -from algokit_utils import ( - Account, - get_account, -) + +from algokit_utils import Account +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, AppCreateParams, AssetConfigParams, AssetCreateParams, @@ -23,47 +26,86 @@ TransactionComposer, ) from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender -from algosdk.transaction import ( - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - AssetDestroyTxn, - AssetFreezeTxn, - AssetTransferTxn, - PaymentTxn, -) -from tests.conftest import get_unique_name -if TYPE_CHECKING: - import algosdk +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def sender(funded_account: Account) -> Account: return funded_account -@pytest.fixture() -def receiver(algod_client: "algosdk.v2client.algod.AlgodClient") -> Account: - return get_account(algod_client, get_unique_name()) +@pytest.fixture +def receiver(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + return new_account + + +@pytest.fixture +def raw_hello_world_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def test_hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def test_hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, test_hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = test_hello_world_arc32_app_spec.global_state_schema + local_schema = test_hello_world_arc32_app_spec.local_state_schema + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=test_hello_world_arc32_app_spec.approval_program, + clear_state_program=test_hello_world_arc32_app_spec.clear_program, + schema={ + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + }, + ) + ) + return response.app_id -@pytest.fixture() -def transaction_sender( - algod_client: "algosdk.v2client.algod.AlgodClient", sender: Account -) -> AlgorandClientTransactionSender: +@pytest.fixture +def transaction_sender(algorand: AlgorandClient, sender: Account) -> AlgorandClientTransactionSender: def new_group() -> TransactionComposer: return TransactionComposer( - algod=algod_client, + algod=algorand.client.algod, get_signer=lambda _: sender.signer, ) return AlgorandClientTransactionSender( new_group=new_group, - asset_manager=AssetManager(algod_client, new_group), - app_manager=AppManager(algod_client), - algod_client=algod_client, + asset_manager=AssetManager(algorand.client.algod, new_group), + app_manager=AppManager(algorand.client.algod), + algod_client=algorand.client.algod, ) @@ -79,7 +121,8 @@ def test_payment(transaction_sender: AlgorandClientTransactionSender, sender: Ac assert len(result.tx_ids) == 1 assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - txn = cast(PaymentTxn, result.transaction) + txn = result.transaction.payment + assert txn assert txn.sender == sender.address assert txn.receiver == receiver.address assert txn.amt == amount.micro_algos @@ -100,7 +143,8 @@ def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_create(params) assert len(result.tx_ids) == 1 assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - txn = cast(AssetCreateTxn, result.transaction) + txn = result.transaction.asset_config + assert txn assert txn.sender == sender.address assert txn.total == total assert txn.decimals == 0 @@ -135,10 +179,12 @@ def test_asset_config(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_config(config_params) assert len(result.tx_ids) == 1 - assert isinstance(result.transaction, AssetConfigTxn) - assert result.transaction.sender == sender.address - assert result.transaction.index == asset_id - assert result.transaction.manager == receiver.address + assert result.transaction.asset_config + txn = result.transaction.asset_config + assert txn + assert txn.sender == sender.address + assert txn.index == asset_id + assert txn.manager == receiver.address def test_asset_freeze( @@ -171,7 +217,9 @@ def test_asset_freeze( result = transaction_sender.asset_freeze(freeze_params) assert len(result.tx_ids) == 1 - txn = cast(AssetFreezeTxn, result.transaction) + assert result.transaction.asset_freeze + txn = result.transaction.asset_freeze + assert txn assert txn.sender == sender.address assert txn.index == asset_id assert txn.target == sender.address @@ -202,7 +250,8 @@ def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, send result = transaction_sender.asset_destroy(destroy_params) assert len(result.tx_ids) == 1 - txn = cast(AssetDestroyTxn, result.transaction) + txn = result.transaction.asset_config + assert txn assert txn.sender == sender.address assert txn.index == asset_id @@ -244,7 +293,8 @@ def test_asset_transfer( result = transaction_sender.asset_transfer(transfer_params) assert len(result.tx_ids) == 1 - txn = cast(AssetTransferTxn, result.transaction) + txn = result.transaction.asset_transfer + assert txn assert txn.sender == sender.address assert txn.index == asset_id assert txn.receiver == receiver.address @@ -275,7 +325,8 @@ def test_asset_opt_in(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_opt_in(opt_in_params) assert len(result.tx_ids) == 1 - txn = cast(AssetTransferTxn, result.transaction) + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer assert txn.sender == receiver.address assert txn.index == asset_id assert txn.amount == 0 @@ -315,7 +366,8 @@ def test_asset_opt_out(transaction_sender: AlgorandClientTransactionSender, send ) result = transaction_sender.asset_opt_out(params=opt_out_params) - txn = cast(AssetTransferTxn, result.transaction) + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer assert txn.sender == receiver.address assert txn.index == asset_id assert txn.amount == 0 @@ -336,13 +388,40 @@ def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: result = transaction_sender.app_create(params) assert result.app_id > 0 assert result.app_address - txn = cast(ApplicationCreateTxn, result.transaction) + + assert result.transaction.application_call + txn = result.transaction.application_call assert txn.sender == sender.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" -# TODO: add remaining app call and app method call tests +def test_app_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account +) -> None: + params = AppCallParams( + app_id=test_hello_world_arc32_app_id, + sender=sender.address, + args=[b"\x02\xbe\xce\x11", b"test"], + ) + + result = transaction_sender.app_call(params) + assert not result.return_value # TODO: improve checks + + +def test_app_call_method_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account +) -> None: + params = AppCallMethodCall( + app_id=test_hello_world_arc32_app_id, + sender=sender.address, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["test"], + ) + + result = transaction_sender.app_call_method_call(params) + assert result.return_value + assert result.return_value.return_value == "Hello2, test" @patch("logging.Logger.debug") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..612ea60d --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,29 @@ +from pathlib import Path + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME + + +def load_arc32_spec( + path: Path, + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> ApplicationSpecification: + spec = ApplicationSpecification.from_json(path.read_text(encoding="utf-8")) + + template_variables = template_values or {} + if updatable is not None: + template_variables["UPDATABLE"] = int(updatable) + + if deletable is not None: + template_variables["DELETABLE"] = int(deletable) + + spec.approval_program = ( + AppManager.replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) + return spec From 18744fe0eb9a1bcfa695fe4e274139d9737755f5 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 11 Dec 2024 18:33:01 +0100 Subject: [PATCH 05/31] chore: expose new interfaces --- src/algokit_utils/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 5b3a1647..8f06e519 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -81,6 +81,15 @@ is_mainnet, is_testnet, ) + +# New interfaces +from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager +from algokit_utils.applications.app_client import AppClient +from algokit_utils.applications.app_factory import AppFactory +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.clients.client_manager import ClientManager from algokit_utils.clients.dispenser_api_client import ( DISPENSER_ACCESS_TOKEN_KEY, DISPENSER_REQUEST_TIMEOUT, @@ -90,6 +99,7 @@ ) from algokit_utils.models.account import Account from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.transactions.transaction_composer import TransactionComposer __all__ = [ "DELETABLE_TEMPLATE_NAME", @@ -105,15 +115,21 @@ "ABIMethod", "ABITransactionResponse", "Account", + "AccountManager", "AlgoClientConfig", + "AlgorandClient", + "AppClient", "AppDeployMetaData", + "AppFactory", "AppLookup", "AppMetaData", "AppReference", "AppSpecStateDict", "ApplicationClient", "ApplicationSpecification", + "AssetManager", "CallConfig", + "ClientManager", "CommonCallParameters", "CommonCallParametersDict", "CreateCallParameters", @@ -131,6 +147,7 @@ "DispenserLimitResponse", "EnsureBalanceParameters", "EnsureFundedResponse", + "KmdAccountManager", "LogicError", "MethodConfigDict", "MethodHints", @@ -145,6 +162,7 @@ "TemplateValueDict", "TemplateValueMapping", "TestNetDispenserApiClient", + "TransactionComposer", "TransactionParameters", "TransactionParametersDict", "TransactionResponse", From 6bf71aab40cad5d251b370d806d276867272b69d Mon Sep 17 00:00:00 2001 From: Al Date: Fri, 13 Dec 2024 20:31:10 +0100 Subject: [PATCH 06/31] refactor: further aligning composer class; initial batch of resource population tests (#125) * fix: configure method tweaks; fixing appdeployresult init * test: adding first half of resource population tests; improving composer txn generation --- pyproject.toml | 4 + src/algokit_utils/accounts/account_manager.py | 141 ++++++---- .../applications/app_deployer.py | 13 +- src/algokit_utils/applications/app_manager.py | 12 +- src/algokit_utils/config.py | 12 +- src/algokit_utils/models/account.py | 76 ++++- .../transactions/transaction_composer.py | 260 ++++++++++-------- .../transactions/transaction_sender.py | 6 +- src/algokit_utils/transactions/utils.py | 157 ++++++++--- tests/artifacts/resource-packer/.gitignore | 3 + .../resource-packer/ExternalApp.arc32.json | 140 ++++++++++ .../resource-packer/ExternalAppV8.arc32.json | 69 +++++ .../ResourcePackerv8.arc32.json | 173 ++++++++++++ .../ResourcePackerv9.arc32.json | 173 ++++++++++++ .../resource-packer/resource-packer.algo.ts | 158 +++++++++++ tests/test_transaction_composer.py | 219 --------------- tests/transactions/test_resource_packing.py | 168 +++++++++++ .../transactions/test_transaction_composer.py | 127 ++++++++- .../transactions/test_transaction_creator.py | 3 +- tests/transactions/test_transaction_sender.py | 2 + 20 files changed, 1464 insertions(+), 452 deletions(-) create mode 100644 tests/artifacts/resource-packer/.gitignore create mode 100644 tests/artifacts/resource-packer/ExternalApp.arc32.json create mode 100644 tests/artifacts/resource-packer/ExternalAppV8.arc32.json create mode 100644 tests/artifacts/resource-packer/ResourcePackerv8.arc32.json create mode 100644 tests/artifacts/resource-packer/ResourcePackerv9.arc32.json create mode 100644 tests/artifacts/resource-packer/resource-packer.algo.ts delete mode 100644 tests/test_transaction_composer.py create mode 100644 tests/transactions/test_resource_packing.py diff --git a/pyproject.toml b/pyproject.toml index f5efc256..e663f468 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,6 +164,10 @@ untyped_calls_exclude = [ module = ["algosdk", "algosdk.*"] disallow_untyped_calls = false +[[tool.mypy.overrides]] +module = ["tests.transactions.test_transaction_composer"] +disable_error_code = ["call-overload", "union-attr"] + [tool.semantic_release] version_toml = "pyproject.toml:tool.poetry.version" remove_dist = false diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index d997a211..598676b1 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -4,16 +4,16 @@ from typing import Any from algosdk import mnemonic -from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.atomic_transaction_composer import LogicSigTransactionSigner, TransactionSigner from algosdk.mnemonic import to_private_key -from algosdk.transaction import SuggestedParams +from algosdk.transaction import LogicSigAccount, SuggestedParams from typing_extensions import Self from algokit_utils.accounts.kmd_account_manager import KmdAccountManager from algokit_utils.clients.client_manager import ClientManager from algokit_utils.clients.dispenser_api_client import DispenserAssetName, TestNetDispenserApiClient from algokit_utils.config import config -from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account +from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account, MultiSigAccount, MultisigMetadata from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( PaymentParams, @@ -52,7 +52,7 @@ def __init__(self, client_manager: ClientManager): """ self._client_manager = client_manager self._kmd_account_manager = KmdAccountManager(client_manager) - self._accounts = dict[str, Account]() + self._signers = dict[str, TransactionSigner]() self._default_signer: TransactionSigner | None = None def set_default_signer(self, signer: TransactionSigner) -> Self: @@ -73,17 +73,26 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: :param signer: The signer to sign transactions with for the given sender :return: The AccountCreator instance for method chaining """ - if isinstance(signer, AccountTransactionSigner): - self._accounts[sender] = Account(private_key=signer.private_key) + self._signers[sender] = signer return self def get_account(self, sender: str) -> Account: - account = self._accounts.get(sender) + account = self._signers.get(sender) if not account: raise ValueError(f"No account found for address {sender}") + if not isinstance(account, Account): + raise ValueError(f"Account {sender} is not a regular account") return account - def get_signer(self, sender: str | Account) -> TransactionSigner: + def get_logic_sig_account(self, sender: str) -> LogicSigAccount: + account = self._signers.get(sender) + if not account: + raise ValueError(f"No account found for address {sender}") + if not isinstance(account, LogicSigAccount): + raise ValueError(f"Account {sender} is not a logic sig account") + return account + + def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSigner: """ Returns the `TransactionSigner` for the given sender address. @@ -92,8 +101,7 @@ def get_signer(self, sender: str | Account) -> TransactionSigner: :param sender: The sender address :return: The `TransactionSigner` or throws an error if not found """ - account = self._accounts.get(self._get_address(sender)) - signer = account.signer if account else self._default_signer + signer = self._signers.get(self._get_address(sender)) if not signer: raise ValueError(f"No signer found for address {sender}") return signer @@ -109,29 +117,48 @@ def get_information(self, sender: str | Account) -> dict[str, Any]: assert isinstance(info, dict) return info - def from_mnemonic(self, mnemonic: str) -> Account: - private_key = to_private_key(mnemonic) + def _register_account(self, private_key: str) -> Account: + """Helper method to create and register an account with its signer. + + Args: + private_key: The private key for the account + + Returns: + The registered Account instance + """ account = Account(private_key=private_key) - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) + self._signers[account.address] = account.signer return account + def _register_logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + logic_sig = LogicSigAccount(program, args) + self._signers[logic_sig.address()] = LogicSigTransactionSigner(logic_sig) + return logic_sig + + def _register_multi_sig( + self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] + ) -> MultiSigAccount: + msig_account = MultiSigAccount( + MultisigMetadata(version=version, threshold=threshold, addresses=addrs), + signing_accounts, + ) + self._signers[str(msig_account.address)] = msig_account.signer + return msig_account + + def from_mnemonic(self, mnemonic: str) -> Account: + private_key = to_private_key(mnemonic) + return self._register_account(private_key) + def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Account: account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") if account_mnemonic: private_key = mnemonic.to_private_key(account_mnemonic) - account = Account(private_key=private_key) - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) - return account + return self._register_account(private_key) if self._client_manager.is_local_net(): kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with) - account = Account(private_key=kmd_account.private_key) - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) - return account + return self._register_account(kmd_account.private_key) raise ValueError(f"Missing environment variable {name.upper()}_MNEMONIC when looking for account {name}") @@ -142,14 +169,38 @@ def from_kmd( if not kmd_account: raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}") - account = Account(private_key=kmd_account.private_key) - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) - return account + return self._register_account(kmd_account.private_key) + + def logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + return self._register_logic_sig(program, args) + + def multi_sig( + self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] + ) -> MultiSigAccount: + return self._register_multi_sig(version, threshold, addrs, signing_accounts) + + def random(self) -> Account: + """ + Tracks and returns a new, random Algorand account. + + :return: The account + """ + account = Account.new_account() + return self._register_account(account.private_key) + + def localnet_dispenser(self) -> Account: + kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() + return self._register_account(kmd_account.private_key) + + def dispenser_from_environment(self) -> Account: + name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") + if name: + return self.from_environment(DISPENSER_ACCOUNT_NAME) + return self.localnet_dispenser() def rekeyed(self, sender: Account | str, account: Account) -> Account: sender_address = sender.address if isinstance(sender, Account) else sender - self._accounts[sender_address] = account + self._signers[sender_address] = account.signer return Account(address=sender_address, private_key=account.private_key) def rekey_account( # noqa: PLR0913 @@ -223,30 +274,6 @@ def rekey_account( # noqa: PLR0913 return result - def random(self) -> Account: - """ - Tracks and returns a new, random Algorand account. - - :return: The account - """ - account = Account.new_account() - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=account.private_key)) - return account - - def localnet_dispenser(self) -> Account: - kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() - account = Account(private_key=kmd_account.private_key) - self._accounts[account.address] = account - self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) - return account - - def dispenser_from_environment(self) -> Account: - name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") - if name: - return self.from_environment(DISPENSER_ACCOUNT_NAME) - return self.localnet_dispenser() - def ensure_funded( # noqa: PLR0913 self, account_to_fund: str | Account, @@ -448,8 +475,16 @@ def ensure_funded_from_testnet_dispenser_api( amount_funded=AlgoAmount.from_micro_algo(result.amount), ) - def _get_address(self, sender: str | Account) -> str: - return sender.address if isinstance(sender, Account) else sender + def _get_address(self, sender: str | Account | LogicSigAccount) -> str: + match sender: + case Account(): + return sender.address + case LogicSigAccount(): + return sender.address() + case str(): + return sender + case _: + raise ValueError(f"Unknown sender type: {type(sender)}") def _get_composer(self, get_suggested_params: Callable[[], SuggestedParams] | None = None) -> TransactionComposer: if get_suggested_params is None: diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 357d808c..c3cc5853 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -268,12 +268,13 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResult: clear_program=clear_program, ) - return AppDeployResult( - **existing_app.__dict__, - operation_performed=OperationPerformed.Nothing, - app_id=existing_app.app_id, - app_address=existing_app.app_address, - ) + existing_app_dict = existing_app.__dict__ + existing_app_dict["operation_performed"] = OperationPerformed.Nothing + existing_app_dict["app_id"] = existing_app.app_id + existing_app_dict["app_address"] = existing_app.app_address + + logger.debug("No detected changes in app, nothing to do.", suppress_log=deployment.suppress_log) + return AppDeployResult(**existing_app_dict) def _create_app( self, diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index e282ede7..9ad6c5fe 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -54,14 +54,22 @@ class DataTypeFlag(IntEnum): class BoxReference(AlgosdkBoxReference): - def __init__(self, app_id: int, name: bytes): - super().__init__(app_index=app_id, name=name) + def __init__(self, app_id: int, name: bytes | str): + super().__init__(app_index=app_id, name=self._b64_decode(name)) def __eq__(self, other: object) -> bool: if isinstance(other, (BoxReference | AlgosdkBoxReference)): return self.app_index == other.app_index and self.name == other.name return False + def _b64_decode(self, value: str | bytes) -> bytes: + if isinstance(value, str): + try: + return base64.b64decode(value) + except Exception: + return value.encode("utf-8") + return value + def _is_valid_token_character(char: str) -> bool: return char.isalnum() or char == "_" diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index f76704ce..eb788910 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -127,11 +127,12 @@ def with_debug(self, func: Callable[[], str | None]) -> None: def configure( self, *, - debug: bool, + debug: bool | None = None, project_root: Path | None = None, trace_all: bool = False, trace_buffer_size_mb: float = 256, max_search_depth: int = 10, + populate_app_call_resources: bool = False, ) -> None: """ Configures various settings for the application. @@ -153,16 +154,17 @@ def configure( None """ - self._debug = debug - - if project_root: + if debug is not None: + self._debug = debug + if project_root is not None: self._project_root = project_root.resolve(strict=True) - elif debug and ALGOKIT_PROJECT_ROOT: + elif debug is not None and ALGOKIT_PROJECT_ROOT: self._project_root = Path(ALGOKIT_PROJECT_ROOT).resolve(strict=True) self._trace_all = trace_all self._trace_buffer_size_mb = trace_buffer_size_mb self._max_search_depth = max_search_depth + self._populate_app_call_resources = populate_app_call_resources config = UpdatableConfig() diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index f83cc1e2..8b5da485 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -1,7 +1,9 @@ import dataclasses import algosdk -from algosdk.atomic_transaction_composer import AccountTransactionSigner +import algosdk.atomic_transaction_composer +from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.transaction import Multisig, MultisigTransaction DISPENSER_ACCOUNT_NAME = "DISPENSER" @@ -35,3 +37,75 @@ def signer(self) -> AccountTransactionSigner: def new_account() -> "Account": private_key, address = algosdk.account.generate_account() return Account(private_key=private_key) + + +@dataclasses.dataclass(kw_only=True) +class MultisigMetadata: + version: int + threshold: int + addresses: list[str] + + +@dataclasses.dataclass(kw_only=True) +class MultiSigAccount: + """Account wrapper that supports partial or full multisig signing.""" + + _params: MultisigMetadata + _signing_accounts: list[Account] + _addr: str + _signer: TransactionSigner + _multisig: Multisig + + def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[Account]) -> None: + """Initialize a new multisig account. + + Args: + multisig_params: The parameters for the multisig account + signing_accounts: The list of accounts that can sign + """ + self._params = multisig_params + self._signing_accounts = signing_accounts + self._multisig = Multisig(multisig_params.version, multisig_params.threshold, multisig_params.addresses) + self._addr = str(self._multisig.address()) + self._signer = algosdk.atomic_transaction_composer.MultisigTransactionSigner( + self._multisig, + [account.private_key for account in signing_accounts], + ) + + @property + def params(self) -> MultisigMetadata: + """The parameters for the multisig account.""" + return self._params + + @property + def signing_accounts(self) -> list[Account]: + """The list of accounts that are present to sign.""" + return self._signing_accounts + + @property + def address(self) -> str: + """The address of the multisig account.""" + return self._addr + + @property + def signer(self) -> TransactionSigner: + """The transaction signer for this multisig account.""" + return self._signer + + def sign(self, transaction: algosdk.transaction.Transaction) -> MultisigTransaction: + """Sign the given transaction. + + Args: + transaction: Either a transaction object or a raw, partially signed transaction + + Returns: + The transaction signed by the present signers + """ + msig_txn = MultisigTransaction( + transaction, + self._multisig, + ) + for signer in self._signing_accounts: + msig_txn.sign(signer.private_key) + + return msig_txn diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 7d66c939..ccb6e199 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -274,7 +274,7 @@ class AppCallParams(CommonTxnParams, SenderParam): :param box_references: Box references. """ - on_complete: OnComplete | None = None + on_complete: OnComplete app_id: int | None = None approval_program: str | bytes | None = None clear_state_program: str | bytes | None = None @@ -372,6 +372,7 @@ class AppMethodCall(CommonTxnParams, SenderParam): app_references: list[int] | None = None asset_references: list[int] | None = None box_references: list[BoxReference] | None = None + schema: dict[str, int] | None = None @dataclass(kw_only=True, frozen=True) @@ -560,7 +561,8 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 transactions_with_signer = atc.build_group() if populate_resources or ( - config.populate_app_call_resource + populate_resources is None + and config.populate_app_call_resource and any(isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer) ): atc = populate_app_call_resources(atc, algod) @@ -971,36 +973,32 @@ def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSign def _common_txn_build_step( self, + build_txn: Callable[[dict], algosdk.transaction.Transaction], params: CommonTxnParams, - txn: algosdk.transaction.Transaction, - suggested_params: algosdk.transaction.SuggestedParams, + txn_params: dict, ) -> algosdk.transaction.Transaction: + # Clone suggested params + txn_params["sp"] = ( + algosdk.transaction.SuggestedParams(**txn_params["sp"].__dict__) if "sp" in txn_params else None + ) + if params.lease: - txn.lease = encode_lease(params.lease) + txn_params["lease"] = encode_lease(params.lease) if params.rekey_to: - txn.rekey_to = params.rekey_to + txn_params["rekey_to"] = params.rekey_to if params.note: - txn.note = params.note - - if params.first_valid_round: - txn.first_valid_round = params.first_valid_round + txn_params["note"] = params.note - if params.last_valid_round: - txn.last_valid_round = params.last_valid_round - else: - txn.last_valid_round = txn.first_valid_round + (params.validity_window or self.default_validity_window) + if params.static_fee is not None and txn_params["sp"]: + txn_params["sp"].fee = params.static_fee.micro_algos + txn_params["sp"].flat_fee = True - if params.static_fee is not None and params.extra_fee is not None: - raise ValueError("Cannot set both static_fee and extra_fee") + txn = build_txn(txn_params) - if params.static_fee is not None: - txn.fee = params.static_fee.micro_algos - else: - txn.fee = txn.estimate_size() * suggested_params.fee or algosdk.constants.min_txn_fee - if params.extra_fee: - txn.fee += params.extra_fee + if params.extra_fee: + txn.fee += params.extra_fee.micro_algos - if params.max_fee is not None and txn.fee > params.max_fee: + if params.max_fee and txn.fee > params.max_fee.micro_algos: raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}") return txn @@ -1064,62 +1062,80 @@ def _build_method_call( # noqa: C901, PLR0912 method_atc = AtomicTransactionComposer() - method_atc.add_method_call( - app_id=params.app_id or 0, - method=params.method, - sender=params.sender, - sp=suggested_params, - signer=params.signer or self.get_signer(params.sender), - method_args=method_args, - on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC, - note=params.note, - lease=params.lease, - boxes=[AppManager.get_box_reference(ref) for ref in params.box_references] + txn_params = { + "app_id": params.app_id or 0, + "method": params.method, + "sender": params.sender, + "sp": suggested_params, + "signer": params.signer or self.get_signer(params.sender), + "method_args": method_args, + "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, + "note": params.note, + "lease": params.lease, + "boxes": [AppManager.get_box_reference(ref) for ref in params.box_references] if params.box_references else None, - foreign_apps=params.app_references, - foreign_assets=params.asset_references, - accounts=params.account_references, - approval_program=params.approval_program if hasattr(params, "approval_program") else None, # type: ignore[arg-type] - clear_program=params.clear_state_program if hasattr(params, "clear_state_program") else None, # type: ignore[arg-type] - rekey_to=params.rekey_to, - ) + "foreign_apps": params.app_references, + "foreign_assets": params.asset_references, + "accounts": params.account_references, + "global_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("global_ints", 0), + num_byte_slices=params.schema.get("global_bytes", 0), + ) + if params.schema + else None, + "local_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("local_ints", 0), + num_byte_slices=params.schema.get("local_bytes", 0), + ) + if params.schema + else None, + "approval_program": params.approval_program if hasattr(params, "approval_program") else None, + "clear_program": params.clear_state_program if hasattr(params, "clear_state_program") else None, + "rekey_to": params.rekey_to, + } + + def _add_method_call_and_return_txn(x: dict) -> algosdk.transaction.Transaction: + method_atc.add_method_call(**x) + return method_atc.build_group()[-1].txn + + self._common_txn_build_step(lambda x: _add_method_call_and_return_txn(x), params, txn_params) return self._build_atc(method_atc) def _build_payment( self, params: PaymentParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.PaymentTxn( - sender=params.sender, - sp=suggested_params, - receiver=params.receiver, - amt=params.amount.micro_algos, - close_remainder_to=params.close_remainder_to, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "receiver": params.receiver, + "amt": params.amount.micro_algos, + "close_remainder_to": params.close_remainder_to, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.PaymentTxn(**x), params, txn_params) def _build_asset_create( self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetCreateTxn( - sender=params.sender, - sp=suggested_params, - total=params.total, - default_frozen=params.default_frozen or False, - unit_name=params.unit_name or "", - asset_name=params.asset_name or "", - manager=params.manager, - reserve=params.reserve, - freeze=params.freeze, - clawback=params.clawback, - url=params.url or "", - metadata_hash=params.metadata_hash, - decimals=params.decimals or 0, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "total": params.total, + "default_frozen": params.default_frozen or False, + "unit_name": params.unit_name or "", + "asset_name": params.asset_name or "", + "manager": params.manager, + "reserve": params.reserve, + "freeze": params.freeze, + "clawback": params.clawback, + "url": params.url or "", + "metadata_hash": params.metadata_hash, + "decimals": params.decimals or 0, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetCreateTxn(**x), params, txn_params) def _build_app_call( self, @@ -1158,6 +1174,8 @@ def _build_app_call( "clear_program": clear_program, } + txn_params = {**sdk_params, "index": app_id} + if not app_id and isinstance(params, AppCreateParams): if not sdk_params["approval_program"] or not sdk_params["clear_program"]: raise ValueError("approval_program and clear_program are required for application creation") @@ -1165,98 +1183,96 @@ def _build_app_call( if not params.schema: raise ValueError("schema is required for application creation") - txn = algosdk.transaction.ApplicationCreateTxn( - **sdk_params, - global_schema=algosdk.transaction.StateSchema( + txn_params = { + **txn_params, + "global_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("global_ints", 0), num_byte_slices=params.schema.get("global_bytes", 0), ), - local_schema=algosdk.transaction.StateSchema( + "local_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("local_ints", 0), num_byte_slices=params.schema.get("local_bytes", 0), ), - extra_pages=params.extra_program_pages + "extra_pages": params.extra_program_pages or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) if params.extra_program_pages else 0, - ) - else: - txn = algosdk.transaction.ApplicationCallTxn(**sdk_params, index=app_id) # type: ignore[assignment] + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.ApplicationCallTxn(**x), params, txn_params) def _build_asset_config( self, params: AssetConfigParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetConfigTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - manager=params.manager, - reserve=params.reserve, - freeze=params.freeze, - clawback=params.clawback, - strict_empty_address_check=False, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + "manager": params.manager, + "reserve": params.reserve, + "freeze": params.freeze, + "clawback": params.clawback, + "strict_empty_address_check": False, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetConfigTxn(**x), params, txn_params) def _build_asset_destroy( self, params: AssetDestroyParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetDestroyTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetDestroyTxn(**x), params, txn_params) def _build_asset_freeze( self, params: AssetFreezeParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetFreezeTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - target=params.account, - new_freeze_state=params.frozen, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + "target": params.account, + "new_freeze_state": params.frozen, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetFreezeTxn(**x), params, txn_params) def _build_asset_transfer( self, params: AssetTransferParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetTransferTxn( - sender=params.sender, - sp=suggested_params, - receiver=params.receiver, - amt=params.amount, - index=params.asset_id, - close_assets_to=params.close_asset_to, - revocation_target=params.clawback_target, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "receiver": params.receiver, + "amt": params.amount, + "index": params.asset_id, + "close_assets_to": params.close_asset_to, + "revocation_target": params.clawback_target, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetTransferTxn(**x), params, txn_params) def _build_key_reg( self, params: OnlineKeyRegistrationParams, suggested_params: algosdk.transaction.SuggestedParams ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.KeyregTxn( - sender=params.sender, - sp=suggested_params, - votekey=params.vote_key, - selkey=params.selection_key, - votefst=params.vote_first, - votelst=params.vote_last, - votekd=params.vote_key_dilution, - rekey_to=params.rekey_to, - nonpart=False, - sprfkey=params.state_proof_key, - ) + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "votekey": params.vote_key, + "selkey": params.selection_key, + "votefst": params.vote_first, + "votelst": params.vote_last, + "votekd": params.vote_key_dilution, + "rekey_to": params.rekey_to, + "nonpart": False, + "sprfkey": params.state_proof_key, + } - return self._common_txn_build_step(params, txn, suggested_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.KeyregTxn(**x), params, txn_params) def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool: if isinstance(x, list | tuple): diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 831100d2..31fa15a4 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -112,7 +112,11 @@ def send_transaction(params: T) -> SendSingleTransactionResult: transaction = composer.build().transactions[-1].txn logger.debug(pre_log(params, transaction)) - raw_result = composer.send() + raw_result = composer.send( + populate_app_call_resources=params.populate_app_call_resources, + max_rounds_to_wait=params.max_rounds_to_wait, + suppress_log=params.suppress_log, + ) raw_result_dict = raw_result.__dict__.copy() raw_result_dict["transactions"] = raw_result.transactions del raw_result_dict["simulate_response"] diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py index 216db262..6ea09224 100644 --- a/src/algokit_utils/transactions/utils.py +++ b/src/algokit_utils/transactions/utils.py @@ -1,17 +1,58 @@ +import base64 +from copy import deepcopy from typing import Any, cast from algosdk import logic, transaction from algosdk.atomic_transaction_composer import AtomicTransactionComposer, EmptySigner, TransactionWithSigner -from algosdk.box_reference import BoxReference from algosdk.error import AtomicTransactionComposerError from algosdk.v2client.algod import AlgodClient from algosdk.v2client.models import SimulateRequest +from algokit_utils.applications.app_manager import BoxReference + # Constants MAX_APP_CALL_ACCOUNT_REFERENCES = 4 MAX_APP_CALL_FOREIGN_REFERENCES = 8 +def _find_available_transaction_index( + txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int +) -> int: + """Find index of first transaction that can accommodate the new reference.""" + + def check_transaction(txn: TransactionWithSigner) -> bool: + # Skip if not an application call transaction + if txn.txn.type != "appl": + return False + + # Get current counts (using get() with default 0 for Pythonic null handling) + accounts = len(getattr(txn.txn, "accounts", []) or []) + assets = len(getattr(txn.txn, "foreign_assets", []) or []) + apps = len(getattr(txn.txn, "foreign_apps", []) or []) + boxes = len(getattr(txn.txn, "boxes", []) or []) + + # For account references, only check account limit + if reference_type == "account": + return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + + # For asset holdings or local state, need space for both account and other reference + if reference_type in ("asset_holding", "app_local"): + return ( + accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + ) + + # For boxes with non-zero app ID, need space for box and app reference + if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0: + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + + # Default case - just check total references + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Return first matching index or -1 if none found + return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1) + + def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: # noqa: C901, PLR0915, PLR0912 """ Populate application call resources based on simulation results. @@ -26,7 +67,7 @@ def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClie continue # Validate no unexpected resources - if txn_resources.get("boxes") or txn_resources.get("extraBoxRefs"): + if txn_resources.get("boxes") or txn_resources.get("extra-box-refs"): raise ValueError("Unexpected boxes at the transaction level") if txn_resources.get("appLocals"): raise ValueError("Unexpected app local at the transaction level") @@ -64,29 +105,28 @@ def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClie app_txn.foreign_assets = foreign_assets app_txn.boxes = boxes - def populate_group_resource( # noqa: C901, PLR0915 - txns: list[TransactionWithSigner], reference: str | BoxReference | dict[str, Any] | int, ref_type: str + def populate_group_resource( # noqa: C901, PLR0912, PLR0915 + txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str ) -> None: - """Helper function to populate group-level resources""" + """Helper function to populate group-level resources matching TypeScript implementation""" def is_appl_below_limit(t: TransactionWithSigner) -> bool: if not isinstance(t.txn, transaction.ApplicationCallTxn): return False - app_txn = t.txn - accounts = len(app_txn.accounts or []) - assets = len(app_txn.foreign_assets or []) - apps = len(app_txn.foreign_apps or []) - boxes = len(app_txn.boxes or []) + accounts = len(getattr(t.txn, "accounts", []) or []) + assets = len(getattr(t.txn, "foreign_assets", []) or []) + apps = len(getattr(t.txn, "foreign_apps", []) or []) + boxes = len(getattr(t.txn, "boxes", []) or []) return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - # Handle asset holding and app local references + # Handle asset holding and app local references first if ref_type in ("assetHolding", "appLocal"): ref_dict = cast(dict[str, Any], reference) account = ref_dict["account"] - # Try to find transaction with account already available + # First try to find transaction with account already available txn_idx = next( ( i @@ -100,7 +140,7 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: logic.get_application_address(app_id) for app_id in (getattr(t.txn, "foreign_apps", []) or []) ) - or any(account in str(v) for v in t.txn.__dict__.values()) + or any(str(account) in str(v) for v in t.txn.__dict__.values()) ) ), -1, @@ -116,48 +156,90 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] return + # Try to find transaction that already has the app/asset available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES + and ( + ( + ref_type == "assetHolding" + and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or []) + ) + or ( + ref_type == "appLocal" + and ( + ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or []) + or t.txn.index == ref_dict["app"] + ) + ) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(account) + app_txn.accounts = accounts + return + + # Handle box references + if ref_type == "box": + box_ref: tuple[int, bytes] = (reference["app"], base64.b64decode(reference["name"])) # type: ignore # noqa: PGH003 + + # Try to find transaction that already has the app available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0]) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) + app_txn.boxes = boxes + return + # Find available transaction for the resource - txn_idx = next( - ( - i - for i, t in enumerate(txns) - if is_appl_below_limit(t) - and isinstance(t.txn, transaction.ApplicationCallTxn) - and ( - len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES - if ref_type == "account" - else True - ) - ), - -1, - ) + txn_idx = _find_available_transaction_index(txns, ref_type, reference) if txn_idx == -1: raise ValueError("No more transactions below reference limit. Add another app call to the group.") app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) - # Add resource based on type if ref_type == "account": accounts = list(getattr(app_txn, "accounts", []) or []) accounts.append(cast(str, reference)) app_txn.accounts = accounts elif ref_type == "app": + app_id = int(cast(str | int, reference)) foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_apps.append(int(cast(str | int, reference))) + foreign_apps.append(app_id) app_txn.foreign_apps = foreign_apps elif ref_type == "box": - box_ref = cast(BoxReference, reference) boxes = list(getattr(app_txn, "boxes", []) or []) - boxes.append(box_ref) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) app_txn.boxes = boxes - if box_ref.app_index != 0: + if box_ref[0] != 0: foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_apps.append(box_ref.app_index) + foreign_apps.append(box_ref[0]) app_txn.foreign_apps = foreign_apps elif ref_type == "asset": + asset_id = int(cast(str | int, reference)) foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) - foreign_assets.append(int(cast(str | int, reference))) + foreign_assets.append(asset_id) app_txn.foreign_assets = foreign_assets elif ref_type == "assetHolding": ref_dict = cast(dict[str, Any], reference) @@ -218,10 +300,9 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: populate_group_resource(group, app, "app") # Handle extra box references - extra_box_refs = group_resources.get("extraBoxRefs", 0) + extra_box_refs = group_resources.get("extra-box-refs", 0) for _ in range(extra_box_refs): - empty_box = BoxReference(0, b"") - populate_group_resource(group, empty_box, "box") + populate_group_resource(group, {"app": 0, "name": ""}, "box") # Create new ATC with updated transactions new_atc = AtomicTransactionComposer() @@ -230,7 +311,7 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: new_atc.add_transaction(txn_with_signer) # Copy method calls - new_atc.method_dict = atc.method_dict.copy() + new_atc.method_dict = deepcopy(atc.method_dict) return new_atc diff --git a/tests/artifacts/resource-packer/.gitignore b/tests/artifacts/resource-packer/.gitignore new file mode 100644 index 00000000..edd15a59 --- /dev/null +++ b/tests/artifacts/resource-packer/.gitignore @@ -0,0 +1,3 @@ +*.teal +*.json +!*.arc32.json diff --git a/tests/artifacts/resource-packer/ExternalApp.arc32.json b/tests/artifacts/resource-packer/ExternalApp.arc32.json new file mode 100644 index 00000000..88424a70 --- /dev/null +++ b/tests/artifacts/resource-packer/ExternalApp.arc32.json @@ -0,0 +1,140 @@ +{ + "hints": { + "optInToApplication()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "dummy()void": { + "call_config": { + "no_op": "CALL" + } + }, + "error()void": { + "call_config": { + "no_op": "CALL" + } + }, + "boxWithPayment(pay)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createAsset()void": { + "call_config": { + "no_op": "CALL" + } + }, + "senderAssetBalance()void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": { + "localKey": { + "type": "bytes", + "key": "localKey" + } + }, + "reserved": {} + }, + "global": { + "declared": { + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 1 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgovLyBUaGlzIFRFQUwgd2FzIGdlbmVyYXRlZCBieSBURUFMU2NyaXB0IHYwLjg3LjAKLy8gaHR0cHM6Ly9naXRodWIuY29tL2FsZ29yYW5kZm91bmRhdGlvbi9URUFMU2NyaXB0CgovLyBUaGlzIGNvbnRyYWN0IGlzIGNvbXBsaWFudCB3aXRoIGFuZC9vciBpbXBsZW1lbnRzIHRoZSBmb2xsb3dpbmcgQVJDczogWyBBUkM0IF0KCi8vIFRoZSBmb2xsb3dpbmcgdGVuIGxpbmVzIG9mIFRFQUwgaGFuZGxlIGluaXRpYWwgcHJvZ3JhbSBmbG93Ci8vIFRoaXMgcGF0dGVybiBpcyB1c2VkIHRvIG1ha2UgaXQgZWFzeSBmb3IgYW55b25lIHRvIHBhcnNlIHRoZSBzdGFydCBvZiB0aGUgcHJvZ3JhbSBhbmQgZGV0ZXJtaW5lIGlmIGEgc3BlY2lmaWMgYWN0aW9uIGlzIGFsbG93ZWQKLy8gSGVyZSwgYWN0aW9uIHJlZmVycyB0byB0aGUgT25Db21wbGV0ZSBpbiBjb21iaW5hdGlvbiB3aXRoIHdoZXRoZXIgdGhlIGFwcCBpcyBiZWluZyBjcmVhdGVkIG9yIGNhbGxlZAovLyBFdmVyeSBwb3NzaWJsZSBhY3Rpb24gZm9yIHRoaXMgY29udHJhY3QgaXMgcmVwcmVzZW50ZWQgaW4gdGhlIHN3aXRjaCBzdGF0ZW1lbnQKLy8gSWYgdGhlIGFjdGlvbiBpcyBub3QgaW1wbGVtZW50ZWQgaW4gdGhlIGNvbnRyYWN0LCBpdHMgcmVzcGVjdGl2ZSBicmFuY2ggd2lsbCBiZSAiKk5PVF9JTVBMRU1FTlRFRCIgd2hpY2gganVzdCBjb250YWlucyAiZXJyIgp0eG4gQXBwbGljYXRpb25JRAohCmludCA2CioKdHhuIE9uQ29tcGxldGlvbgorCnN3aXRjaCAqY2FsbF9Ob09wICpjYWxsX09wdEluICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKmNyZWF0ZV9Ob09wICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRAoKKk5PVF9JTVBMRU1FTlRFRDoKCWVycgoKLy8gb3B0SW5Ub0FwcGxpY2F0aW9uKCl2b2lkCiphYmlfcm91dGVfb3B0SW5Ub0FwcGxpY2F0aW9uOgoJLy8gZXhlY3V0ZSBvcHRJblRvQXBwbGljYXRpb24oKXZvaWQKCWNhbGxzdWIgb3B0SW5Ub0FwcGxpY2F0aW9uCglpbnQgMQoJcmV0dXJuCgovLyBvcHRJblRvQXBwbGljYXRpb24oKTogdm9pZApvcHRJblRvQXBwbGljYXRpb246Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTIKCS8vIHRoaXMubG9jYWxLZXkodGhpcy50eG4uc2VuZGVyKS52YWx1ZSA9ICdmb28nCgl0eG4gU2VuZGVyCglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglieXRlIDB4NjY2ZjZmIC8vICJmb28iCglhcHBfbG9jYWxfcHV0CglyZXRzdWIKCi8vIGR1bW15KCl2b2lkCiphYmlfcm91dGVfZHVtbXk6CgkvLyBleGVjdXRlIGR1bW15KCl2b2lkCgljYWxsc3ViIGR1bW15CglpbnQgMQoJcmV0dXJuCgovLyBkdW1teSgpOiB2b2lkCmR1bW15OgoJcHJvdG8gMCAwCglyZXRzdWIKCi8vIGVycm9yKCl2b2lkCiphYmlfcm91dGVfZXJyb3I6CgkvLyBleGVjdXRlIGVycm9yKCl2b2lkCgljYWxsc3ViIGVycm9yCglpbnQgMQoJcmV0dXJuCgovLyBlcnJvcigpOiB2b2lkCmVycm9yOgoJcHJvdG8gMCAwCgllcnIKCi8vIGJveFdpdGhQYXltZW50KHBheSl2b2lkCiphYmlfcm91dGVfYm94V2l0aFBheW1lbnQ6CgkvLyBfcGF5bWVudDogcGF5Cgl0eG4gR3JvdXBJbmRleAoJaW50IDEKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBwYXkKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGJveFdpdGhQYXltZW50KHBheSl2b2lkCgljYWxsc3ViIGJveFdpdGhQYXltZW50CglpbnQgMQoJcmV0dXJuCgovLyBib3hXaXRoUGF5bWVudChfcGF5bWVudDogUGF5VHhuKTogdm9pZApib3hXaXRoUGF5bWVudDoKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoyMgoJLy8gdGhpcy5ib3hLZXkudmFsdWUgPSAnZm9vJwoJYnl0ZSAweDYyNmY3ODRiNjU3OSAvLyAiYm94S2V5IgoJZHVwCglib3hfZGVsCglwb3AKCWJ5dGUgMHg2NjZmNmYgLy8gImZvbyIKCWJveF9wdXQKCXJldHN1YgoKLy8gY3JlYXRlQXNzZXQoKXZvaWQKKmFiaV9yb3V0ZV9jcmVhdGVBc3NldDoKCS8vIGV4ZWN1dGUgY3JlYXRlQXNzZXQoKXZvaWQKCWNhbGxzdWIgY3JlYXRlQXNzZXQKCWludCAxCglyZXR1cm4KCi8vIGNyZWF0ZUFzc2V0KCk6IHZvaWQKY3JlYXRlQXNzZXQ6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MjYKCS8vIHRoaXMuYXNhLnZhbHVlID0gc2VuZEFzc2V0Q3JlYXRpb24oewoJLy8gICAgICAgY29uZmlnQXNzZXRUb3RhbDogMSwKCS8vICAgICB9KQoJYnl0ZSAweDYxNzM2MSAvLyAiYXNhIgoJaXR4bl9iZWdpbgoJaW50IGFjZmcKCWl0eG5fZmllbGQgVHlwZUVudW0KCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MjcKCS8vIGNvbmZpZ0Fzc2V0VG90YWw6IDEKCWludCAxCglpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VG90YWwKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglpdHhuIENyZWF0ZWRBc3NldElECglhcHBfZ2xvYmFsX3B1dAoJcmV0c3ViCgovLyBzZW5kZXJBc3NldEJhbGFuY2UoKXZvaWQKKmFiaV9yb3V0ZV9zZW5kZXJBc3NldEJhbGFuY2U6CgkvLyBleGVjdXRlIHNlbmRlckFzc2V0QmFsYW5jZSgpdm9pZAoJY2FsbHN1YiBzZW5kZXJBc3NldEJhbGFuY2UKCWludCAxCglyZXR1cm4KCi8vIHNlbmRlckFzc2V0QmFsYW5jZSgpOiB2b2lkCnNlbmRlckFzc2V0QmFsYW5jZToKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czozMgoJLy8gYXNzZXJ0KCF0aGlzLnR4bi5zZW5kZXIuaXNPcHRlZEluVG9Bc3NldCh0aGlzLmFzYS52YWx1ZSkpCgl0eG4gU2VuZGVyCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCglzd2FwCglwb3AKCSEKCWFzc2VydAoJcmV0c3ViCgoqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uOgoJaW50IDEKCXJldHVybgoKKmNyZWF0ZV9Ob09wOgoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfY3JlYXRlQXBwbGljYXRpb24KCWVycgoKKmNhbGxfTm9PcDoKCW1ldGhvZCAiZHVtbXkoKXZvaWQiCgltZXRob2QgImVycm9yKCl2b2lkIgoJbWV0aG9kICJib3hXaXRoUGF5bWVudChwYXkpdm9pZCIKCW1ldGhvZCAiY3JlYXRlQXNzZXQoKXZvaWQiCgltZXRob2QgInNlbmRlckFzc2V0QmFsYW5jZSgpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfZHVtbXkgKmFiaV9yb3V0ZV9lcnJvciAqYWJpX3JvdXRlX2JveFdpdGhQYXltZW50ICphYmlfcm91dGVfY3JlYXRlQXNzZXQgKmFiaV9yb3V0ZV9zZW5kZXJBc3NldEJhbGFuY2UKCWVycgoKKmNhbGxfT3B0SW46CgltZXRob2QgIm9wdEluVG9BcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfb3B0SW5Ub0FwcGxpY2F0aW9uCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + }, + "contract": { + "name": "ExternalApp", + "desc": "", + "methods": [ + { + "name": "optInToApplication", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "dummy", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "error", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "boxWithPayment", + "args": [ + { + "name": "_payment", + "type": "pay" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createAsset", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "senderAssetBalance", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ExternalAppV8.arc32.json b/tests/artifacts/resource-packer/ExternalAppV8.arc32.json new file mode 100644 index 00000000..d8f07b6b --- /dev/null +++ b/tests/artifacts/resource-packer/ExternalAppV8.arc32.json @@ -0,0 +1,69 @@ +{ + "hints": { + "dummy()void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": {}, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDkKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuNjMuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsbWVudGVkIGluIHRoZSBjb250cmFjdCwgaXRzIHJlcHNlY3RpdmUgYnJhbmNoIHdpbGwgYmUgIk5PVF9JTVBMTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECmludCAwCj4KaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoIGNyZWF0ZV9Ob09wIE5PVF9JTVBMRU1FTlRFRCBOT1RfSU1QTEVNRU5URUQgTk9UX0lNUExFTUVOVEVEIE5PVF9JTVBMRU1FTlRFRCBOT1RfSU1QTEVNRU5URUQgY2FsbF9Ob09wCgpOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGR1bW15KCl2b2lkCmFiaV9yb3V0ZV9kdW1teToKCS8vIGV4ZWN1dGUgZHVtbXkoKXZvaWQKCWNhbGxzdWIgZHVtbXkKCWludCAxCglyZXR1cm4KCmR1bW15OgoJcHJvdG8gMCAwCglyZXRzdWIKCmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCmNyZWF0ZV9Ob09wOgoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoIGFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbgoJZXJyCgpjYWxsX05vT3A6CgltZXRob2QgImR1bW15KCl2b2lkIgoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAoJbWF0Y2ggYWJpX3JvdXRlX2R1bW15CgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDk=" + }, + "contract": { + "name": "ExternalAppV8", + "desc": "", + "methods": [ + { + "name": "dummy", + "args": [], + "desc": "", + "returns": { + "type": "void", + "desc": "" + } + }, + { + "name": "createApplication", + "desc": "", + "returns": { + "type": "void", + "desc": "" + }, + "args": [] + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json b/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json new file mode 100644 index 00000000..d5afe92f --- /dev/null +++ b/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json @@ -0,0 +1,173 @@ +{ + "hints": { + "bootstrap()void": { + "call_config": { + "no_op": "CALL" + } + }, + "addressBalance(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "smallBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "mediumBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalAppCall()void": { + "call_config": { + "no_op": "CALL" + } + }, + "assetTotal()void": { + "call_config": { + "no_op": "CALL" + } + }, + "hasAsset(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalLocal(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": { + "externalAppID": { + "type": "uint64", + "key": "externalAppID" + }, + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 2 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuODcuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoICpjYWxsX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpjcmVhdGVfTm9PcCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQKCipOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGJvb3RzdHJhcCgpdm9pZAoqYWJpX3JvdXRlX2Jvb3RzdHJhcDoKCS8vIGV4ZWN1dGUgYm9vdHN0cmFwKCl2b2lkCgljYWxsc3ViIGJvb3RzdHJhcAoJaW50IDEKCXJldHVybgoKLy8gYm9vdHN0cmFwKCk6IHZvaWQKYm9vdHN0cmFwOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjQ5CgkvLyBzZW5kTWV0aG9kQ2FsbDxbXSwgdm9pZD4oewoJLy8gICAgICAgbmFtZTogJ2NyZWF0ZUFwcGxpY2F0aW9uJywKCS8vICAgICAgIGFwcHJvdmFsUHJvZ3JhbTogRXh0ZXJuYWxBcHAuYXBwcm92YWxQcm9ncmFtKCksCgkvLyAgICAgICBjbGVhclN0YXRlUHJvZ3JhbTogRXh0ZXJuYWxBcHAuY2xlYXJQcm9ncmFtKCksCgkvLyAgICAgICBsb2NhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmxvY2FsLm51bUJ5dGVTbGljZSwKCS8vICAgICAgIGdsb2JhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1CeXRlU2xpY2UsCgkvLyAgICAgICBnbG9iYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEuZ2xvYmFsLm51bVVpbnQsCgkvLyAgICAgICBsb2NhbE51bVVpbnQ6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1VaW50LAoJLy8gICAgIH0pCglpdHhuX2JlZ2luCglpbnQgYXBwbAoJaXR4bl9maWVsZCBUeXBlRW51bQoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCWl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjUxCgkvLyBhcHByb3ZhbFByb2dyYW06IEV4dGVybmFsQXBwLmFwcHJvdmFsUHJvZ3JhbSgpCglieXRlIGI2NCBDaUFCQVNZQ0EyWnZid05oYzJFeEdCU0JCZ3N4R1FpTkRBQ0hBTFVBQUFBQUFBQUFBQUI1QUFBQUFBQUFBQUFBQUFDSUFBSWlRNG9BQURFQWdBaHNiMk5oYkV0bGVTaG1pWWdBQWlKRGlnQUFpWWdBQWlKRGlnQUFBREVXSWdsSk9CQWlFa1NJQUFJaVE0b0JBSUFHWW05NFMyVjVTYnhJS0wrSmlBQUNJa09LQUFBcHNZRURzaEFpc2lLQkFMSUJzN1E4WjRtSUFBSWlRNG9BQURFQUtXUndBRXhJRkVTSklrT0FCTGhFZXpZMkdnQ09BZi94QUlBRW93em4vNEFFUk5EYURZQUUxaDVDVllBRXBscXIvb0FFWlZ4ZUFqWWFBSTRGLzJUL2JmOTIvNWIvc0FDQUJBR2pvLzgyR2dDT0FmOC9BQT09CglpdHhuX2ZpZWxkIEFwcHJvdmFsUHJvZ3JhbQoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo1MgoJLy8gY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpCglieXRlIGI2NCBDZz09CglpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjUzCgkvLyBsb2NhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmxvY2FsLm51bUJ5dGVTbGljZQoJaW50IDEKCWl0eG5fZmllbGQgTG9jYWxOdW1CeXRlU2xpY2UKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6NTQKCS8vIGdsb2JhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1CeXRlU2xpY2UKCWludCAwCglpdHhuX2ZpZWxkIEdsb2JhbE51bUJ5dGVTbGljZQoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo1NQoJLy8gZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50CglpbnQgMQoJaXR4bl9maWVsZCBHbG9iYWxOdW1VaW50CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjU2CgkvLyBsb2NhbE51bVVpbnQ6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1VaW50CglpbnQgMAoJaXR4bl9maWVsZCBMb2NhbE51bVVpbnQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjU5CgkvLyB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUgPSB0aGlzLml0eG4uY3JlYXRlZEFwcGxpY2F0aW9uSUQKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWl0eG4gQ3JlYXRlZEFwcGxpY2F0aW9uSUQKCWFwcF9nbG9iYWxfcHV0CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjYxCgkvLyB0aGlzLmFzYS52YWx1ZSA9IHNlbmRBc3NldENyZWF0aW9uKHsKCS8vICAgICAgIGNvbmZpZ0Fzc2V0VG90YWw6IDEsCgkvLyAgICAgfSkKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWl0eG5fYmVnaW4KCWludCBhY2ZnCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjYyCgkvLyBjb25maWdBc3NldFRvdGFsOiAxCglpbnQgMQoJaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCgoJLy8gRmVlIGZpZWxkIG5vdCBzZXQsIGRlZmF1bHRpbmcgdG8gMAoJaW50IDAKCWl0eG5fZmllbGQgRmVlCgoJLy8gU3VibWl0IGlubmVyIHRyYW5zYWN0aW9uCglpdHhuX3N1Ym1pdAoJaXR4biBDcmVhdGVkQXNzZXRJRAoJYXBwX2dsb2JhbF9wdXQKCXJldHN1YgoKLy8gYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfYWRkcmVzc0JhbGFuY2U6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBhZGRyZXNzQmFsYW5jZShhZGRyZXNzKXZvaWQKCWNhbGxzdWIgYWRkcmVzc0JhbGFuY2UKCWludCAxCglyZXR1cm4KCi8vIGFkZHJlc3NCYWxhbmNlKGFkZHI6IEFkZHJlc3MpOiB2b2lkCmFkZHJlc3NCYWxhbmNlOgoJcHJvdG8gMSAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjY3CgkvLyBsb2cocmF3Qnl0ZXMoYWRkci5pc0luTGVkZ2VyKSkKCWZyYW1lX2RpZyAtMSAvLyBhZGRyOiBBZGRyZXNzCglhY2N0X3BhcmFtc19nZXQgQWNjdEJhbGFuY2UKCXN3YXAKCXBvcAoJYnl0ZSAweDAwCglpbnQgMAoJdW5jb3ZlciAyCglzZXRiaXQKCWxvZwoJcmV0c3ViCgovLyBzbWFsbEJveCgpdm9pZAoqYWJpX3JvdXRlX3NtYWxsQm94OgoJLy8gZXhlY3V0ZSBzbWFsbEJveCgpdm9pZAoJY2FsbHN1YiBzbWFsbEJveAoJaW50IDEKCXJldHVybgoKLy8gc21hbGxCb3goKTogdm9pZApzbWFsbEJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo3MQoJLy8gdGhpcy5zbWFsbEJveEtleS52YWx1ZSA9ICcnCglieXRlIDB4NzMgLy8gInMiCglkdXAKCWJveF9kZWwKCXBvcAoJYnl0ZSAweCAvLyAiIgoJYm94X3B1dAoJcmV0c3ViCgovLyBtZWRpdW1Cb3goKXZvaWQKKmFiaV9yb3V0ZV9tZWRpdW1Cb3g6CgkvLyBleGVjdXRlIG1lZGl1bUJveCgpdm9pZAoJY2FsbHN1YiBtZWRpdW1Cb3gKCWludCAxCglyZXR1cm4KCi8vIG1lZGl1bUJveCgpOiB2b2lkCm1lZGl1bUJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo3NQoJLy8gdGhpcy5tZWRpdW1Cb3hLZXkuY3JlYXRlKDVfMDAwKQoJYnl0ZSAweDZkIC8vICJtIgoJaW50IDVfMDAwCglib3hfY3JlYXRlCglwb3AKCXJldHN1YgoKLy8gZXh0ZXJuYWxBcHBDYWxsKCl2b2lkCiphYmlfcm91dGVfZXh0ZXJuYWxBcHBDYWxsOgoJLy8gZXhlY3V0ZSBleHRlcm5hbEFwcENhbGwoKXZvaWQKCWNhbGxzdWIgZXh0ZXJuYWxBcHBDYWxsCglpbnQgMQoJcmV0dXJuCgovLyBleHRlcm5hbEFwcENhbGwoKTogdm9pZApleHRlcm5hbEFwcENhbGw6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6NzkKCS8vIHNlbmRNZXRob2RDYWxsPFtdLCB2b2lkPih7CgkvLyAgICAgICBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUsCgkvLyAgICAgICBuYW1lOiAnZHVtbXknLAoJLy8gICAgIH0pCglpdHhuX2JlZ2luCglpbnQgYXBwbAoJaXR4bl9maWVsZCBUeXBlRW51bQoJbWV0aG9kICJkdW1teSgpdm9pZCIKCWl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjgwCgkvLyBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglyZXRzdWIKCi8vIGFzc2V0VG90YWwoKXZvaWQKKmFiaV9yb3V0ZV9hc3NldFRvdGFsOgoJLy8gZXhlY3V0ZSBhc3NldFRvdGFsKCl2b2lkCgljYWxsc3ViIGFzc2V0VG90YWwKCWludCAxCglyZXR1cm4KCi8vIGFzc2V0VG90YWwoKTogdm9pZAphc3NldFRvdGFsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjg2CgkvLyBhc3NlcnQodGhpcy5hc2EudmFsdWUudG90YWwpCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfcGFyYW1zX2dldCBBc3NldFRvdGFsCglwb3AKCWFzc2VydAoJcmV0c3ViCgovLyBoYXNBc3NldChhZGRyZXNzKXZvaWQKKmFiaV9yb3V0ZV9oYXNBc3NldDoKCS8vIGFkZHI6IGFkZHJlc3MKCXR4bmEgQXBwbGljYXRpb25BcmdzIDEKCWR1cAoJbGVuCglpbnQgMzIKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGhhc0Fzc2V0KGFkZHJlc3Mpdm9pZAoJY2FsbHN1YiBoYXNBc3NldAoJaW50IDEKCXJldHVybgoKLy8gaGFzQXNzZXQoYWRkcjogQWRkcmVzcyk6IHZvaWQKaGFzQXNzZXQ6Cglwcm90byAxIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6OTAKCS8vIGFzc2VydCghYWRkci5pc09wdGVkSW5Ub0Fzc2V0KHRoaXMuYXNhLnZhbHVlKSkKCWZyYW1lX2RpZyAtMSAvLyBhZGRyOiBBZGRyZXNzCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCglzd2FwCglwb3AKCSEKCWFzc2VydAoJcmV0c3ViCgovLyBleHRlcm5hbExvY2FsKGFkZHJlc3Mpdm9pZAoqYWJpX3JvdXRlX2V4dGVybmFsTG9jYWw6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBleHRlcm5hbExvY2FsKGFkZHJlc3Mpdm9pZAoJY2FsbHN1YiBleHRlcm5hbExvY2FsCglpbnQgMQoJcmV0dXJuCgovLyBleHRlcm5hbExvY2FsKGFkZHI6IEFkZHJlc3MpOiB2b2lkCmV4dGVybmFsTG9jYWw6Cglwcm90byAxIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6OTQKCS8vIGxvZyh0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUubG9jYWxTdGF0ZShhZGRyLCAnbG9jYWxLZXknKSBhcyBieXRlcykKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglmcmFtZV9kaWcgLTEgLy8gYWRkcjogQWRkcmVzcwoJY292ZXIgMgoJYXBwX2xvY2FsX2dldF9leAoJYXNzZXJ0Cglsb2cKCXJldHN1YgoKKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCipjcmVhdGVfTm9PcDoKCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uCgllcnIKCipjYWxsX05vT3A6CgltZXRob2QgImJvb3RzdHJhcCgpdm9pZCIKCW1ldGhvZCAiYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkIgoJbWV0aG9kICJzbWFsbEJveCgpdm9pZCIKCW1ldGhvZCAibWVkaXVtQm94KCl2b2lkIgoJbWV0aG9kICJleHRlcm5hbEFwcENhbGwoKXZvaWQiCgltZXRob2QgImFzc2V0VG90YWwoKXZvaWQiCgltZXRob2QgImhhc0Fzc2V0KGFkZHJlc3Mpdm9pZCIKCW1ldGhvZCAiZXh0ZXJuYWxMb2NhbChhZGRyZXNzKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2Jvb3RzdHJhcCAqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlICphYmlfcm91dGVfc21hbGxCb3ggKmFiaV9yb3V0ZV9tZWRpdW1Cb3ggKmFiaV9yb3V0ZV9leHRlcm5hbEFwcENhbGwgKmFiaV9yb3V0ZV9hc3NldFRvdGFsICphYmlfcm91dGVfaGFzQXNzZXQgKmFiaV9yb3V0ZV9leHRlcm5hbExvY2FsCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDg=" + }, + "contract": { + "name": "ResourcePackerv8", + "desc": "", + "methods": [ + { + "name": "bootstrap", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "addressBalance", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "smallBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "mediumBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "externalAppCall", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "assetTotal", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "hasAsset", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "externalLocal", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json b/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json new file mode 100644 index 00000000..2aa29555 --- /dev/null +++ b/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json @@ -0,0 +1,173 @@ +{ + "hints": { + "bootstrap()void": { + "call_config": { + "no_op": "CALL" + } + }, + "addressBalance(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "smallBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "mediumBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalAppCall()void": { + "call_config": { + "no_op": "CALL" + } + }, + "assetTotal()void": { + "call_config": { + "no_op": "CALL" + } + }, + "hasAsset(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalLocal(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": { + "externalAppID": { + "type": "uint64", + "key": "externalAppID" + }, + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 2 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDkKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuODcuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoICpjYWxsX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpjcmVhdGVfTm9PcCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQKCipOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGJvb3RzdHJhcCgpdm9pZAoqYWJpX3JvdXRlX2Jvb3RzdHJhcDoKCS8vIGV4ZWN1dGUgYm9vdHN0cmFwKCl2b2lkCgljYWxsc3ViIGJvb3RzdHJhcAoJaW50IDEKCXJldHVybgoKLy8gYm9vdHN0cmFwKCk6IHZvaWQKYm9vdHN0cmFwOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExMQoJLy8gc2VuZE1ldGhvZENhbGw8W10sIHZvaWQ+KHsKCS8vICAgICAgIG5hbWU6ICdjcmVhdGVBcHBsaWNhdGlvbicsCgkvLyAgICAgICBhcHByb3ZhbFByb2dyYW06IEV4dGVybmFsQXBwLmFwcHJvdmFsUHJvZ3JhbSgpLAoJLy8gICAgICAgY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpLAoJLy8gICAgICAgbG9jYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1CeXRlU2xpY2UsCgkvLyAgICAgICBnbG9iYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5nbG9iYWwubnVtQnl0ZVNsaWNlLAoJLy8gICAgICAgZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50LAoJLy8gICAgICAgbG9jYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEubG9jYWwubnVtVWludCwKCS8vICAgICB9KQoJaXR4bl9iZWdpbgoJaW50IGFwcGwKCWl0eG5fZmllbGQgVHlwZUVudW0KCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMTMKCS8vIGFwcHJvdmFsUHJvZ3JhbTogRXh0ZXJuYWxBcHAuYXBwcm92YWxQcm9ncmFtKCkKCWJ5dGUgYjY0IENpQUJBU1lDQTJadmJ3TmhjMkV4R0JTQkJnc3hHUWlOREFDSEFMVUFBQUFBQUFBQUFBQjVBQUFBQUFBQUFBQUFBQUNJQUFJaVE0b0FBREVBZ0Foc2IyTmhiRXRsZVNobWlZZ0FBaUpEaWdBQWlZZ0FBaUpEaWdBQUFERVdJZ2xKT0JBaUVrU0lBQUlpUTRvQkFJQUdZbTk0UzJWNVNieElLTCtKaUFBQ0lrT0tBQUFwc1lFRHNoQWlzaUtCQUxJQnM3UThaNG1JQUFJaVE0b0FBREVBS1dSd0FFeElGRVNKSWtPQUJMaEVlelkyR2dDT0FmL3hBSUFFb3d6bi80QUVSTkRhRFlBRTFoNUNWWUFFcGxxci9vQUVaVnhlQWpZYUFJNEYvMlQvYmY5Mi81Yi9zQUNBQkFHam8vODJHZ0NPQWY4L0FBPT0KCWl0eG5fZmllbGQgQXBwcm92YWxQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNAoJLy8gY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpCglieXRlIGI2NCBDZz09CglpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNQoJLy8gbG9jYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1CeXRlU2xpY2UKCWludCAxCglpdHhuX2ZpZWxkIExvY2FsTnVtQnl0ZVNsaWNlCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNgoJLy8gZ2xvYmFsTnVtQnl0ZVNsaWNlOiBFeHRlcm5hbEFwcC5zY2hlbWEuZ2xvYmFsLm51bUJ5dGVTbGljZQoJaW50IDAKCWl0eG5fZmllbGQgR2xvYmFsTnVtQnl0ZVNsaWNlCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNwoJLy8gZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50CglpbnQgMQoJaXR4bl9maWVsZCBHbG9iYWxOdW1VaW50CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExOAoJLy8gbG9jYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEubG9jYWwubnVtVWludAoJaW50IDAKCWl0eG5fZmllbGQgTG9jYWxOdW1VaW50CgoJLy8gRmVlIGZpZWxkIG5vdCBzZXQsIGRlZmF1bHRpbmcgdG8gMAoJaW50IDAKCWl0eG5fZmllbGQgRmVlCgoJLy8gU3VibWl0IGlubmVyIHRyYW5zYWN0aW9uCglpdHhuX3N1Ym1pdAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMjEKCS8vIHRoaXMuZXh0ZXJuYWxBcHBJRC52YWx1ZSA9IHRoaXMuaXR4bi5jcmVhdGVkQXBwbGljYXRpb25JRAoJYnl0ZSAweDY1Nzg3NDY1NzI2ZTYxNmM0MTcwNzA0OTQ0IC8vICJleHRlcm5hbEFwcElEIgoJaXR4biBDcmVhdGVkQXBwbGljYXRpb25JRAoJYXBwX2dsb2JhbF9wdXQKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTIzCgkvLyB0aGlzLmFzYS52YWx1ZSA9IHNlbmRBc3NldENyZWF0aW9uKHsKCS8vICAgICAgIGNvbmZpZ0Fzc2V0VG90YWw6IDEsCgkvLyAgICAgfSkKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWl0eG5fYmVnaW4KCWludCBhY2ZnCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjEyNAoJLy8gY29uZmlnQXNzZXRUb3RhbDogMQoJaW50IDEKCWl0eG5fZmllbGQgQ29uZmlnQXNzZXRUb3RhbAoKCS8vIEZlZSBmaWVsZCBub3Qgc2V0LCBkZWZhdWx0aW5nIHRvIDAKCWludCAwCglpdHhuX2ZpZWxkIEZlZQoKCS8vIFN1Ym1pdCBpbm5lciB0cmFuc2FjdGlvbgoJaXR4bl9zdWJtaXQKCWl0eG4gQ3JlYXRlZEFzc2V0SUQKCWFwcF9nbG9iYWxfcHV0CglyZXRzdWIKCi8vIGFkZHJlc3NCYWxhbmNlKGFkZHJlc3Mpdm9pZAoqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlOgoJLy8gYWRkcjogYWRkcmVzcwoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQoJZHVwCglsZW4KCWludCAzMgoJPT0KCWFzc2VydAoKCS8vIGV4ZWN1dGUgYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkCgljYWxsc3ViIGFkZHJlc3NCYWxhbmNlCglpbnQgMQoJcmV0dXJuCgovLyBhZGRyZXNzQmFsYW5jZShhZGRyOiBBZGRyZXNzKTogdm9pZAphZGRyZXNzQmFsYW5jZToKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMjkKCS8vIGxvZyhyYXdCeXRlcyhhZGRyLmlzSW5MZWRnZXIpKQoJZnJhbWVfZGlnIC0xIC8vIGFkZHI6IEFkZHJlc3MKCWFjY3RfcGFyYW1zX2dldCBBY2N0QmFsYW5jZQoJc3dhcAoJcG9wCglieXRlIDB4MDAKCWludCAwCgl1bmNvdmVyIDIKCXNldGJpdAoJbG9nCglyZXRzdWIKCi8vIHNtYWxsQm94KCl2b2lkCiphYmlfcm91dGVfc21hbGxCb3g6CgkvLyBleGVjdXRlIHNtYWxsQm94KCl2b2lkCgljYWxsc3ViIHNtYWxsQm94CglpbnQgMQoJcmV0dXJuCgovLyBzbWFsbEJveCgpOiB2b2lkCnNtYWxsQm94OgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjEzMwoJLy8gdGhpcy5zbWFsbEJveEtleS52YWx1ZSA9ICcnCglieXRlIDB4NzMgLy8gInMiCglkdXAKCWJveF9kZWwKCXBvcAoJYnl0ZSAweCAvLyAiIgoJYm94X3B1dAoJcmV0c3ViCgovLyBtZWRpdW1Cb3goKXZvaWQKKmFiaV9yb3V0ZV9tZWRpdW1Cb3g6CgkvLyBleGVjdXRlIG1lZGl1bUJveCgpdm9pZAoJY2FsbHN1YiBtZWRpdW1Cb3gKCWludCAxCglyZXR1cm4KCi8vIG1lZGl1bUJveCgpOiB2b2lkCm1lZGl1bUJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMzcKCS8vIHRoaXMubWVkaXVtQm94S2V5LmNyZWF0ZSg1XzAwMCkKCWJ5dGUgMHg2ZCAvLyAibSIKCWludCA1XzAwMAoJYm94X2NyZWF0ZQoJcG9wCglyZXRzdWIKCi8vIGV4dGVybmFsQXBwQ2FsbCgpdm9pZAoqYWJpX3JvdXRlX2V4dGVybmFsQXBwQ2FsbDoKCS8vIGV4ZWN1dGUgZXh0ZXJuYWxBcHBDYWxsKCl2b2lkCgljYWxsc3ViIGV4dGVybmFsQXBwQ2FsbAoJaW50IDEKCXJldHVybgoKLy8gZXh0ZXJuYWxBcHBDYWxsKCk6IHZvaWQKZXh0ZXJuYWxBcHBDYWxsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE0MQoJLy8gc2VuZE1ldGhvZENhbGw8W10sIHZvaWQ+KHsKCS8vICAgICAgIGFwcGxpY2F0aW9uSUQ6IHRoaXMuZXh0ZXJuYWxBcHBJRC52YWx1ZSwKCS8vICAgICAgIG5hbWU6ICdkdW1teScsCgkvLyAgICAgfSkKCWl0eG5fYmVnaW4KCWludCBhcHBsCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgltZXRob2QgImR1bW15KCl2b2lkIgoJaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTQyCgkvLyBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglyZXRzdWIKCi8vIGFzc2V0VG90YWwoKXZvaWQKKmFiaV9yb3V0ZV9hc3NldFRvdGFsOgoJLy8gZXhlY3V0ZSBhc3NldFRvdGFsKCl2b2lkCgljYWxsc3ViIGFzc2V0VG90YWwKCWludCAxCglyZXR1cm4KCi8vIGFzc2V0VG90YWwoKTogdm9pZAphc3NldFRvdGFsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE0OAoJLy8gYXNzZXJ0KHRoaXMuYXNhLnZhbHVlLnRvdGFsKQoJYnl0ZSAweDYxNzM2MSAvLyAiYXNhIgoJYXBwX2dsb2JhbF9nZXQKCWFzc2V0X3BhcmFtc19nZXQgQXNzZXRUb3RhbAoJcG9wCglhc3NlcnQKCXJldHN1YgoKLy8gaGFzQXNzZXQoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfaGFzQXNzZXQ6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBoYXNBc3NldChhZGRyZXNzKXZvaWQKCWNhbGxzdWIgaGFzQXNzZXQKCWludCAxCglyZXR1cm4KCi8vIGhhc0Fzc2V0KGFkZHI6IEFkZHJlc3MpOiB2b2lkCmhhc0Fzc2V0OgoJcHJvdG8gMSAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE1MgoJLy8gYXNzZXJ0KCFhZGRyLmlzT3B0ZWRJblRvQXNzZXQodGhpcy5hc2EudmFsdWUpKQoJZnJhbWVfZGlnIC0xIC8vIGFkZHI6IEFkZHJlc3MKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWFwcF9nbG9iYWxfZ2V0Cglhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKCXN3YXAKCXBvcAoJIQoJYXNzZXJ0CglyZXRzdWIKCi8vIGV4dGVybmFsTG9jYWwoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfZXh0ZXJuYWxMb2NhbDoKCS8vIGFkZHI6IGFkZHJlc3MKCXR4bmEgQXBwbGljYXRpb25BcmdzIDEKCWR1cAoJbGVuCglpbnQgMzIKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGV4dGVybmFsTG9jYWwoYWRkcmVzcyl2b2lkCgljYWxsc3ViIGV4dGVybmFsTG9jYWwKCWludCAxCglyZXR1cm4KCi8vIGV4dGVybmFsTG9jYWwoYWRkcjogQWRkcmVzcyk6IHZvaWQKZXh0ZXJuYWxMb2NhbDoKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxNTYKCS8vIGxvZyh0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUubG9jYWxTdGF0ZShhZGRyLCAnbG9jYWxLZXknKSBhcyBieXRlcykKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglmcmFtZV9kaWcgLTEgLy8gYWRkcjogQWRkcmVzcwoJY292ZXIgMgoJYXBwX2xvY2FsX2dldF9leAoJYXNzZXJ0Cglsb2cKCXJldHN1YgoKKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCipjcmVhdGVfTm9PcDoKCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uCgllcnIKCipjYWxsX05vT3A6CgltZXRob2QgImJvb3RzdHJhcCgpdm9pZCIKCW1ldGhvZCAiYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkIgoJbWV0aG9kICJzbWFsbEJveCgpdm9pZCIKCW1ldGhvZCAibWVkaXVtQm94KCl2b2lkIgoJbWV0aG9kICJleHRlcm5hbEFwcENhbGwoKXZvaWQiCgltZXRob2QgImFzc2V0VG90YWwoKXZvaWQiCgltZXRob2QgImhhc0Fzc2V0KGFkZHJlc3Mpdm9pZCIKCW1ldGhvZCAiZXh0ZXJuYWxMb2NhbChhZGRyZXNzKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2Jvb3RzdHJhcCAqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlICphYmlfcm91dGVfc21hbGxCb3ggKmFiaV9yb3V0ZV9tZWRpdW1Cb3ggKmFiaV9yb3V0ZV9leHRlcm5hbEFwcENhbGwgKmFiaV9yb3V0ZV9hc3NldFRvdGFsICphYmlfcm91dGVfaGFzQXNzZXQgKmFiaV9yb3V0ZV9leHRlcm5hbExvY2FsCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDk=" + }, + "contract": { + "name": "ResourcePackerv9", + "desc": "", + "methods": [ + { + "name": "bootstrap", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "addressBalance", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "smallBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "mediumBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "externalAppCall", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "assetTotal", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "hasAsset", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "externalLocal", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/resource-packer.algo.ts b/tests/artifacts/resource-packer/resource-packer.algo.ts new file mode 100644 index 00000000..d1e98660 --- /dev/null +++ b/tests/artifacts/resource-packer/resource-packer.algo.ts @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { Contract } from '@algorandfoundation/tealscript' + +class ExternalApp extends Contract { + localKey = LocalStateKey() + + boxKey = BoxKey() + + asa = GlobalStateKey() + + optInToApplication(): void { + this.localKey(this.txn.sender).value = 'foo' + } + + dummy(): void {} + + error(): void { + throw Error() + } + + boxWithPayment(_payment: PayTxn): void { + this.boxKey.value = 'foo' + } + + createAsset(): void { + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + senderAssetBalance(): void { + assert(!this.txn.sender.isOptedInToAsset(this.asa.value)) + } +} + +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class ResourcePackerv8 extends Contract { + programVersion = 8 + + externalAppID = GlobalStateKey() + + asa = GlobalStateKey() + + smallBoxKey = BoxKey({ key: 's' }) + + mediumBoxKey = BoxKey({ key: 'm' }) + + bootstrap(): void { + sendMethodCall<[], void>({ + name: 'createApplication', + approvalProgram: ExternalApp.approvalProgram(), + clearStateProgram: ExternalApp.clearProgram(), + localNumByteSlice: ExternalApp.schema.local.numByteSlice, + globalNumByteSlice: ExternalApp.schema.global.numByteSlice, + globalNumUint: ExternalApp.schema.global.numUint, + localNumUint: ExternalApp.schema.local.numUint, + }) + + this.externalAppID.value = this.itxn.createdApplicationID + + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + addressBalance(addr: Address): void { + log(rawBytes(addr.isInLedger)) + } + + smallBox(): void { + this.smallBoxKey.value = '' + } + + mediumBox(): void { + this.mediumBoxKey.create(5_000) + } + + externalAppCall(): void { + sendMethodCall<[], void>({ + applicationID: this.externalAppID.value, + name: 'dummy', + }) + } + + assetTotal(): void { + assert(this.asa.value.total) + } + + hasAsset(addr: Address): void { + assert(!addr.isOptedInToAsset(this.asa.value)) + } + + externalLocal(addr: Address): void { + log(this.externalAppID.value.localState(addr, 'localKey') as bytes) + } +} + +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class ResourcePackerv9 extends Contract { + programVersion = 9 + + externalAppID = GlobalStateKey() + + asa = GlobalStateKey() + + smallBoxKey = BoxKey({ key: 's' }) + + mediumBoxKey = BoxKey({ key: 'm' }) + + bootstrap(): void { + sendMethodCall<[], void>({ + name: 'createApplication', + approvalProgram: ExternalApp.approvalProgram(), + clearStateProgram: ExternalApp.clearProgram(), + localNumByteSlice: ExternalApp.schema.local.numByteSlice, + globalNumByteSlice: ExternalApp.schema.global.numByteSlice, + globalNumUint: ExternalApp.schema.global.numUint, + localNumUint: ExternalApp.schema.local.numUint, + }) + + this.externalAppID.value = this.itxn.createdApplicationID + + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + addressBalance(addr: Address): void { + log(rawBytes(addr.isInLedger)) + } + + smallBox(): void { + this.smallBoxKey.value = '' + } + + mediumBox(): void { + this.mediumBoxKey.create(5_000) + } + + externalAppCall(): void { + sendMethodCall<[], void>({ + applicationID: this.externalAppID.value, + name: 'dummy', + }) + } + + assetTotal(): void { + assert(this.asa.value.total) + } + + hasAsset(addr: Address): void { + assert(!addr.isOptedInToAsset(this.asa.value)) + } + + externalLocal(addr: Address): void { + log(this.externalAppID.value.localState(addr, 'localKey') as bytes) + } +} diff --git a/tests/test_transaction_composer.py b/tests/test_transaction_composer.py deleted file mode 100644 index a7096c83..00000000 --- a/tests/test_transaction_composer.py +++ /dev/null @@ -1,219 +0,0 @@ -from typing import TYPE_CHECKING - -import pytest -from algosdk.transaction import ( - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - PaymentTxn, -) - -from algokit_utils._legacy_v2.account import get_account -from algokit_utils.clients.algorand_client import AlgorandClient -from algokit_utils.models.account import Account -from algokit_utils.models.amount import AlgoAmount -from algokit_utils.transactions.transaction_composer import ( - AppCreateParams, - AssetConfigParams, - AssetCreateParams, - PaymentParams, - SendAtomicTransactionComposerResults, - TransactionComposer, -) -from legacy_v2_tests.conftest import get_unique_name - -if TYPE_CHECKING: - from algokit_utils.transactions.models import Arc2TransactionNote - - -@pytest.fixture -def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() - - -@pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: - new_account = algorand.account.random() - dispenser = algorand.account.localnet_dispenser() - algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) - ) - algorand.set_signer(sender=new_account.address, signer=new_account.signer) - return new_account - - -@pytest.fixture -def funded_secondary_account(algorand: AlgorandClient) -> Account: - secondary_name = get_unique_name() - return get_account(algorand.client.algod, secondary_name) - - -def test_add_transaction(algorand: AlgorandClient, funded_account: Account) -> None: - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - txn = PaymentTxn( - sender=funded_account.address, - sp=algorand.client.algod.suggested_params(), - receiver=funded_account.address, - amt=AlgoAmount.from_algos(1).micro_algos, - ) - composer.add_transaction(txn) - built = composer.build_transactions() - - assert len(built.transactions) == 1 - assert isinstance(built.transactions[0], PaymentTxn) - assert built.transactions[0].sender == funded_account.address - assert built.transactions[0].receiver == funded_account.address - assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos - - -def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> None: - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - expected_total = 1000 - params = AssetCreateParams( - sender=funded_account.address, - total=expected_total, - decimals=0, - default_frozen=False, - unit_name="TEST", - asset_name="Test Asset", - url="https://example.com", - ) - - composer.add_asset_create(params) - built = composer.build_transactions() - response = composer.execute(max_rounds_to_wait=20) - created_asset = algorand.client.algod.asset_info( - algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] - )["params"] - - assert len(response.tx_ids) == 1 - assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - assert isinstance(built.transactions[0], AssetCreateTxn) - txn = built.transactions[0] - assert txn.sender == funded_account.address - assert created_asset["creator"] == funded_account.address - assert txn.total == created_asset["total"] == expected_total - assert txn.decimals == created_asset["decimals"] == 0 - assert txn.default_frozen == created_asset["default-frozen"] is False - assert txn.unit_name == created_asset["unit-name"] == "TEST" - assert txn.asset_name == created_asset["name"] == "Test Asset" - - -def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account) -> None: - # First create an asset - asset_txn = AssetCreateTxn( - sender=funded_account.address, - sp=algorand.client.algod.suggested_params(), - total=1000, - decimals=0, - default_frozen=False, - unit_name="CFG", - asset_name="Configurable Asset", - manager=funded_account.address, - ) - signed_asset_txn = asset_txn.sign(funded_account.signer.private_key) - tx_id = algorand.client.algod.send_transaction(signed_asset_txn) - asset_before_config = algorand.client.algod.asset_info( - algorand.client.algod.pending_transaction_info(tx_id)["asset-index"] # type: ignore[call-overload] - ) - asset_before_config_index = asset_before_config["index"] # type: ignore[call-overload] - - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - params = AssetConfigParams( - sender=funded_account.address, - asset_id=asset_before_config_index, - manager=funded_secondary_account.address, - ) - composer.add_asset_config(params) - built = composer.build_transactions() - - assert len(built.transactions) == 1 - assert isinstance(built.transactions[0], AssetConfigTxn) - txn = built.transactions[0] - assert txn.sender == funded_account.address - assert txn.index == asset_before_config_index - assert txn.manager == funded_secondary_account.address - - composer.execute(max_rounds_to_wait=20) - updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] - assert updated_asset["manager"] == funded_secondary_account.address - - -def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> None: - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - approval_program = "#pragma version 6\nint 1" - clear_state_program = "#pragma version 6\nint 1" - params = AppCreateParams( - sender=funded_account.address, - approval_program=approval_program, - clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, - ) - composer.add_app_create(params) - built = composer.build_transactions() - - assert len(built.transactions) == 1 - assert isinstance(built.transactions[0], ApplicationCreateTxn) - txn = built.transactions[0] - assert txn.sender == funded_account.address - assert txn.approval_program == b"\x06\x81\x01" - assert txn.clear_program == b"\x06\x81\x01" - composer.execute(max_rounds_to_wait=20) - - -def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - composer.add_payment( - PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_algos(1), - ) - ) - composer.build() - simulate_response = composer.simulate() - assert simulate_response - - -def test_send(algorand: AlgorandClient, funded_account: Account) -> None: - composer = TransactionComposer( - algod=algorand.client.algod, - get_signer=lambda _: funded_account.signer, - ) - composer.add_payment( - PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_algos(1), - ) - ) - response = composer.send() - assert isinstance(response, SendAtomicTransactionComposerResults) - assert len(response.tx_ids) == 1 - assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - - -def test_arc2_note() -> None: - note_data: Arc2TransactionNote = { - "dapp_name": "TestDApp", - "format": "j", - "data": '{"key":"value"}', - } - encoded_note = TransactionComposer.arc2_note(note_data) - expected_note = b'TestDApp:j{"key":"value"}' - assert encoded_note == expected_note diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py new file mode 100644 index 00000000..ea975470 --- /dev/null +++ b/tests/transactions/test_resource_packing.py @@ -0,0 +1,168 @@ +from collections.abc import Generator +from pathlib import Path + +import pytest + +from algokit_utils import Account +from algokit_utils.applications.app_client import ( + AppClientMethodCallWithSendParams, + FundAppAccountParams, +) +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.config import config +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.amount import AlgoAmount + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algos(100)) + return new_account + + +def load_arc32_spec(version: int) -> str: + # Load the appropriate spec file from the resource-packer directory + spec_path = Path(__file__).parent.parent / "artifacts" / "resource-packer" / f"ResourcePackerv{version}.arc32.json" + return spec_path.read_text() + + +class TestResourcePackerAVM8: + """Test resource packing with AVM 8""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Create v8 app + v8_spec = load_arc32_spec(8) + v8_factory = algorand.client.get_app_factory( + app_spec=v8_spec, + default_sender=funded_account.address, + ) + self.v8_client, _ = v8_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(2334300))) + self.v8_client.send.call( + AppClientMethodCallWithSendParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) + ) + + yield + + config.configure(populate_app_call_resources=False) + + def test_accounts_address_balance_invalid_ref(self, algorand: AlgorandClient) -> None: + random_account = algorand.account.random() + with pytest.raises(LogicError, match=f"invalid Account reference {random_account.address}"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[random_account.address], # Use the address + populate_app_call_resources=False, + ) + ) + + def test_accounts_address_balance_valid_ref(self, algorand: AlgorandClient) -> None: + random_account = algorand.account.random() + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[random_account.address], # Use the address + populate_app_call_resources=True, + ) + ) + + def test_boxes_invalid_ref(self) -> None: + with pytest.raises(LogicError, match="invalid Box reference"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="smallBox", + populate_app_call_resources=False, + ) + ) + + def test_boxes_valid_ref(self) -> None: + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="smallBox", + populate_app_call_resources=True, + ) + ) + + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="mediumBox", + populate_app_call_resources=True, + ) + ) + + def test_apps_external_unavailable_app(self) -> None: + with pytest.raises(LogicError, match="unavailable App"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="externalAppCall", + populate_app_call_resources=False, + static_fee=AlgoAmount.from_micro_algo(2_000), + ) + ) + + def test_apps_external_app(self) -> None: + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="externalAppCall", + populate_app_call_resources=True, + static_fee=AlgoAmount.from_micro_algo(2_000), + ) + ) + + def test_assets_unavailable_asset(self) -> None: + with pytest.raises(LogicError, match="unavailable Asset"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="assetTotal", + populate_app_call_resources=False, + ) + ) + + def test_assets_valid_asset(self) -> None: + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="assetTotal", + populate_app_call_resources=True, + ) + ) + + def test_cross_product_reference_invalid_has_asset(self, funded_account: Account) -> None: + with pytest.raises(LogicError, match="unavailable Asset"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="hasAsset", + args=[funded_account.address], + populate_app_call_resources=False, + ) + ) + + def test_cross_product_reference_has_asset(self, funded_account: Account) -> None: + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="hasAsset", + args=[funded_account.address], + populate_app_call_resources=True, + ) + ) + + def test_cross_product_reference_invalid_external_local(self, funded_account: Account) -> None: + with pytest.raises(LogicError, match="unavailable App"): + self.v8_client.send.call( + AppClientMethodCallWithSendParams( + method="externalLocal", + args=[funded_account.address], + populate_app_call_resources=False, + ) + ) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 9217cc08..21bbbcfe 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -1,11 +1,13 @@ +import base64 +from collections.abc import Generator from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock, patch import algosdk import pytest from algosdk.transaction import ( ApplicationCallTxn, - ApplicationCreateTxn, AssetConfigTxn, AssetCreateTxn, PaymentTxn, @@ -35,6 +37,14 @@ def algorand() -> AlgorandClient: return AlgorandClient.default_local_net() +@pytest.fixture(autouse=True) +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.transactions.transaction_composer.config", new_callable=Mock) as mock_config: + mock_config.debug = True + mock_config.project_root = None + yield mock_config + + @pytest.fixture def funded_account(algorand: AlgorandClient) -> Account: new_account = algorand.account.random() @@ -169,7 +179,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No built = composer.build_transactions() assert len(built.transactions) == 1 - assert isinstance(built.transactions[0], ApplicationCreateTxn) + assert isinstance(built.transactions[0], ApplicationCallTxn) txn = built.transactions[0] assert txn.sender == funded_account.address assert txn.approval_program == b"\x06\x81\x01" @@ -261,3 +271,114 @@ def test_arc2_note() -> None: encoded_note = TransactionComposer.arc2_note(note_data) expected_note = b'TestDApp:j{"key":"value"}' assert encoded_note == expected_note + + +def _get_test_transaction( + default_account: Account, amount: AlgoAmount | None = None, sender: Account | None = None +) -> dict[str, Any]: + return { + "sender": sender.address if sender else default_account.address, + "receiver": default_account.address, + "amount": amount or AlgoAmount.from_algos(1), + } + + +def test_transaction_is_capped_by_low_min_txn_fee(algorand: AlgorandClient, funded_account: Account) -> None: + with pytest.raises(ValueError, match="Transaction fee 1000 is greater than max_fee 1 µALGO"): + algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1)) + ) + + +def test_transaction_cap_is_ignored_if_higher_than_fee(algorand: AlgorandClient, funded_account: Account) -> None: + response = algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1_000_000)) + ) + assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_micro_algo(1000) + + +def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account: Account) -> None: + response = algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algos(1)) + ) + assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algos(1) + + +def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: Account) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(1)))) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(2)))) + response = composer.send() + + assert response.confirmations[0]["txn"]["txn"]["grp"] is not None + assert response.confirmations[1]["txn"]["txn"]["grp"] is not None + assert response.transactions[0].payment.group is not None + assert response.transactions[1].payment.group is not None + assert len(response.confirmations) == 2 + assert response.confirmations[0]["confirmed-round"] >= response.transactions[0].payment.first_valid_round + assert response.confirmations[1]["confirmed-round"] >= response.transactions[1].payment.first_valid_round + assert ( + response.confirmations[0]["txn"]["txn"]["grp"] + == base64.b64encode(response.transactions[0].payment.group).decode() + ) + assert ( + response.confirmations[1]["txn"]["txn"]["grp"] + == base64.b64encode(response.transactions[1].payment.group).decode() + ) + + +def test_multisig_single_account(algorand: AlgorandClient, funded_account: Account) -> None: + multisig = algorand.account.multi_sig( + version=1, threshold=1, addrs=[funded_account.address], signing_accounts=[funded_account] + ) + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + ) + algorand.send.payment( + PaymentParams(sender=multisig.address, receiver=funded_account.address, amount=AlgoAmount.from_micro_algo(500)) + ) + + +def test_multisig_double_account(algorand: AlgorandClient, funded_account: Account) -> None: + account2 = algorand.account.random() + algorand.account.ensure_funded(account2, funded_account, AlgoAmount.from_algos(10)) + + # Setup multisig + multisig = algorand.account.multi_sig( + version=1, + threshold=2, + addrs=[funded_account.address, account2.address], + signing_accounts=[funded_account, account2], + ) + + # Fund multisig + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + ) + + # Use multisig + algorand.send.payment( + PaymentParams(sender=multisig.address, receiver=funded_account.address, amount=AlgoAmount.from_micro_algo(500)) + ) + + +@pytest.mark.usefixtures("mock_config") +def test_transactions_fails_in_debug_mode(algorand: AlgorandClient, funded_account: Account) -> None: + txn1 = algorand.create_transaction.payment(PaymentParams(**_get_test_transaction(funded_account))) + txn2 = algorand.create_transaction.payment( + PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_micro_algo(9999999999999))) + ) + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_transaction(txn1) + composer.add_transaction(txn2) + + with pytest.raises(Exception) as e: # noqa: PT011 + composer.send() + + assert f"transaction {txn2.get_txid()}: overspend" in e.value.traces[0]["failure_message"] # type: ignore[attr-defined] diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index 3c944041..e7cd9dcd 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -4,7 +4,6 @@ import pytest from algosdk.transaction import ( ApplicationCallTxn, - ApplicationCreateTxn, AssetConfigTxn, AssetCreateTxn, AssetDestroyTxn, @@ -212,7 +211,7 @@ def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: ) ) - assert isinstance(txn, ApplicationCreateTxn) + assert isinstance(txn, ApplicationCallTxn) assert txn.sender == funded_account.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index def636fd..975ce5d2 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -3,6 +3,7 @@ import algosdk import pytest +from algosdk.transaction import OnComplete from algokit_utils import Account from algokit_utils._legacy_v2.application_specification import ApplicationSpecification @@ -402,6 +403,7 @@ def test_app_call( params = AppCallParams( app_id=test_hello_world_arc32_app_id, sender=sender.address, + on_complete=OnComplete.NoOpOC, args=[b"\x02\xbe\xce\x11", b"test"], ) From ec8cb137715f40e8e0fab9063227e206b74a6b29 Mon Sep 17 00:00:00 2001 From: Al Date: Mon, 16 Dec 2024 01:28:05 +0100 Subject: [PATCH 07/31] chore: remaining resource packing related tests (#126) --- src/algokit_utils/accounts/account_manager.py | 2 +- src/algokit_utils/applications/app_client.py | 6 +- src/algokit_utils/clients/client_manager.py | 50 +++ .../transactions/transaction_composer.py | 5 +- src/algokit_utils/transactions/utils.py | 18 +- tests/transactions/test_resource_packing.py | 318 ++++++++++++++++-- 6 files changed, 352 insertions(+), 47 deletions(-) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 598676b1..4ec6587d 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -270,7 +270,7 @@ def rekey_account( # noqa: PLR0913 self.rekeyed(account, rekey_to) if not suppress_log: - logger.info(f"Rekeyed {account} to {rekey_to} via transaction {result.tx_ids[-1]}") + logger.info(f"Rekeyed {sender_address} to {rekey_address} via transaction {result.tx_ids[-1]}") return result diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 4c71d171..e2808800 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -772,9 +772,9 @@ def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransac ) def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: - is_read_only_call = params.on_complete == algosdk.transaction.OnComplete.NoOpOC or ( - not params.on_complete and get_arc56_method(params.method, self._app_spec).method.readonly - ) + is_read_only_call = ( + params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None + ) and get_arc56_method(params.method, self._app_spec).method.readonly if is_read_only_call: method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index ece39c6e..3f41f668 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -6,11 +6,13 @@ import algosdk from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.kmd import KMDClient +from algosdk.source_map import SourceMap from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient # from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_client import AppClient, AppClientParams from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams from algokit_utils.applications.app_manager import TealTemplateParams from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient @@ -152,6 +154,54 @@ def get_app_factory( ) ) + def get_app_client_by_id( + self, + app_spec: (Arc56Contract | ApplicationSpecification | str), + app_id: int, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return AppClient( + AppClientParams( + app_spec=app_spec, + algorand=self._algorand, + app_id=app_id, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + + def get_app_client_by_network( + self, + app_spec: (Arc56Contract | ApplicationSpecification | str), + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return AppClient.from_network( + app_spec=app_spec, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + @staticmethod def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index ccb6e199..be78818f 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -12,7 +12,6 @@ TransactionSigner, TransactionWithSigner, ) -from algosdk.error import AlgodHTTPError from algosdk.transaction import OnComplete from algosdk.v2client.algod import AlgodClient from typing_extensions import deprecated @@ -611,7 +610,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 returns=result.abi_results, ) - except AlgodHTTPError as e: + except Exception as e: # Handle error with debug info if enabled if config.debug: logger.error( @@ -649,7 +648,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 raise error from e logger.error("Received error executing Atomic Transaction Composer, for more information enable the debug flag") - raise Exception(f"Transaction failed: {e}") from e + raise e class TransactionComposer: diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py index 6ea09224..96a67c13 100644 --- a/src/algokit_utils/transactions/utils.py +++ b/src/algokit_utils/transactions/utils.py @@ -6,7 +6,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionComposer, EmptySigner, TransactionWithSigner from algosdk.error import AtomicTransactionComposerError from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.models import SimulateRequest +from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup from algokit_utils.applications.app_manager import BoxReference @@ -319,20 +319,22 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: def get_unnamed_app_call_resources_accessed(atc: AtomicTransactionComposer, algod: AlgodClient) -> dict[str, Any]: """Get unnamed resources accessed by application calls in an atomic transaction group.""" # Create simulation request with required flags - simulate_request = SimulateRequest( - txn_groups=[], allow_unnamed_resources=True, allow_empty_signatures=True, extra_opcode_budget=0 - ) + simulate_request = SimulateRequest(txn_groups=[], allow_unnamed_resources=True, allow_empty_signatures=True) # Create empty signer null_signer = EmptySigner() # Clone the ATC and replace signers - empty_signer_atc = atc.clone() - for txn in empty_signer_atc.txn_list: - txn.signer = null_signer + unsigned_txn_groups = atc.build_group() + txn_group = [ + SimulateRequestTransactionGroup( + txns=null_signer.sign_transactions([txn_group.txn for txn_group in unsigned_txn_groups], []) + ) + ] + simulate_request = SimulateRequest(txn_groups=txn_group, allow_unnamed_resources=True, allow_empty_signatures=True) # Run simulation - result = empty_signer_atc.simulate(algod, simulate_request) + result = atc.simulate(algod, simulate_request) # Get first group response group_response = result.simulate_response["txn-groups"][0] diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index ea975470..58c4df24 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -1,10 +1,14 @@ from collections.abc import Generator from pathlib import Path +import algosdk import pytest +from algosdk.atomic_transaction_composer import TransactionWithSigner +from algosdk.transaction import OnComplete, PaymentTxn from algokit_utils import Account from algokit_utils.applications.app_client import ( + AppClient, AppClientMethodCallWithSendParams, FundAppAccountParams, ) @@ -34,22 +38,24 @@ def load_arc32_spec(version: int) -> str: return spec_path.read_text() -class TestResourcePackerAVM8: - """Test resource packing with AVM 8""" +class BaseResourcePackerTest: + """Base class for resource packing tests""" + + version: int @pytest.fixture(autouse=True) def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: config.configure(populate_app_call_resources=True) - # Create v8 app - v8_spec = load_arc32_spec(8) - v8_factory = algorand.client.get_app_factory( - app_spec=v8_spec, + # Create app based on version + spec = load_arc32_spec(self.version) + factory = algorand.client.get_app_factory( + app_spec=spec, default_sender=funded_account.address, ) - self.v8_client, _ = v8_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) - self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(2334300))) - self.v8_client.send.call( + self.app_client, _ = factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + self.app_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(2334300))) + self.app_client.send.call( AppClientMethodCallWithSendParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) ) @@ -57,30 +63,42 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ config.configure(populate_app_call_resources=False) + @pytest.fixture + def external_client(self, algorand: AlgorandClient, funded_account: Account) -> AppClient: + external_spec = ( + Path(__file__).parent.parent / "artifacts" / "resource-packer" / "ExternalApp.arc32.json" + ).read_text() + return algorand.client.get_app_client_by_id( + app_spec=external_spec, + app_id=int(self.app_client.get_global_state()["externalAppID"].value), + app_name="external", + default_sender=funded_account.address, + ) + def test_accounts_address_balance_invalid_ref(self, algorand: AlgorandClient) -> None: random_account = algorand.account.random() with pytest.raises(LogicError, match=f"invalid Account reference {random_account.address}"): - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="addressBalance", - args=[random_account.address], # Use the address + args=[random_account.address], populate_app_call_resources=False, ) ) def test_accounts_address_balance_valid_ref(self, algorand: AlgorandClient) -> None: random_account = algorand.account.random() - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="addressBalance", - args=[random_account.address], # Use the address + args=[random_account.address], populate_app_call_resources=True, ) ) def test_boxes_invalid_ref(self) -> None: with pytest.raises(LogicError, match="invalid Box reference"): - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="smallBox", populate_app_call_resources=False, @@ -88,14 +106,14 @@ def test_boxes_invalid_ref(self) -> None: ) def test_boxes_valid_ref(self) -> None: - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="smallBox", populate_app_call_resources=True, ) ) - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="mediumBox", populate_app_call_resources=True, @@ -104,7 +122,7 @@ def test_boxes_valid_ref(self) -> None: def test_apps_external_unavailable_app(self) -> None: with pytest.raises(LogicError, match="unavailable App"): - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="externalAppCall", populate_app_call_resources=False, @@ -113,7 +131,7 @@ def test_apps_external_unavailable_app(self) -> None: ) def test_apps_external_app(self) -> None: - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="externalAppCall", populate_app_call_resources=True, @@ -123,7 +141,7 @@ def test_apps_external_app(self) -> None: def test_assets_unavailable_asset(self) -> None: with pytest.raises(LogicError, match="unavailable Asset"): - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="assetTotal", populate_app_call_resources=False, @@ -131,25 +149,15 @@ def test_assets_unavailable_asset(self) -> None: ) def test_assets_valid_asset(self) -> None: - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="assetTotal", populate_app_call_resources=True, ) ) - def test_cross_product_reference_invalid_has_asset(self, funded_account: Account) -> None: - with pytest.raises(LogicError, match="unavailable Asset"): - self.v8_client.send.call( - AppClientMethodCallWithSendParams( - method="hasAsset", - args=[funded_account.address], - populate_app_call_resources=False, - ) - ) - def test_cross_product_reference_has_asset(self, funded_account: Account) -> None: - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="hasAsset", args=[funded_account.address], @@ -159,10 +167,256 @@ def test_cross_product_reference_has_asset(self, funded_account: Account) -> Non def test_cross_product_reference_invalid_external_local(self, funded_account: Account) -> None: with pytest.raises(LogicError, match="unavailable App"): - self.v8_client.send.call( + self.app_client.send.call( AppClientMethodCallWithSendParams( method="externalLocal", args=[funded_account.address], populate_app_call_resources=False, ) ) + + def test_cross_product_reference_external_local( + self, external_client: AppClient, funded_account: Account, algorand: AlgorandClient + ) -> None: + algorand.send.app_call_method_call( + external_client.params.opt_in( + AppClientMethodCallWithSendParams( + method="optInToApplication", + sender=funded_account.address, + ) + ) + ) + + algorand.send.app_call_method_call( + self.app_client.params.call( + AppClientMethodCallWithSendParams( + method="externalLocal", + args=[funded_account.address], + sender=funded_account.address, + populate_app_call_resources=True, + ) + ) + ) + + def test_address_balance_invalid_account_reference( + self, + ) -> None: + with pytest.raises(LogicError, match="invalid Account reference"): + self.app_client.send.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[algosdk.account.generate_account()[1]], + populate_app_call_resources=False, + ) + ) + + def test_address_balance( + self, + ) -> None: + self.app_client.send.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[algosdk.account.generate_account()[1]], + on_complete=OnComplete.NoOpOC, + populate_app_call_resources=True, + ) + ) + + def test_cross_product_reference_invalid_has_asset(self, funded_account: Account) -> None: + with pytest.raises(LogicError, match="unavailable Asset"): + self.app_client.send.call( + AppClientMethodCallWithSendParams( + method="hasAsset", + args=[funded_account.address], + populate_app_call_resources=False, + ) + ) + + +class TestResourcePackerAVM8(BaseResourcePackerTest): + """Test resource packing with AVM 8""" + + version = 8 + + +class TestResourcePackerAVM9(BaseResourcePackerTest): + """Test resource packing with AVM 9""" + + version = 9 + + +class TestResourcePackerMixed: + """Test resource packing with mixed AVM versions""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Create v8 app + v8_spec = load_arc32_spec(8) + v8_factory = algorand.client.get_app_factory( + app_spec=v8_spec, + default_sender=funded_account.address, + ) + self.v8_client, _ = v8_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + + # Create v9 app + v9_spec = load_arc32_spec(9) + v9_factory = algorand.client.get_app_factory( + app_spec=v9_spec, + default_sender=funded_account.address, + ) + self.v9_client, _ = v9_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + + yield + + config.configure(populate_app_call_resources=False) + + def test_same_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + rekeyed_to = algorand.account.random() + algorand.account.rekey_account(funded_account, rekeyed_to) + + random_account = algorand.account.random() + + txn_group = algorand.send.new_group() + txn_group.add_app_call_method_call( + self.v8_client.params.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[random_account.address], + sender=funded_account.address, + signer=rekeyed_to.signer, + ) + ) + ) + txn_group.add_app_call_method_call( + self.v9_client.params.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[random_account.address], + sender=funded_account.address, + signer=rekeyed_to.signer, + ) + ) + ) + + result = txn_group.send(populate_app_call_resources=True) + + v8_accounts = getattr(result.transactions[0].application_call, "accounts", None) or [] + v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] + assert len(v8_accounts) + len(v9_accounts) == 1 + + def test_app_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(328500))) + self.v8_client.send.call( + AppClientMethodCallWithSendParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) + ) + + external_app_id = int(self.v8_client.get_global_state()["externalAppID"].value) + external_app_addr = algosdk.logic.get_application_address(external_app_id) + + txn_group = algorand.send.new_group() + txn_group.add_app_call_method_call( + self.v8_client.params.call( + AppClientMethodCallWithSendParams( + method="externalAppCall", + static_fee=AlgoAmount.from_micro_algo(2_000), + sender=funded_account.address, + ) + ) + ) + txn_group.add_app_call_method_call( + self.v9_client.params.call( + AppClientMethodCallWithSendParams( + method="addressBalance", + args=[external_app_addr], + sender=funded_account.address, + ) + ) + ) + + result = txn_group.send(populate_app_call_resources=True) + + v8_apps = getattr(result.transactions[0].application_call, "foreign_apps", None) or [] + v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] + assert len(v8_apps) + len(v9_accounts) == 1 + + +class TestResourcePackerMeta: + """Test meta aspects of resource packing""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + external_spec = ( + Path(__file__).parent.parent / "artifacts" / "resource-packer" / "ExternalApp.arc32.json" + ).read_text() + factory = algorand.client.get_app_factory( + app_spec=external_spec, + default_sender=funded_account.address, + ) + self.external_client, _ = factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + yield + + config.configure(populate_app_call_resources=False) + + def test_error_during_simulate(self) -> None: + with pytest.raises(LogicError) as exc_info: + self.external_client.send.call( + AppClientMethodCallWithSendParams( + method="error", + populate_app_call_resources=True, + ) + ) + assert "Error during resource population simulation in transaction 0" in exc_info.value.logic_error_str + + def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: Account) -> None: + payment = PaymentTxn( + sender=funded_account.address, + receiver=funded_account.address, + amt=0, + sp=algorand.client.algod.suggested_params(), + ) + payment_with_signer = TransactionWithSigner(payment, funded_account.signer) + + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(106100))) + + self.external_client.send.call( + AppClientMethodCallWithSendParams( + method="boxWithPayment", + args=[payment_with_signer], + ) + ) + + def test_sender_asset_holding(self) -> None: + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_000))) + + self.external_client.send.call( + AppClientMethodCallWithSendParams( + method="createAsset", + static_fee=AlgoAmount.from_micro_algo(2_000), + ) + ) + result = self.external_client.send.call(AppClientMethodCallWithSendParams(method="senderAssetBalance")) + + assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 + + def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + auth_addr = algorand.account.random() + algorand.account.rekey_account(funded_account, auth_addr) + + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_001))) + + self.external_client.send.call( + AppClientMethodCallWithSendParams( + method="createAsset", + static_fee=AlgoAmount.from_micro_algo(2_001), + ) + ) + result = self.external_client.send.call(AppClientMethodCallWithSendParams(method="senderAssetBalance")) + + assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 From 92774f70995f249e66c7c40e03514883fa54bc0b Mon Sep 17 00:00:00 2001 From: Al Date: Fri, 20 Dec 2024 20:42:04 +0100 Subject: [PATCH 08/31] refactor: refining codebase; cleanup for initial beta release (#127) * chore: wip * chore: refining AppFactory and AppDeployer implementations * chore: adding missing client retrieval methods; missing box state accessor * chore: aligning kmd config resolution * chore: fixing default signer assignment in app factory * chore: fix state accessor * refactor: reusing useful legacy code; refining pyproject; refining deploy response in factory * chore: adding module deprecation warnings; aliasing applicationspec to arc32contract * refactor: rewriting arc56 converter and arc56contract class * refactor: remaining batch of refactoring efforts; improving project structure --- .gitignore | 3 + .vscode/launch.json | 7 + ...new_client_missing_source_map.approved.txt | 6 +- ...legacy_build_teal_sourcemaps.approved.txt} | 0 ...l_sourcemaps_without_sources.approved.txt} | 0 legacy_v2_tests/test_debug_utils.py | 8 +- poetry.lock | 142 +--- pyproject.toml | 8 +- src/algokit_utils/__init__.py | 104 ++- src/algokit_utils/_debugging.py | 47 +- .../_legacy_v2/application_specification.py | 210 +---- src/algokit_utils/_legacy_v2/logic_error.py | 82 +- src/algokit_utils/_legacy_v2/models.py | 11 +- src/algokit_utils/account.py | 12 +- src/algokit_utils/accounts/__init__.py | 2 + src/algokit_utils/accounts/account_manager.py | 50 +- .../accounts/kmd_account_manager.py | 2 + src/algokit_utils/application_client.py | 12 +- .../application_specification.py | 37 +- src/algokit_utils/applications/__init__.py | 5 + src/algokit_utils/applications/abi.py | 200 +++++ src/algokit_utils/applications/app_client.py | 527 +++++++----- .../applications/app_deployer.py | 417 +++++----- src/algokit_utils/applications/app_factory.py | 761 +++++++++-------- src/algokit_utils/applications/app_manager.py | 68 +- .../applications/app_spec/__init__.py | 2 + .../applications/app_spec/arc32.py | 204 +++++ .../applications/app_spec/arc56.py | 777 ++++++++++++++++++ src/algokit_utils/applications/utils.py | 428 ---------- src/algokit_utils/asset.py | 33 +- src/algokit_utils/assets/__init__.py | 1 + src/algokit_utils/assets/asset_manager.py | 2 + src/algokit_utils/clients/__init__.py | 3 + src/algokit_utils/clients/algorand_client.py | 45 +- src/algokit_utils/clients/client_manager.py | 48 +- .../clients/dispenser_api_client.py | 13 + src/algokit_utils/common.py | 11 +- src/algokit_utils/config.py | 2 - src/algokit_utils/deploy.py | 11 +- src/algokit_utils/dispenser_api.py | 11 +- src/algokit_utils/errors/__init__.py | 1 + src/algokit_utils/errors/logic_error.py | 16 +- src/algokit_utils/logic_error.py | 11 +- src/algokit_utils/models/__init__.py | 9 +- src/algokit_utils/models/abi.py | 14 - src/algokit_utils/models/account.py | 3 + src/algokit_utils/models/amount.py | 2 + src/algokit_utils/models/application.py | 444 +--------- src/algokit_utils/models/network.py | 5 + src/algokit_utils/models/simulate.py | 11 + src/algokit_utils/models/state.py | 59 ++ src/algokit_utils/models/transaction.py | 95 +++ src/algokit_utils/network_clients.py | 10 +- src/algokit_utils/protocols/__init__.py | 1 + .../protocols/{application.py => client.py} | 34 +- src/algokit_utils/transactions/__init__.py | 4 + src/algokit_utils/transactions/models.py | 80 -- .../transactions/transaction_composer.py | 267 +++--- .../transactions/transaction_creator.py | 20 +- .../transactions/transaction_sender.py | 87 +- src/algokit_utils/transactions/utils.py | 7 +- .../test_build_teal_sourcemaps.approved.txt | 1 + ...al_sourcemaps_without_sources.approved.txt | 1 + tests/accounts/test_account_manager.py | 13 +- .../test_comment_stripping.approved.txt | 0 .../test_template_substitution.approved.txt | 0 ...est_arc56_from_arc32_instance.approved.txt | 58 ++ .../test_arc56_from_arc32_json.approved.txt | 58 ++ .../test_arc56_from_dict.approved.txt | 510 ++++++++++++ .../test_arc56_from_json.approved.txt | 510 ++++++++++++ tests/applications/test_app_client.py | 60 +- tests/applications/test_app_factory.py | 194 +++-- tests/applications/test_arc56.py | 45 + tests/applications/test_utils.py | 16 - .../amm_arc56_example/amm.arc56.json | 510 ++++++++++++ ...rc32_app_spec.json => app_spec.arc32.json} | 0 .../app_client_test.json | 378 +++++++++ .../legacy_app_client_test/app_client_test.py | 199 +++++ ...rc32_app_spec.json => app_spec.arc32.json} | 0 ...rc32_app_spec.json => app_spec.arc32.json} | 0 ...rc56_app_spec.json => app_spec.arc56.json} | 0 ...rc32_app_spec.json => app_spec.arc32.json} | 0 .../clients/algorand_client/test_transfer.py | 12 +- tests/conftest.py | 5 +- tests/test_debug_utils.py | 209 +++++ tests/transactions/test_abi_return.py | 105 +++ .../transactions/test_transaction_composer.py | 16 +- .../transactions/test_transaction_creator.py | 4 +- tests/transactions/test_transaction_sender.py | 14 +- tests/utils.py | 47 +- 90 files changed, 5817 insertions(+), 2640 deletions(-) rename legacy_v2_tests/test_debug_utils.approvals/{test_build_teal_sourcemaps.approved.txt => test_legacy_build_teal_sourcemaps.approved.txt} (100%) rename legacy_v2_tests/test_debug_utils.approvals/{test_build_teal_sourcemaps_without_sources.approved.txt => test_legacy_build_teal_sourcemaps_without_sources.approved.txt} (100%) create mode 100644 src/algokit_utils/applications/abi.py create mode 100644 src/algokit_utils/applications/app_spec/__init__.py create mode 100644 src/algokit_utils/applications/app_spec/arc32.py create mode 100644 src/algokit_utils/applications/app_spec/arc56.py delete mode 100644 src/algokit_utils/applications/utils.py delete mode 100644 src/algokit_utils/models/abi.py create mode 100644 src/algokit_utils/models/simulate.py create mode 100644 src/algokit_utils/models/state.py rename src/algokit_utils/protocols/{application.py => client.py} (58%) delete mode 100644 src/algokit_utils/transactions/models.py create mode 100644 tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt create mode 100644 tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt rename tests/applications/{snapshots => _snapshots}/test_app_manager.approvals/test_comment_stripping.approved.txt (100%) rename tests/applications/{snapshots => _snapshots}/test_app_manager.approvals/test_template_substitution.approved.txt (100%) create mode 100644 tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt create mode 100644 tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt create mode 100644 tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt create mode 100644 tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt create mode 100644 tests/applications/test_arc56.py delete mode 100644 tests/applications/test_utils.py create mode 100644 tests/artifacts/amm_arc56_example/amm.arc56.json rename tests/artifacts/hello_world/{arc32_app_spec.json => app_spec.arc32.json} (100%) create mode 100644 tests/artifacts/legacy_app_client_test/app_client_test.json create mode 100644 tests/artifacts/legacy_app_client_test/app_client_test.py rename tests/artifacts/legacy_hello_world/{arc32_app_spec.json => app_spec.arc32.json} (100%) rename tests/artifacts/testing_app/{arc32_app_spec.json => app_spec.arc32.json} (100%) rename tests/artifacts/testing_app_arc56/{arc56_app_spec.json => app_spec.arc56.json} (100%) rename tests/artifacts/testing_app_puya/{arc32_app_spec.json => app_spec.arc32.json} (100%) create mode 100644 tests/test_debug_utils.py create mode 100644 tests/transactions/test_abi_return.py diff --git a/.gitignore b/.gitignore index 81433e4c..e7713f87 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ cython_debug/ /docs/source/apidocs !docs/html + +# Received approval test files +*.received.* diff --git a/.vscode/launch.json b/.vscode/launch.json index 6b9d5948..49cabdde 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,13 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, { "name": "Python: Debug Tests", "type": "debugpy", diff --git a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt index 70d16cc9..fb8b9ea5 100644 --- a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt +++ b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt @@ -2,6 +2,6 @@ Txn {txn} had error 'assert failed pc=743' at PC 743: Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the error please provide an approval SourceMap. Either by: - 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2. Set approval_source_map from a previously compiled approval program OR - 3. Import a previously exported source map using import_source_map \ No newline at end of file + 1.Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2.Set approval_source_map from a previously compiled approval program OR + 3.Import a previously exported source map using import_source_map \ No newline at end of file diff --git a/legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps.approved.txt similarity index 100% rename from legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps.approved.txt diff --git a/legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps_without_sources.approved.txt similarity index 100% rename from legacy_v2_tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps_without_sources.approved.txt diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index b827ecd3..0514fb58 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -37,7 +37,7 @@ def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecificati return client -def test_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory) -> None: +def test_legacy_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory) -> None: cwd = tmp_path_factory.mktemp("cwd") approval = """ @@ -78,7 +78,7 @@ def test_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: py assert item.location != "dummy" -def test_build_teal_sourcemaps_without_sources( +def test_legacy_build_teal_sourcemaps_without_sources( algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory ) -> None: cwd = tmp_path_factory.mktemp("cwd") @@ -118,7 +118,7 @@ def test_build_teal_sourcemaps_without_sources( check_output_stability(json.dumps(result.to_dict())) -def test_simulate_and_persist_response_via_app_call( +def test_legacy_simulate_and_persist_response_via_app_call( tmp_path_factory: pytest.TempPathFactory, client_fixture: ApplicationClient, mocker: Mock, @@ -142,7 +142,7 @@ def test_simulate_and_persist_response_via_app_call( assert simulated_txn["apid"] == client_fixture.app_id -def test_simulate_and_persist_response( +def test_legacy_simulate_and_persist_response( tmp_path_factory: pytest.TempPathFactory, client_fixture: ApplicationClient, mocker: Mock, funded_account: Account ) -> None: mock_config = mocker.patch("algokit_utils._legacy_v2.application_client.config") diff --git a/poetry.lock b/poetry.lock index e173428a..cafb6951 100644 --- a/poetry.lock +++ b/poetry.lock @@ -538,23 +538,6 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -[[package]] -name = "deprecated" -version = "1.2.15" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, - {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] - [[package]] name = "distlib" version = "0.3.9" @@ -2018,29 +2001,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.2" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, - {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, - {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, - {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, - {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, - {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, - {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] [[package]] @@ -2500,17 +2483,6 @@ rfc3986 = ">=1.4.0" tqdm = ">=4.14" urllib3 = ">=1.26.0" -[[package]] -name = "types-deprecated" -version = "1.2.15.20241117" -description = "Typing stubs for Deprecated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e"}, - {file = "types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -2598,80 +2570,6 @@ files = [ [package.extras] test = ["pytest (>=6.0.0)", "setuptools (>=65)"] -[[package]] -name = "wrapt" -version = "1.17.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, -] - [[package]] name = "zipp" version = "3.21.0" @@ -2694,4 +2592,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "726e13c507aac04c65d86a3ad85222f7c218eaed40689343445e9ff8574e8ec2" +content-hash = "9669798ad0a27bb4f0309b4cd4f23b1db96e00dd9d18c40a702a6e40ea265a4b" diff --git a/pyproject.toml b/pyproject.toml index e663f468..5d7106cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,11 @@ readme = "README.md" python = "^3.10" py-algorand-sdk = "^2.4.0" httpx = "^0.23.1" -deprecated = "^1.2.14" +typing-extensions = ">=4.6.0" # Add this line [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" -ruff = ">=0.1.6,<=0.8.2" +ruff = ">=0.1.6,<=0.8.3" pip-audit = "^2.5.6" pytest-mock = "^3.11.1" mypy = "^1.5.1" @@ -29,7 +29,6 @@ sphinx-rtd-theme = "^1.2.0" sphinx-autodoc2 = ">=0.4.2,<0.6.0" poethepoet = ">=0.19,<0.26" beaker-pyteal = "^1.1.1" -types-deprecated = "^1.2.9.2" pytest-httpx = "^0.21.3" pytest-xdist = "^3.4.0" sphinx-markdown-builder = "^0.6.6" @@ -81,7 +80,6 @@ lint.select = [ "SLF", # flake8-self "SIM", # flake8-simplify "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib "ERA", # eradicate @@ -131,12 +129,12 @@ suppress-none-returning = true "tests/clients/test_algorand_client.py" = ["ERA001"] "src/algokit_utils/_legacy_v2/**/*" = ["E501"] "tests/**/*" = ["PLR2004"] +"src/algokit_utils/__init__.py" = ["I001", "RUF022"] # Ignore import sorting for __init__.py [tool.poe.tasks] docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" -"tests/**/*" = ["PLR2004"] [tool.pytest.ini_options] pythonpath = ["src", "tests"] diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 8f06e519..5aacda4a 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -1,6 +1,48 @@ -from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response -from algokit_utils._legacy_v2._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded -from algokit_utils._legacy_v2._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset +"""AlgoKit Python Utilities - a set of utilities for building solutions on Algorand + +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + + from algokit_utils.accounts import KmdAccountManager + from algokit_utils.applications import AppClient + from algokit_utils.applications.app_spec import Arc52Contract + etc. +""" + +# Core types and utilities that are commonly used +from algokit_utils.models.account import Account +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.clients.algorand_client import AlgorandClient + +# Common managers/clients that are frequently used entry points +from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.applications.app_client import AppClient +from algokit_utils.applications.app_factory import AppFactory +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.transactions.transaction_composer import TransactionComposer + +# Commonly used constants +from algokit_utils.clients.dispenser_api_client import ( + DISPENSER_ACCESS_TOKEN_KEY, + TestNetDispenserApiClient, + DISPENSER_REQUEST_TIMEOUT, +) + +# ==== LEGACY V2 SUPPORT BEGIN ==== +# These imports are maintained for backwards compatibility +from algokit_utils._legacy_v2._ensure_funded import ( + EnsureBalanceParameters, + EnsureFundedResponse, + ensure_funded, +) +from algokit_utils._legacy_v2._transfer import ( + TransferAssetParameters, + TransferParameters, + transfer, + transfer_asset, +) from algokit_utils._legacy_v2.account import ( create_kmd_wallet_account, get_account, @@ -54,7 +96,6 @@ get_creator_apps, replace_template_variables, ) -from algokit_utils._legacy_v2.logic_error import LogicError from algokit_utils._legacy_v2.models import ( ABIArgsDict, ABIMethod, @@ -81,32 +122,35 @@ is_mainnet, is_testnet, ) +# ==== LEGACY V2 SUPPORT END ==== -# New interfaces -from algokit_utils.accounts.account_manager import AccountManager -from algokit_utils.accounts.kmd_account_manager import KmdAccountManager -from algokit_utils.applications.app_client import AppClient -from algokit_utils.applications.app_factory import AppFactory -from algokit_utils.assets.asset_manager import AssetManager -from algokit_utils.clients.algorand_client import AlgorandClient -from algokit_utils.clients.client_manager import ClientManager -from algokit_utils.clients.dispenser_api_client import ( - DISPENSER_ACCESS_TOKEN_KEY, - DISPENSER_REQUEST_TIMEOUT, - DispenserFundResponse, - DispenserLimitResponse, - TestNetDispenserApiClient, +# Debugging utilities +from algokit_utils._debugging import ( + PersistSourceMapInput, + persist_sourcemaps, + simulate_and_persist_response, ) -from algokit_utils.models.account import Account -from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME -from algokit_utils.transactions.transaction_composer import TransactionComposer __all__ = [ + # Core types and utilities + "Account", + "LogicError", + "AlgorandClient", "DELETABLE_TEMPLATE_NAME", + "UPDATABLE_TEMPLATE_NAME", + # Common managers/clients + "AccountManager", + "AppClient", + "AppFactory", + "AssetManager", + "ClientManager", + "TransactionComposer", + "TestNetDispenserApiClient", + # Constants "DISPENSER_ACCESS_TOKEN_KEY", "DISPENSER_REQUEST_TIMEOUT", "NOTE_PREFIX", - "UPDATABLE_TEMPLATE_NAME", + # Legacy v2 exports - maintained for backwards compatibility "ABIArgsDict", "ABICallArgs", "ABICallArgsDict", @@ -114,22 +158,15 @@ "ABICreateCallArgsDict", "ABIMethod", "ABITransactionResponse", - "Account", - "AccountManager", "AlgoClientConfig", - "AlgorandClient", - "AppClient", "AppDeployMetaData", - "AppFactory", "AppLookup", "AppMetaData", "AppReference", "AppSpecStateDict", "ApplicationClient", "ApplicationSpecification", - "AssetManager", "CallConfig", - "ClientManager", "CommonCallParameters", "CommonCallParametersDict", "CreateCallParameters", @@ -143,12 +180,8 @@ "DeployCreateCallArgsDict", "DeployResponse", "DeploymentFailedError", - "DispenserFundResponse", - "DispenserLimitResponse", "EnsureBalanceParameters", "EnsureFundedResponse", - "KmdAccountManager", - "LogicError", "MethodConfigDict", "MethodHints", "OnCompleteActionName", @@ -161,14 +194,12 @@ "Program", "TemplateValueDict", "TemplateValueMapping", - "TestNetDispenserApiClient", - "TransactionComposer", "TransactionParameters", "TransactionParametersDict", "TransactionResponse", "TransferAssetParameters", "TransferParameters", - # ==== LEGACY V2 EXPORTS BEGIN ==== + # Legacy v2 functions "create_kmd_wallet_account", "ensure_funded", "execute_atc_with_logic_error", @@ -198,5 +229,4 @@ "simulate_and_persist_response", "transfer", "transfer_asset", - # ==== LEGACY V2 EXPORTS END ==== ] diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index 0b9f798f..fa5dcd99 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -15,6 +15,7 @@ from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig from algokit_utils._legacy_v2.common import Program +from algokit_utils.models.application import CompiledTeal if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -64,7 +65,11 @@ def to_dict(self) -> dict: @dataclass class PersistSourceMapInput: def __init__( - self, app_name: str, file_name: str, raw_teal: str | None = None, compiled_teal: Program | None = None + self, + app_name: str, + file_name: str, + raw_teal: str | None = None, + compiled_teal: CompiledTeal | Program | None = None, ): self.compiled_teal = compiled_teal self.app_name = app_name @@ -76,7 +81,9 @@ def from_raw_teal(cls, raw_teal: str, app_name: str, file_name: str) -> "Persist return cls(app_name, file_name, raw_teal=raw_teal) @classmethod - def from_compiled_teal(cls, compiled_teal: Program, app_name: str, file_name: str) -> "PersistSourceMapInput": + def from_compiled_teal( + cls, compiled_teal: CompiledTeal | Program, app_name: str, file_name: str + ) -> "PersistSourceMapInput": return cls(app_name, file_name, compiled_teal=compiled_teal) @property @@ -150,15 +157,28 @@ def _build_avm_sourcemap( output_path: Path, client: "AlgodClient", raw_teal: str | None = None, - compiled_teal: Program | None = None, + compiled_teal: CompiledTeal | Program | None = None, with_sources: bool = True, ) -> AVMDebuggerSourceMapEntry: if not raw_teal and not compiled_teal: raise ValueError("Either raw teal or compiled teal must be provided") - result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client) - program_hash = base64.b64encode(checksum(result.raw_binary)).decode() - source_map = result.source_map.__dict__ + # Handle both legacy Program and new CompiledTeal + if isinstance(compiled_teal, Program): + program_hash = base64.b64encode(checksum(compiled_teal.raw_binary)).decode() + source_map = compiled_teal.source_map.__dict__ + teal_content = compiled_teal.teal + elif isinstance(compiled_teal, CompiledTeal): + program_hash = base64.b64encode(checksum(compiled_teal.compiled)).decode() + source_map = compiled_teal.source_map.__dict__ if compiled_teal.source_map else {} + teal_content = compiled_teal.teal + else: + # Handle raw TEAL case + result = Program(str(raw_teal), client=client) + program_hash = base64.b64encode(checksum(result.raw_binary)).decode() + source_map = result.source_map.__dict__ + teal_content = result.teal + source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else [] output_dir_path = output_path / ALGOKIT_DIR / SOURCES_DIR / app_name @@ -167,7 +187,7 @@ def _build_avm_sourcemap( _write_to_file(source_map_output_path, json.dumps(source_map)) if with_sources: - _write_to_file(teal_output_path, result.teal) + _write_to_file(teal_output_path, teal_content) return AVMDebuggerSourceMapEntry(str(source_map_output_path), program_hash) @@ -209,9 +229,8 @@ def simulate_response( allow_unnamed_resources: bool | None = None, extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, - round: int | None = None, # noqa: A002 TODO: revisit + simulation_round: int | None = None, skip_signatures: int | None = None, # noqa: ARG001 TODO: revisit - fix_signers: bool | None = None, # noqa: ARG001 TODO: revisit ) -> SimulateAtomicTransactionResponse: """ Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. @@ -234,7 +253,7 @@ def simulate_response( simulate_request = SimulateRequest( txn_groups=txn_group, allow_more_logs=allow_more_logs or True, - round=round, + round=simulation_round, extra_opcode_budget=extra_opcode_budget or 0, allow_unnamed_resources=allow_unnamed_resources or True, allow_empty_signatures=allow_empty_signatures or True, @@ -244,7 +263,7 @@ def simulate_response( return atc.simulate(algod_client, simulate_request) -def simulate_and_persist_response( # noqa: PLR0913 TODO: revisit +def simulate_and_persist_response( # noqa: PLR0913 atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", @@ -254,9 +273,8 @@ def simulate_and_persist_response( # noqa: PLR0913 TODO: revisit allow_unnamed_resources: bool | None = None, extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, - round: int | None = None, # noqa: A002 TODO: revisit + simulation_round: int | None = None, skip_signatures: int | None = None, - fix_signers: bool | None = None, ) -> SimulateAtomicTransactionResponse: """ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, @@ -289,9 +307,8 @@ def simulate_and_persist_response( # noqa: PLR0913 TODO: revisit allow_unnamed_resources, extra_opcode_budget, exec_trace_config, - round, + simulation_round, skip_signatures, - fix_signers, ) txn_results = response.simulate_response["txn-groups"] diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py index 5b034929..93001f82 100644 --- a/src/algokit_utils/_legacy_v2/application_specification.py +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -1,14 +1,13 @@ -import base64 -import dataclasses -import json -from enum import IntFlag -from pathlib import Path -from typing import Any, Literal, TypeAlias, TypedDict - -from algosdk.abi import Contract -from algosdk.abi.method import MethodDict -from algosdk.transaction import StateSchema -from typing_extensions import deprecated +from algokit_utils.applications.app_spec.arc32 import ( + AppSpecStateDict, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) +from algokit_utils.applications.app_spec.arc32 import Arc32Contract as ApplicationSpecification __all__ = [ "AppSpecStateDict", @@ -20,192 +19,3 @@ "MethodHints", "OnCompleteActionName", ] - - -AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] -"""Type defining Application Specification state entries""" - - -class CallConfig(IntFlag): - """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" - - NEVER = 0 - """Never handle the specified on completion type""" - CALL = 1 - """Only handle the specified on completion type for application calls""" - CREATE = 2 - """Only handle the specified on completion type for application create calls""" - ALL = 3 - """Handle the specified on completion type for both create and normal application calls""" - - -class StructArgDict(TypedDict): - name: str - elements: list[list[str]] - - -OnCompleteActionName: TypeAlias = Literal[ - "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" -] -"""String literals representing on completion transaction types""" -MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] -"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" -DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] -"""Literal values describing the types of default argument sources""" - - -class DefaultArgumentDict(TypedDict): - """ - DefaultArgument is a container for any arguments that may - be resolved prior to calling some target method - """ - - source: DefaultArgumentType - data: int | str | bytes | MethodDict - - -StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword - "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} -) - - -@dataclasses.dataclass(kw_only=True) -class MethodHints: - """MethodHints provides hints to the caller about how to call the method""" - - #: hint to indicate this method can be called through Dryrun - read_only: bool = False - #: hint to provide names for tuple argument indices - #: method_name=>param_name=>{name:str, elements:[str,str]} - structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) - #: defaults - default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) - call_config: MethodConfigDict = dataclasses.field(default_factory=dict) - - def empty(self) -> bool: - return not self.dictify() - - def dictify(self) -> dict[str, Any]: - d: dict[str, Any] = {} - if self.read_only: - d["read_only"] = True - if self.default_arguments: - d["default_arguments"] = self.default_arguments - if self.structs: - d["structs"] = self.structs - if any(v for v in self.call_config.values() if v != CallConfig.NEVER): - d["call_config"] = _encode_method_config(self.call_config) - return d - - @staticmethod - def undictify(data: dict[str, Any]) -> "MethodHints": - return MethodHints( - read_only=data.get("read_only", False), - default_arguments=data.get("default_arguments", {}), - structs=data.get("structs", {}), - call_config=_decode_method_config(data.get("call_config", {})), - ) - - -def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: - return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} - - -def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: - return {k: CallConfig[v] for k, v in data.items()} - - -def _encode_source(teal_text: str) -> str: - return base64.b64encode(teal_text.encode()).decode("utf-8") - - -def _decode_source(b64_text: str) -> str: - return base64.b64decode(b64_text).decode("utf-8") - - -def _encode_state_schema(schema: StateSchema) -> dict[str, int]: - return { - "num_byte_slices": schema.num_byte_slices, - "num_uints": schema.num_uints, - } - - -def _decode_state_schema(data: dict[str, int]) -> StateSchema: - return StateSchema( - num_byte_slices=data.get("num_byte_slices", 0), - num_uints=data.get("num_uints", 0), - ) - - -@deprecated( - "The ApplicationSpecification class is deprecated. Use Arc56Contract and the TransactionComposer and AppClient " - "classes for modern application development." -) -@dataclasses.dataclass(kw_only=True) -class ApplicationSpecification: - """ARC-0032 application specification - - See """ - - approval_program: str - clear_program: str - contract: Contract - hints: dict[str, MethodHints] - schema: StateDict - global_state_schema: StateSchema - local_state_schema: StateSchema - bare_call_config: MethodConfigDict - - def dictify(self) -> dict: - return { - "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, - "source": { - "approval": _encode_source(self.approval_program), - "clear": _encode_source(self.clear_program), - }, - "state": { - "global": _encode_state_schema(self.global_state_schema), - "local": _encode_state_schema(self.local_state_schema), - }, - "schema": self.schema, - "contract": self.contract.dictify(), - "bare_call_config": _encode_method_config(self.bare_call_config), - } - - def to_json(self) -> str: - return json.dumps(self.dictify(), indent=4) - - @staticmethod - def from_json(application_spec: str) -> "ApplicationSpecification": - json_spec = json.loads(application_spec) - return ApplicationSpecification( - approval_program=_decode_source(json_spec["source"]["approval"]), - clear_program=_decode_source(json_spec["source"]["clear"]), - schema=json_spec["schema"], - global_state_schema=_decode_state_schema(json_spec["state"]["global"]), - local_state_schema=_decode_state_schema(json_spec["state"]["local"]), - contract=Contract.undictify(json_spec["contract"]), - hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, - bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), - ) - - def export(self, directory: Path | str | None = None) -> None: - """write out the artifacts generated by the application to disk - - Args: - directory(optional): path to the directory where the artifacts should be written - """ - if directory is None: - output_dir = Path.cwd() - else: - output_dir = Path(directory) - output_dir.mkdir(exist_ok=True, parents=True) - - (output_dir / "approval.teal").write_text(self.approval_program) - (output_dir / "clear.teal").write_text(self.clear_program) - (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) - (output_dir / "application.json").write_text(self.to_json()) - - -def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py index a556d90f..0c171cb7 100644 --- a/src/algokit_utils/_legacy_v2/logic_error.py +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -1,88 +1,14 @@ -import re -from copy import copy -from typing import TYPE_CHECKING, TypedDict - from typing_extensions import deprecated -from algokit_utils._legacy_v2.models import SimulationTrace - -if TYPE_CHECKING: - from algosdk.source_map import SourceMap as AlgoSourceMap +from algokit_utils.errors.logic_error import LogicError as NewLogicError +from algokit_utils.errors.logic_error import parse_logic_error __all__ = [ "LogicError", "parse_logic_error", ] -LOGIC_ERROR = ( - ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" -) - - -class LogicErrorData(TypedDict): - transaction_id: str - message: str - pc: int - - -def parse_logic_error( - error_str: str, -) -> LogicErrorData | None: - match = re.match(LOGIC_ERROR, error_str) - if match is None: - return None - - return { - "transaction_id": match.group("transaction_id"), - "message": match.group("message"), - "pc": int(match.group("pc")), - } - @deprecated("Use algokit_utils.models.error.LogicError instead") -class LogicError(Exception): - def __init__( - self, - *, - logic_error_str: str, - program: str, - source_map: "AlgoSourceMap | None", - transaction_id: str, - message: str, - pc: int, - logic_error: Exception | None = None, - traces: list[SimulationTrace] | None = None, - ): - self.logic_error = logic_error - self.logic_error_str = logic_error_str - self.program = program - self.source_map = source_map - self.lines = program.split("\n") - self.transaction_id = transaction_id - self.message = message - self.pc = pc - self.traces = traces - - self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None - - def __str__(self) -> str: - return ( - f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" - + (":" if self.line_no is None else f" and Source Line {self.line_no}:") - + f"\n{self.trace()}" - ) - - def trace(self, lines: int = 5) -> str: - if self.line_no is None: - return """ -Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the -error please provide an approval SourceMap. Either by: - 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2. Set approval_source_map from a previously compiled approval program OR - 3. Import a previously exported source map using import_source_map""" - - program_lines = copy(self.lines) - program_lines[self.line_no] += "\t\t<-- Error" - lines_before = max(0, self.line_no - lines) - lines_after = min(len(program_lines), self.line_no + lines) - return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) +class LogicError(NewLogicError): + pass diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index 7887cb60..316e1005 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -11,6 +11,8 @@ ) from typing_extensions import deprecated +from algokit_utils.models.simulate import SimulationTrace + # Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) @@ -23,6 +25,7 @@ "CreateTransactionParameters", "OnCompleteCallParameters", "OnCompleteCallParametersDict", + "SimulationTrace", "TransactionParameters", "TransactionResponse", ] @@ -198,11 +201,3 @@ class CommonCallParameters(TransactionParameters): @deprecated("Use TransactionParametersDict instead") class CommonCallParametersDict(TransactionParametersDict): """Deprecated, use TransactionParametersDict instead""" - - -@dataclasses.dataclass -class SimulationTrace: - app_budget_added: int | None - app_budget_consumed: int | None - failure_message: str | None - exec_trace: dict[str, object] diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index cb51b335..27343963 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -1 +1,11 @@ -from algokit_utils._legacy_v2.account import * # noqa: F403 +import warnings + +warnings.warn( + """The legacy v2 account module is deprecated and will be removed in a future version. + Use `Account` abstraction from `algokit_utils.models` instead. +""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.account import * # noqa: F403, E402 diff --git a/src/algokit_utils/accounts/__init__.py b/src/algokit_utils/accounts/__init__.py index e69de29b..87da256b 100644 --- a/src/algokit_utils/accounts/__init__.py +++ b/src/algokit_utils/accounts/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.accounts.account_manager import * # noqa: F403 +from algokit_utils.accounts.kmd_account_manager import * # noqa: F403 diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 4ec6587d..baaddfcf 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -24,6 +24,13 @@ logger = config.logger +__all__ = [ + "AccountInformation", + "AccountManager", + "EnsureFundedFromTestnetDispenserApiResponse", + "EnsureFundedResponse", +] + @dataclass(frozen=True, kw_only=True) class _CommonEnsureFundedParams: @@ -41,6 +48,40 @@ class EnsureFundedFromTestnetDispenserApiResponse(_CommonEnsureFundedParams): pass +@dataclass(frozen=True, kw_only=True) +class AccountInformation: + address: str + amount: int + amount_without_pending_rewards: int + min_balance: int + pending_rewards: int + rewards: int + round: int + status: str + total_apps_opted_in: int | None = None + total_assets_opted_in: int | None = None + total_box_bytes: int | None = None + total_boxes: int | None = None + total_created_apps: int | None = None + total_created_assets: int | None = None + apps_local_state: list[dict] | None = None + apps_total_extra_pages: int | None = None + apps_total_schema: dict | None = None + assets: list[dict] | None = None + auth_addr: str | None = None + closed_at_round: int | None = None + created_apps: list[dict] | None = None + created_assets: list[dict] | None = None + created_at_round: int | None = None + deleted: bool | None = None + incentive_eligible: bool | None = None + last_heartbeat: int | None = None + last_proposed: int | None = None + participation: dict | None = None + reward_base: int | None = None + sig_type: str | None = None + + class AccountManager: """Creates and keeps track of addresses and signers""" @@ -101,12 +142,12 @@ def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSign :param sender: The sender address :return: The `TransactionSigner` or throws an error if not found """ - signer = self._signers.get(self._get_address(sender)) + signer = self._signers.get(self._get_address(sender)) or self._default_signer if not signer: raise ValueError(f"No signer found for address {sender}") return signer - def get_information(self, sender: str | Account) -> dict[str, Any]: + def get_information(self, sender: str | Account) -> AccountInformation: """ Returns the given sender account's current status, balance and spendable amounts. @@ -115,7 +156,8 @@ def get_information(self, sender: str | Account) -> dict[str, Any]: """ info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) - return info + info = {k.replace("-", "_"): v for k, v in info.items()} + return AccountInformation(**info) def _register_account(self, private_key: str) -> Account: """Helper method to create and register an account with its signer. @@ -516,7 +558,7 @@ def _get_ensure_funded_amount( min_funding_increment: AlgoAmount | None = None, ) -> AlgoAmount | None: account_info = self.get_information(sender) - current_spending_balance = account_info["amount"] - account_info["min-balance"] + current_spending_balance = account_info.amount - account_info.min_balance min_increment = min_funding_increment.micro_algo if min_funding_increment else 0 amount_funded = self._calculate_fund_amount( diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index 6ac08c2d..611f59d0 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -9,6 +9,8 @@ from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer +__all__ = ["KmdAccount", "KmdAccountManager"] + logger = config.logger diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 2859c5d0..a81118bd 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -1 +1,11 @@ -from algokit_utils._legacy_v2.application_client import * # noqa: F403 +import warnings + +warnings.warn( + """The legacy v2 application_client module is deprecated and will be removed in a future version. + Use `AppClient` abstraction from `algokit_utils.applications` instead. +""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.application_client import * # noqa: F403, E402 diff --git a/src/algokit_utils/application_specification.py b/src/algokit_utils/application_specification.py index 56c286ee..dcd73e21 100644 --- a/src/algokit_utils/application_specification.py +++ b/src/algokit_utils/application_specification.py @@ -1 +1,36 @@ -from algokit_utils._legacy_v2.application_specification import * # noqa: F403 +import warnings + +warnings.warn( + """The legacy v2 application_specification module is deprecated and will be removed in a future version. + Use `from algokit_utils.applications.app_spec.arc32 import ...` to access Arc32 app spec instead. + By default, the ARC52Contract is a recommended app spec to use, serving as a replacement + for legacy 'ApplicationSpecification' class. + To convert legacy app specs to ARC52, use `arc32_to_arc52` function from algokit_utils.applications.utils. +""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 + AppSpecStateDict, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) +from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 + Arc32Contract as ApplicationSpecification, +) + +__all__ = [ + "AppSpecStateDict", + "ApplicationSpecification", + "CallConfig", + "DefaultArgumentDict", + "DefaultArgumentType", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", +] diff --git a/src/algokit_utils/applications/__init__.py b/src/algokit_utils/applications/__init__.py index e69de29b..9e4e3158 100644 --- a/src/algokit_utils/applications/__init__.py +++ b/src/algokit_utils/applications/__init__.py @@ -0,0 +1,5 @@ +from algokit_utils.applications.app_client import * # noqa: F403 +from algokit_utils.applications.app_deployer import * # noqa: F403 +from algokit_utils.applications.app_factory import * # noqa: F403 +from algokit_utils.applications.app_manager import * # noqa: F403 +from algokit_utils.applications.app_spec import * # noqa: F403 diff --git a/src/algokit_utils/applications/abi.py b/src/algokit_utils/applications/abi.py new file mode 100644 index 00000000..36f77c04 --- /dev/null +++ b/src/algokit_utils/applications/abi.py @@ -0,0 +1,200 @@ +from dataclasses import dataclass +from typing import Any, TypeAlias + +import algosdk +from algosdk.abi.method import Method as AlgorandABIMethod +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, StructField +from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method +from algokit_utils.models.state import BoxName + +ABIValue: TypeAlias = ( + bool | int | str | bytes | bytearray | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] +) +ABIStruct: TypeAlias = dict[str, list[dict[str, "ABIValue"]]] +Arc56ReturnValueType: TypeAlias = ABIValue | ABIStruct | None + +ABIType: TypeAlias = algosdk.abi.ABIType +ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType + +__all__ = [ + "ABIArgumentType", + "ABIReturn", + "ABIStruct", + "ABIType", + "ABIValue", + "Arc56ReturnValueType", + "BoxABIValue", + "get_abi_decoded_value", + "get_abi_encoded_value", + "get_abi_struct_from_abi_tuple", + "get_abi_tuple_from_abi_struct", + "get_abi_tuple_type_from_abi_struct_definition", + "get_arc56_value", +] + + +@dataclass(kw_only=True) +class ABIReturn: + raw_value: bytes | None = None + value: ABIValue | None = None + method: AlgorandABIMethod | None = None + decode_error: Exception | None = None + + def __init__(self, result: ABIResult) -> None: + self.decode_error = result.decode_error + if not self.decode_error: + self.raw_value = result.raw_value + self.value = result.return_value + self.method = result.method + + @property + def is_success(self) -> bool: + """Returns True if the ABI call was successful (no decode error)""" + return self.decode_error is None + + def get_arc56_value( + self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] + ) -> ABIValue | ABIStruct | None: + return get_arc56_value(self, method, structs) + + +def get_arc56_value( + abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] +) -> ABIValue | ABIStruct | None: + if isinstance(method, AlgorandABIMethod): + type_str = method.returns.type + struct = None # AlgorandABIMethod doesn't have struct info + else: + type_str = method.returns.type + struct = method.returns.struct + + if type_str == "void" or abi_return.value is None: + return None + + if abi_return.decode_error: + raise ValueError(abi_return.decode_error) + + raw_value = abi_return.raw_value + + # Handle AVM types + if type_str == "AVMBytes": + return raw_value + if type_str == "AVMString" and raw_value: + return raw_value.decode("utf-8") + if type_str == "AVMUint64" and raw_value: + return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return] + + # Handle structs + if struct and struct in structs: + return_tuple = abi_return.value + return Arc56Contract.get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs) + + # Return as-is + return abi_return.value + + +def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: PLR0911, ANN401 + if isinstance(value, (bytes | bytearray)): + return value + if type_str == "AVMUint64": + return ABIType.from_string("uint64").encode(value) + if type_str in ("AVMBytes", "AVMString"): + if isinstance(value, str): + return value.encode("utf-8") + if not isinstance(value, (bytes | bytearray)): + raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}") + return value + if type_str in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs) + if isinstance(value, (list | tuple)): + return tuple_type.encode(value) # type: ignore[arg-type] + else: + tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs) + return tuple_type.encode(tuple_values) + else: + abi_type = ABIType.from_string(type_str) + return abi_type.encode(value) + + +def get_abi_decoded_value( + value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[StructField]] +) -> ABIValue: + type_value = str(type_str) + + if type_value == "AVMBytes" or not isinstance(value, bytes): + return value + if type_value == "AVMString": + return value.decode("utf-8") + if type_value == "AVMUint64": + return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return] + if type_value in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs) + decoded_tuple = tuple_type.decode(value) + return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs) + return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return] + + +def get_abi_tuple_from_abi_struct( + struct_value: dict[str, Any], + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> list[Any]: + result = [] + for field in struct_fields: + key = field.name + if key not in struct_value: + raise ValueError(f"Missing value for field '{key}'") + value = struct_value[key] + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_tuple_from_abi_struct(value, field_type, structs) + result.append(value) + return result + + +def get_abi_tuple_type_from_abi_struct_definition( + struct_def: list[StructField], structs: dict[str, list[StructField]] +) -> algosdk.abi.TupleType: + types = [] + for field in struct_def: + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs)) + else: + types.append(ABIType.from_string(field_type)) # type: ignore[arg-type] + elif isinstance(field_type, list): + types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs)) + else: + raise ValueError(f"Invalid field type: {field_type}") + return algosdk.abi.TupleType(types) + + +def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> dict[str, Any]: + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + +@dataclass(kw_only=True, frozen=True) +class BoxABIValue: + name: BoxName + value: ABIValue diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index e2808800..a3c5e586 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -11,53 +11,79 @@ from algosdk.source_map import SourceMap from algosdk.transaction import OnComplete, Transaction -from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils.applications.app_manager import BoxABIValue, BoxName, BoxValue -from algokit_utils.applications.utils import ( +from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps +from algokit_utils.applications.abi import ( + BoxABIValue, get_abi_decoded_value, get_abi_encoded_value, get_abi_tuple_from_abi_struct, - get_arc56_method, ) -from algokit_utils.errors.logic_error import LogicError, parse_logic_error -from algokit_utils.models.application import ( - AppState, +from algokit_utils.applications.app_spec.arc32 import Arc32Contract +from algokit_utils.applications.app_spec.arc56 import ( Arc56Contract, - CompiledTeal, + PcOffsetMethod, ProgramSourceInfo, - SourceInfoDetail, + SourceInfo, StorageKey, StorageMap, ) +from algokit_utils.config import config +from algokit_utils.errors.logic_error import LogicError, parse_logic_error +from algokit_utils.models.application import ( + AppSourceMaps, + AppState, + CompiledTeal, +) +from algokit_utils.models.state import BoxName, BoxValue from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCallParams, - AppDeleteMethodCall, + AppDeleteMethodCallParams, AppMethodCallTransactionArgument, - AppUpdateMethodCall, + AppUpdateMethodCallParams, AppUpdateParams, BuiltTransactions, PaymentParams, ) -from algokit_utils.transactions.transaction_sender import SendAppTransactionResult, SendSingleTransactionResult +from algokit_utils.transactions.transaction_sender import ( + SendAppTransactionResult, + SendAppUpdateTransactionResult, + SendSingleTransactionResult, +) if TYPE_CHECKING: from collections.abc import Callable from algosdk.atomic_transaction_composer import TransactionSigner - from algokit_utils.applications.app_manager import ( - AppManager, - BoxIdentifier, - BoxReference, - TealTemplateParams, - ) - from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue + from algokit_utils.applications.abi import ABIStruct, ABIType, ABIValue + from algokit_utils.applications.app_deployer import AppLookup + from algokit_utils.applications.app_manager import AppManager from algokit_utils.models.amount import AlgoAmount - from algokit_utils.protocols.application import AlgorandClientProtocol + from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams + from algokit_utils.protocols.client import AlgorandClientProtocol from algokit_utils.transactions.transaction_composer import TransactionComposer +__all__ = [ + "AppClient", + "AppClientBareCallParams", + "AppClientBareCallWithCallOnCompleteParams", + "AppClientBareCallWithCompilationAndSendParams", + "AppClientBareCallWithCompilationParams", + "AppClientBareCallWithSendParams", + "AppClientCallParams", + "AppClientCompilationParams", + "AppClientCompilationResult", + "AppClientMethodCallParams", + "AppClientMethodCallWithCompilationAndSendParams", + "AppClientMethodCallWithCompilationParams", + "AppClientMethodCallWithSendParams", + "AppClientParams", + "AppSourceMaps", + "FundAppAccountParams", +] + # TEAL opcodes for constant blocks BYTE_CBLOCK = 38 # bytecblock opcode INT_CBLOCK = 32 # intcblock opcode @@ -127,39 +153,6 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 return max(bytecblock_offset or 0, intcblock_offset or 0) -@dataclass(kw_only=True, frozen=True) -class AppClientCompilationParams: - deploy_time_params: TealTemplateParams | None = None - updatable: bool | None = None - deletable: bool | None = None - - -@dataclass(kw_only=True, frozen=True) -class ExposedLogicErrorDetails: - is_clear_state_program: bool = False - approval_source_map: SourceMap | None = None - clear_source_map: SourceMap | None = None - program: bytes | None = None - approval_source_info: ProgramSourceInfo | None = None - clear_source_info: ProgramSourceInfo | None = None - - -@dataclass(kw_only=True, frozen=True) -class AppClientParams: - """Full parameters for creating an app client""" - - app_spec: ( - Arc56Contract | ApplicationSpecification | str - ) # Using string quotes since these types may be defined elsewhere - algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere - app_id: int - app_name: str | None = None - default_sender: str | bytes | None = None # Address can be string or bytes - default_signer: TransactionSigner | None = None - approval_source_map: SourceMap | None = None - clear_source_map: SourceMap | None = None - - @dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: approval_program: bytes @@ -169,18 +162,10 @@ class AppClientCompilationResult: @dataclass(kw_only=True, frozen=True) -class CommonTxnParams: - sender: str - signer: TransactionSigner | None = None - rekey_to: str | None = None - note: bytes | None = None - lease: bytes | None = None - static_fee: AlgoAmount | None = None - extra_fee: AlgoAmount | None = None - max_fee: AlgoAmount | None = None - validity_window: int | None = None - first_valid_round: int | None = None - last_valid_round: int | None = None +class AppClientCompilationParams: + deploy_time_params: TealTemplateParams | None = None + updatable: bool | None = None + deletable: bool | None = None @dataclass(kw_only=True) @@ -278,7 +263,7 @@ class AppClientBareCallParams: @dataclass(kw_only=True, frozen=True) -class CallOnComplete: +class _CallOnComplete: on_complete: algosdk.transaction.OnComplete @@ -298,33 +283,26 @@ class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, App @dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, CallOnComplete): +class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, _CallOnComplete): """Combined parameters for bare calls with an OnComplete value""" -@dataclass(kw_only=True, frozen=True) -class ResolveAppClientByNetwork: - app_spec: Arc56Contract | ApplicationSpecification | str - algorand: AlgorandClientProtocol - app_name: str | None = None - default_sender: str | bytes | None = None - default_signer: TransactionSigner | None = None - approval_source_map: SourceMap | None = None - clear_source_map: SourceMap | None = None +class _AppClientStateMethodsProtocol(Protocol): + def get_all(self) -> dict[str, Any]: ... + def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: ... -@dataclass(kw_only=True, frozen=True) -class AppSourceMaps: - approval_source_map: SourceMap | None = None - clear_source_map: SourceMap | None = None + def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: ... # noqa: ANN401 + def get_map(self, map_name: str) -> dict[str, ABIValue]: ... -class _AppClientStateMethodsProtocol(Protocol): + +class _AppClientBoxMethodsProtocol(Protocol): def get_all(self) -> dict[str, Any]: ... - def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: ... + def get_value(self, name: str) -> ABIValue | None: ... - def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: ... # noqa: ANN401 + def get_map_value(self, map_name: str, key: bytes | Any) -> Any: ... # noqa: ANN401 def get_map(self, map_name: str) -> dict[str, ABIValue]: ... @@ -356,6 +334,33 @@ def get_map(self, map_name: str) -> dict[str, ABIValue]: return self._get_map(map_name) +class _AppClientBoxMethods(_AppClientBoxMethodsProtocol): + def __init__( + self, + *, + get_all: Callable[[], dict[str, Any]], + get_value: Callable[[str], ABIValue | None], + get_map_value: Callable[[str, bytes | Any], Any], + get_map: Callable[[str], dict[str, ABIValue]], + ) -> None: + self._get_all = get_all + self._get_value = get_value + self._get_map_value = get_map_value + self._get_map = get_map + + def get_all(self) -> dict[str, Any]: + return self._get_all() + + def get_value(self, name: str) -> ABIValue | None: + return self._get_value(name) + + def get_map_value(self, map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 + return self._get_map_value(map_name, key) + + def get_map(self, map_name: str) -> dict[str, ABIValue]: + return self._get_map(map_name) + + class _AppClientStateAccessor: def __init__(self, client: AppClient) -> None: self._client = client @@ -367,8 +372,8 @@ def local_state(self, address: str) -> _AppClientStateMethodsProtocol: """Methods to access local state for the current app for a given address""" return self._get_state_methods( state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address), - key_getter=lambda: self._app_spec.state.keys.get("local", {}), - map_getter=lambda: self._app_spec.state.maps.get("local", {}), + key_getter=lambda: self._app_spec.state.keys.local_state, + map_getter=lambda: self._app_spec.state.maps.local_state, ) @property @@ -376,18 +381,86 @@ def global_state(self) -> _AppClientStateMethodsProtocol: """Methods to access global state for the current app""" return self._get_state_methods( state_getter=lambda: self._algorand.app.get_global_state(self._app_id), - key_getter=lambda: self._app_spec.state.keys.get("global", {}), - map_getter=lambda: self._app_spec.state.maps.get("global", {}), + key_getter=lambda: self._app_spec.state.keys.global_state, + map_getter=lambda: self._app_spec.state.maps.global_state, ) - # @property - # def box(self) -> AppClientStateMethods: - # """Methods to access box storage for the current app""" - # return self._get_state_methods( - # state_getter=lambda: self._algorand.app.get_box_state(self._app_id), - # key_getter=lambda: self._app_spec.state.keys.get("box", {}), - # map_getter=lambda: self._app_spec.state.maps.get("box", {}), - # ) + @property + def box(self) -> _AppClientBoxMethodsProtocol: + """Methods to access box storage for the current app""" + return self._get_box_methods() + + def _get_box_methods(self) -> _AppClientBoxMethodsProtocol: + """Get methods to access box storage for the current app.""" + + def get_all() -> dict[str, Any]: + """Returns all single-key box values in a dict keyed by the key name.""" + return {key: get_value(key) for key in self._app_spec.state.keys.box} + + def get_value(name: str) -> ABIValue | None: + """Returns a single box value for the current app with the value a decoded ABI value. + + Args: + name: The name of the box value to retrieve + """ + metadata = self._app_spec.state.keys.box[name] + value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(metadata.key)) + return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs) + + def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 + """Get a value from a box map. + + Args: + map_name: The name of the map to read from + key: The key within the map (without any map prefix) as either bytes or a value + that will be converted to bytes by encoding it using the specified ABI key type + """ + metadata = self._app_spec.state.maps.box[map_name] + prefix = base64.b64decode(metadata.prefix or "") + encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) + full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") + value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(full_key)) + return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs) + + def get_map(map_name: str) -> dict[str, ABIValue]: + """Get all key-value pairs from a box map. + + Args: + map_name: The name of the map to read from + """ + metadata = self._app_spec.state.maps.box[map_name] + prefix = base64.b64decode(metadata.prefix or "") + box_names = self._algorand.app.get_box_names(self._app_id) + + result = {} + for box in box_names: + if not box.name_raw.startswith(prefix): + continue + + encoded_key = prefix + box.name_raw + base64_key = base64.b64encode(encoded_key).decode("utf-8") + + try: + key = get_abi_decoded_value(box.name_raw[len(prefix) :], metadata.key_type, self._app_spec.structs) + value = get_abi_decoded_value( + self._algorand.app.get_box_value(self._app_id, base64.b64decode(base64_key)), + metadata.value_type, + self._app_spec.structs, + ) + result[str(key)] = value + except Exception as e: + if "Failed to decode key" in str(e): + raise ValueError(f"Failed to decode key {base64_key}") from e + raise ValueError(f"Failed to decode value for key {base64_key}") from e + + return result + + return _AppClientBoxMethods( + get_all=get_all, + get_value=get_value, + get_map_value=get_map_value, + get_map=get_map, + ) def _get_state_methods( # noqa: C901 self, @@ -408,7 +481,7 @@ def get_value(name: str, app_state: dict[str, AppState] | None = None) -> ABIVal if value and value.value_raw: return get_abi_decoded_value(value.value_raw, key_info.value_type, self._app_spec.structs) - return None + return value.value if value else None def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 state = app_state or state_getter() @@ -420,7 +493,7 @@ def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState value = next((s for s in state.values() if s.key_base64 == full_key), None) if value and value.value_raw: return get_abi_decoded_value(value.value_raw, metadata.value_type, self._app_spec.structs) - return None + return value.value if value else None def get_map(map_name: str) -> dict[str, ABIValue]: state = state_getter() @@ -558,23 +631,23 @@ def random_note() -> bytes: close_remainder_to=params.close_remainder_to, ) - def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) - return AppCallMethodCall(**input_params) + return AppCallMethodCallParams(**input_params) - def call(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) - return AppCallMethodCall(**input_params) + return AppCallMethodCallParams(**input_params) - def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: input_params = self._get_abi_params( params.__dict__, on_complete=algosdk.transaction.OnComplete.DeleteApplicationOC ) - return AppDeleteMethodCall(**input_params) + return AppDeleteMethodCallParams(**input_params) def update( self, params: AppClientMethodCallParams | AppClientMethodCallWithCompilationAndSendParams - ) -> AppUpdateMethodCall: + ) -> AppUpdateMethodCallParams: compile_params = ( self._client.compile( app_spec=self._client.app_spec, @@ -591,14 +664,14 @@ def update( **self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.UpdateApplicationOC), **compile_params, } - # Filter input_params to include only fields valid for AppUpdateMethodCall - app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCall)} + # Filter input_params to include only fields valid for AppUpdateMethodCallParams + app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCallParams)} filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields} - return AppUpdateMethodCall(**filtered_input_params) + return AppUpdateMethodCallParams(**filtered_input_params) - def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.CloseOutOC) - return AppCallMethodCall(**input_params) + return AppCallMethodCallParams(**input_params) def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: input_params = copy.deepcopy(params) @@ -610,7 +683,7 @@ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transacti input_params["signer"] = self._client._get_signer(params["sender"], params["signer"]) if params.get("method"): - input_params["method"] = get_arc56_method(params["method"], self._app_spec) + input_params["method"] = self._app_spec.get_arc56_method(params["method"]).to_abi_method() if params.get("args"): input_params["args"] = self._client._get_abi_args_with_default_values( method_name_or_signature=params["method"], @@ -699,9 +772,7 @@ def update( Returns: The result of sending the transaction """ - compiled = self._client.compile_and_persist_sourcemaps( - params.deploy_time_params, params.updatable, params.deletable - ) + compiled = self._client.compile_sourcemaps(params.deploy_time_params, params.updatable, params.deletable) bare_params = self._client.params.bare.update(params) bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) @@ -761,7 +832,7 @@ def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactio lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)) ) - def update(self, params: AppClientMethodCallWithCompilationAndSendParams) -> SendAppTransactionResult: + def update(self, params: AppClientMethodCallWithCompilationAndSendParams) -> SendAppUpdateTransactionResult: return self._client._handle_call_errors( # type: ignore[no-any-return] lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)) ) @@ -774,7 +845,7 @@ def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransac def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: is_read_only_call = ( params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None - ) and get_arc56_method(params.method, self._app_spec).method.readonly + ) and self._app_spec.get_arc56_method(params.method).readonly if is_read_only_call: method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( @@ -789,8 +860,7 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR allow_empty_signatures=True, extra_opcode_budget=None, exec_trace_config=None, - round=None, - fix_signers=None, # TODO: double check on whether algosdk py even has this param + simulation_round=None, ) ) @@ -802,7 +872,7 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR confirmations=simulate_response.confirmations, group_id=simulate_response.group_id or "", returns=simulate_response.returns, - return_value=simulate_response.returns[-1], + abi_return=simulate_response.returns[-1], ) return self._client._handle_call_errors( @@ -810,6 +880,20 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR ) +@dataclass(kw_only=True, frozen=True) +class AppClientParams: + """Full parameters for creating an app client""" + + app_spec: Arc56Contract | Arc32Contract | str # Using string quotes since these types may be defined elsewhere + algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere + app_id: int + app_name: str | None = None + default_sender: str | bytes | None = None # Address can be string or bytes + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + class AppClient: def __init__(self, params: AppClientParams) -> None: self._app_id = params.app_id @@ -859,31 +943,26 @@ def create_transaction(self) -> _AppClientMethodCallTransactionCreator: return self._create_transaction_accessor @staticmethod - def normalise_app_spec(app_spec: Arc56Contract | ApplicationSpecification | str) -> Arc56Contract: + def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Contract: if isinstance(app_spec, str): - spec = json.loads(app_spec) - if "hints" in spec: - spec = ApplicationSpecification.from_json(app_spec) + spec_dict = json.loads(app_spec) + spec = Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict else: spec = app_spec - if isinstance(spec, Arc56Contract): - return spec - - elif isinstance(spec, ApplicationSpecification): - # Convert ARC-32 to ARC-56 - from algokit_utils.applications.utils import arc32_to_arc56 - - return arc32_to_arc56(spec) - elif isinstance(spec, dict): - # normalize field names to lowercase to python camel - return Arc56Contract.from_json(spec) - else: - raise ValueError("Invalid app spec format") + match spec: + case Arc56Contract(): + return spec + case Arc32Contract(): + return Arc56Contract.from_arc32(spec.to_json()) + case dict(): + return Arc56Contract.from_dict(spec) + case _: + raise ValueError("Invalid app spec format") @staticmethod def from_network( - app_spec: Arc56Contract | ApplicationSpecification | str, + app_spec: Arc56Contract | Arc32Contract | str, algorand: AlgorandClientProtocol, app_name: str | None = None, default_sender: str | bytes | None = None, @@ -908,7 +987,7 @@ def from_network( if network_index is None: raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec") - app_id = app_spec.networks[available_app_spec_networks[network_index]]["app_id"] # type: ignore[index] + app_id = app_spec.networks[available_app_spec_networks[network_index]].app_id # type: ignore[index] return AppClient( AppClientParams( @@ -923,6 +1002,40 @@ def from_network( ) ) + @staticmethod + def from_creator_and_name( + creator_address: str, + app_name: str, + app_spec: Arc56Contract | Arc32Contract | str, + algorand: AlgorandClientProtocol, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + ) -> AppClient: + app_spec_ = AppClient.normalise_app_spec(app_spec) + app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name( + creator_address=creator_address, ignore_cache=ignore_cache or False + ) + app_metadata = app_lookup.apps.get(app_name or app_spec_.name) + if not app_metadata: + raise ValueError(f"App not found for creator {creator_address} and name {app_name or app_spec_.name}") + + return AppClient( + AppClientParams( + app_id=app_metadata.app_id, + app_spec=app_spec_, + algorand=algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + @staticmethod def compile( app_spec: Arc56Contract, @@ -938,20 +1051,16 @@ def is_base64(s: str) -> bool: return False if not app_spec.source: - if not app_spec.byte_code or not app_spec.byte_code.get("approval") or not app_spec.byte_code.get("clear"): + if not app_spec.byte_code or not app_spec.byte_code.approval or not app_spec.byte_code.clear: raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code") return AppClientCompilationResult( - approval_program=base64.b64decode(app_spec.byte_code.get("approval", "")), - clear_state_program=base64.b64decode(app_spec.byte_code.get("clear", "")), + approval_program=base64.b64decode(app_spec.byte_code.approval), + clear_state_program=base64.b64decode(app_spec.byte_code.clear), ) - approval_source = app_spec.source.get("approval", "") - approval_template: str = ( - base64.b64decode(approval_source).decode("utf-8") if is_base64(approval_source) else approval_source - ) compiled_approval = app_manager.compile_teal_template( - approval_template, + app_spec.source.get_decoded_approval(), template_params=deploy_time_params, deployment_metadata=( {"updatable": updatable or False, "deletable": deletable or False} @@ -960,16 +1069,24 @@ def is_base64(s: str) -> bool: ), ) - clear_source = app_spec.source.get("clear", "") - clear_template: str = ( - base64.b64decode(clear_source).decode("utf-8") if is_base64(clear_source) else clear_source - ) compiled_clear = app_manager.compile_teal_template( - clear_template, + app_spec.source.get_decoded_clear(), template_params=deploy_time_params, ) - # TODO: Add invocation of persisting sourcemaps + if config.debug and config.project_root: + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=compiled_approval, app_name=app_spec.name, file_name="approval.teal" + ), + PersistSourceMapInput(compiled_teal=compiled_clear, app_name=app_spec.name, file_name="clear.teal"), + ], + project_root=config.project_root, + client=app_manager._algod, + with_sources=True, + ) + return AppClientCompilationResult( approval_program=compiled_approval.compiled_base64_to_bytes, compiled_approval=compiled_approval, @@ -978,11 +1095,19 @@ def is_base64(s: str) -> bool: ) @staticmethod - def expose_logic_error_static( # noqa: C901 - e: Exception, app_spec: Arc56Contract, details: ExposedLogicErrorDetails + def _expose_logic_error_static( # noqa: C901 + *, + e: Exception, + app_spec: Arc56Contract, + is_clear_state_program: bool = False, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + program: bytes | None = None, + approval_source_info: ProgramSourceInfo | None = None, + clear_source_info: ProgramSourceInfo | None = None, ) -> Exception: """Takes an error that may include a logic error and re-exposes it with source info.""" - source_map = details.clear_source_map if details.is_clear_state_program else details.approval_source_map + source_map = clear_source_map if is_clear_state_program else approval_source_map error_details = parse_logic_error(str(e)) if not error_details: @@ -991,26 +1116,24 @@ def expose_logic_error_static( # noqa: C901 # The PC value to find in the ARC56 SourceInfo arc56_pc = error_details["pc"] - program_source_info = ( - details.clear_source_info if details.is_clear_state_program else details.approval_source_info - ) + program_source_info = clear_source_info if is_clear_state_program else approval_source_info # The offset to apply to the PC if using the cblocks pc offset method cblocks_offset = 0 # If the program uses cblocks offset, then we need to adjust the PC accordingly - if program_source_info and program_source_info.pc_offset_method == "cblocks": - if not details.program: + if program_source_info and program_source_info.pc_offset_method == PcOffsetMethod.CBLOCKS: + if not program: raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset") - cblocks_offset = get_constant_block_offset(details.program) + cblocks_offset = get_constant_block_offset(program) arc56_pc = error_details["pc"] - cblocks_offset # Find the source info for this PC and get the error message source_info = None if program_source_info and program_source_info.source_info: source_info = next( - (s for s in program_source_info.source_info if isinstance(s, SourceInfoDetail) and arc56_pc in s.pc), + (s for s in program_source_info.source_info if isinstance(s, SourceInfo) and arc56_pc in s.pc), None, ) error_message = source_info.error_message if source_info else None @@ -1018,7 +1141,11 @@ def expose_logic_error_static( # noqa: C901 # If we have the source we can display the TEAL in the error message if hasattr(app_spec, "source"): program_source = ( - (app_spec.source.get("clear") if details.is_clear_state_program else app_spec.source.get("approval")) + ( + app_spec.source.get_decoded_clear() + if is_clear_state_program + else app_spec.source.get_decoded_approval() + ) if app_spec.source else None ) @@ -1062,7 +1189,7 @@ def get_line_for_pc(input_pc: int) -> int | None: return e # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' - def compile_and_persist_sourcemaps( + def compile_sourcemaps( self, deploy_time_params: TealTemplateParams | None = None, updatable: bool | None = None, @@ -1152,15 +1279,9 @@ def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type) def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]: - names = self.get_box_names() - if filter_func: - names = [name for name in names if filter_func(name)] - - # Get values for filtered names - values = self._algorand.app.get_box_values(self.app_id, [name.name_raw for name in names]) - - # Return list of BoxValue objects - return [BoxValue(name=name, value=values[i]) for i, name in enumerate(names)] + names = [n for n in self.get_box_names() if not filter_func or filter_func(n)] + values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names]) + return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)] def get_box_values_from_abi_type( self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None @@ -1184,7 +1305,7 @@ def new_group(self) -> TransactionComposer: def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: return self.send.fund_app_account(params) - def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT001, FBT002 + def _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT001, FBT002 """Takes an error that may include a logic error from a call to the current app and re-exposes the error to include source code information via the source map and ARC-56 spec. @@ -1200,9 +1321,7 @@ def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) source_info = None if hasattr(self._app_spec, "source_info") and self._app_spec.source_info: source_info = ( - self._app_spec.source_info.get("clear") - if is_clear_state_program - else self._app_spec.source_info.get("approval") + self._app_spec.source_info.clear if is_clear_state_program else self._app_spec.source_info.approval ) pc_offset_method = source_info.pc_offset_method if source_info else None @@ -1213,25 +1332,15 @@ def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) app_info = self._algorand.app.get_by_id(self.app_id) program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program - return AppClient.expose_logic_error_static( - e, - self._app_spec, - ExposedLogicErrorDetails( - is_clear_state_program=is_clear_state_program, - approval_source_map=self._approval_source_map, - clear_source_map=self._clear_source_map, - program=program, - approval_source_info=( - self._app_spec.source_info.get("approval") - if self._app_spec.source_info and hasattr(self._app_spec, "source_info") - else None - ), - clear_source_info=( - self._app_spec.source_info.get("clear") - if self._app_spec.source_info and hasattr(self._app_spec, "source_info") - else None - ), - ), + return AppClient._expose_logic_error_static( + e=e, + app_spec=self._app_spec, + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=program, + approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None), + clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None), ) def _handle_call_errors(self, call: Callable[[], T]) -> T: @@ -1239,7 +1348,7 @@ def _handle_call_errors(self, call: Callable[[], T]) -> T: try: return call() except Exception as e: - raise self.expose_logic_error(e=e) from None + raise self._expose_logic_error(e=e) from None def _get_sender(self, sender: str | None) -> str: if not sender and not self._default_sender: @@ -1249,7 +1358,7 @@ def _get_sender(self, sender: str | None) -> str: return sender or self._default_sender # type: ignore[return-value] def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: - return signer or self._default_signer if sender else None + return signer or self._default_signer if not sender or sender == self._default_sender else None def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: """Get bare parameters for application calls. @@ -1290,10 +1399,10 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 Raises: ValueError: If required argument is missing or default value lookup fails """ - method = get_arc56_method(method_name_or_signature, self._app_spec) + method = self._app_spec.get_arc56_method(method_name_or_signature) result = [] - for i, method_arg in enumerate(method.arc56_args): + for i, method_arg in enumerate(method.args): # Get provided arg value if any arg_value = args[i] if args and i < len(args) else None @@ -1317,10 +1426,10 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 case "method": # Get method return value - default_method = get_arc56_method(default_value.data, self._app_spec) + default_method = self._app_spec.get_arc56_method(default_value.data) empty_args = [None] * len(default_method.args) call_result = self._algorand.send.app_call_method_call( - AppCallMethodCall( + AppCallMethodCallParams( app_id=self._app_id, method=algosdk.abi.Method.from_signature(default_value.data), args=empty_args, @@ -1328,20 +1437,20 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 ) ) - if not call_result.return_value: + if not call_result.abi_return: raise ValueError("Default value method call did not return a value") - if isinstance(call_result.return_value, dict): + if isinstance(call_result.abi_return, dict): # Convert struct return value to tuple result.append( get_abi_tuple_from_abi_struct( - call_result.return_value, - self._app_spec.structs[str(default_method.arc56_returns.type)], + call_result.abi_return, + self._app_spec.structs[str(default_method.returns.type)], self._app_spec.structs, ) ) - else: - result.append(call_result.return_value.return_value) + elif call_result.abi_return.value: + result.append(call_result.abi_return.value) case "local" | "global": # Get state value @@ -1381,7 +1490,7 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: sender = self._get_sender(params.get("sender")) - method = get_arc56_method(params["method"], self._app_spec) + method = self._app_spec.get_arc56_method(params["method"]) args = self._get_abi_args_with_default_values( method_name_or_signature=params["method"], args=params.get("args"), sender=sender ) diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index c3cc5853..34f84b3e 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -1,74 +1,130 @@ import base64 import dataclasses import json -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from enum import Enum from typing import Literal -import algosdk -from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner from algosdk.logic import get_application_address -from algosdk.transaction import OnComplete from algosdk.v2client.indexer import IndexerClient -from algokit_utils._legacy_v2.deploy import ( - AppDeployMetaData, - AppLookup, - AppMetaData, - OnSchemaBreak, - OnUpdate, - OperationPerformed, -) -from algokit_utils.applications.app_manager import AppManager, BoxReference, TealTemplateParams +from algokit_utils.applications.app_manager import AppManager from algokit_utils.config import config -from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.models.state import TealTemplateParams from algokit_utils.transactions.transaction_composer import ( - AppCreateMethodCall, + AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCall, + AppDeleteMethodCallParams, AppDeleteParams, - AppUpdateMethodCall, + AppUpdateMethodCallParams, AppUpdateParams, ) from algokit_utils.transactions.transaction_sender import ( AlgorandClientTransactionSender, + SendAppCreateTransactionResult, + SendAppTransactionResult, + SendAppUpdateTransactionResult, ) -APP_DEPLOY_NOTE_DAPP = "algokit_deployer" +__all__ = [ + "APP_DEPLOY_NOTE_DAPP", + "AppDeployMetaData", + "AppDeployParams", + "AppDeployResponse", + "AppDeployer", + "AppLookup", + "AppMetaData", + "AppReference", + "OnSchemaBreak", + "OnUpdate", + "OperationPerformed", +] + + +APP_DEPLOY_NOTE_DAPP: str = "ALGOKIT_DEPLOYER" logger = config.logger -@dataclass(kw_only=True) -class DeployAppUpdateParams: - """Parameters for an update transaction in app deployment""" - - sender: str - on_complete: OnComplete = OnComplete.UpdateApplicationOC - signer: TransactionSigner | None = None - args: list[bytes] | None = None - note: bytes | None = None - lease: bytes | None = None - rekey_to: str | None = None - account_references: list[str] | None = None - app_references: list[int] | None = None - asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None +@dataclasses.dataclass +class AppReference: + """Information about an Algorand app""" + app_id: int + app_address: str -@dataclass(kw_only=True) -class DeployAppDeleteParams: - """Parameters for a delete transaction in app deployment""" - sender: str - on_complete: OnComplete = OnComplete.DeleteApplicationOC - signer: TransactionSigner | None = None - note: bytes | None = None - lease: bytes | None = None - rekey_to: str | None = None - account_references: list[str] | None = None - app_references: list[int] | None = None - asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None +@dataclasses.dataclass +class AppDeployMetaData: + """Metadata about an application stored in a transaction note during creation. + + The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field + as part of {py:meth}`ApplicationClient.deploy` + """ + + name: str + version: str + deletable: bool | None + updatable: bool | None + + +@dataclasses.dataclass +class AppMetaData(AppReference, AppDeployMetaData): + """Metadata about a deployed app""" + + created_round: int + updated_round: int + created_metadata: AppDeployMetaData + deleted: bool + + +@dataclasses.dataclass +class AppLookup: + """Cache of {py:class}`AppMetaData` for a specific `creator` + + Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple + apps or discovering multiple app_ids + """ + + creator: str + apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) + + +class OnSchemaBreak(str, Enum): + """Action to take if an Application's schema has breaking changes""" + + Fail = "fail" + """Fail the deployment""" + ReplaceApp = "replace_app" + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = "append_app" + """Create a new Application""" + + +class OnUpdate(str, Enum): + """Action to take if an Application has been updated""" + + Fail = "fail" + """Fail the deployment""" + UpdateApp = "update_app" + """Update the Application with the new approval and clear programs""" + ReplaceApp = "replace_app" + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = "append_app" + """Create a new application""" + + +class OperationPerformed(str, Enum): + """Describes the actions taken during deployment""" + + Nothing = "nothing" + """An existing Application was found""" + Create = "create" + """No existing Application was found, created a new Application""" + Update = "update" + """An existing Application was found, but was out of date, updated to latest version""" + Replace = "replace" + """An existing Application was found, but was out of date, created a new Application and deleted the original""" @dataclass(kw_only=True) @@ -79,50 +135,25 @@ class AppDeployParams: deploy_time_params: TealTemplateParams | None = None on_schema_break: Literal["replace", "fail", "append"] | OnSchemaBreak = OnSchemaBreak.Fail on_update: Literal["update", "replace", "fail", "append"] | OnUpdate = OnUpdate.Fail - create_params: AppCreateParams | AppCreateMethodCall - update_params: DeployAppUpdateParams | AppUpdateMethodCall - delete_params: DeployAppDeleteParams | AppDeleteMethodCall + create_params: AppCreateParams | AppCreateMethodCallParams + update_params: AppUpdateParams | AppUpdateMethodCallParams + delete_params: AppDeleteParams | AppDeleteMethodCallParams existing_deployments: AppLookup | None = None ignore_cache: bool = False max_fee: int | None = None max_rounds_to_wait: int | None = None suppress_log: bool = False + populate_app_call_resources: bool = False -@dataclass(kw_only=True, frozen=True) -class ConfirmedTransactionResult: - transaction: TransactionWrapper - confirmation: algosdk.v2client.algod.AlgodResponseType - confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None - - -@dataclass(kw_only=True, frozen=True) -class AppDeployResult: +# Union type for all possible deploy results +@dataclass(frozen=True) +class AppDeployResponse: + app: AppMetaData operation_performed: OperationPerformed - - # Common fields from AppMetadata - name: str - version: str - created_round: int - updated_round: int - deleted: bool - created_metadata: dict - deletable: bool | None = None - updatable: bool | None = None - - app_id: int | None = None - app_address: str | None = None - transaction: TransactionWrapper | None = None - tx_id: str | None = None - transactions: list[TransactionWrapper] | None = None - tx_ids: list[str] | None = None - confirmation: algosdk.v2client.algod.AlgodResponseType | None = None - confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None - compiled_approval: dict | None = None - compiled_clear: dict | None = None - return_value: ABIResult | None = None - delete_return_value: ABIResult | None = None - delete_result: ConfirmedTransactionResult | None = None + create_response: SendAppCreateTransactionResult | None = None + update_response: SendAppUpdateTransactionResult | None = None + delete_response: SendAppTransactionResult | None = None class AppDeployer: @@ -147,7 +178,7 @@ def _create_deploy_note(self, metadata: AppDeployMetaData) -> bytes: } return json.dumps(note).encode() - def deploy(self, deployment: AppDeployParams) -> AppDeployResult: + def deploy(self, deployment: AppDeployParams) -> AppDeployResponse: # Create new instances with updated notes logger.info( f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " @@ -268,37 +299,35 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResult: clear_program=clear_program, ) - existing_app_dict = existing_app.__dict__ - existing_app_dict["operation_performed"] = OperationPerformed.Nothing - existing_app_dict["app_id"] = existing_app.app_id - existing_app_dict["app_address"] = existing_app.app_address - logger.debug("No detected changes in app, nothing to do.", suppress_log=deployment.suppress_log) - return AppDeployResult(**existing_app_dict) + return AppDeployResponse( + app=existing_app, + operation_performed=OperationPerformed.Nothing, + ) def _create_app( self, deployment: AppDeployParams, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResult: + ) -> AppDeployResponse: """Create a new application""" - if isinstance(deployment.create_params, AppCreateMethodCall): - result = self._transaction_sender.app_create_method_call( - AppCreateMethodCall( + if isinstance(deployment.create_params, AppCreateMethodCallParams): + create_response = self._transaction_sender.app_create_method_call( + AppCreateMethodCallParams( **{ - **deployment.create_params.__dict__, + **asdict(deployment.create_params), "approval_program": approval_program, "clear_state_program": clear_program, } ) ) else: - result = self._transaction_sender.app_create( + create_response = self._transaction_sender.app_create( AppCreateParams( **{ - **deployment.create_params.__dict__, + **asdict(deployment.create_params), "approval_program": approval_program, "clear_state_program": clear_program, } @@ -306,97 +335,40 @@ def _create_app( ) app_metadata = AppMetaData( - app_id=result.app_id, - app_address=get_application_address(result.app_id), - **deployment.metadata.__dict__, + app_id=create_response.app_id, + app_address=get_application_address(create_response.app_id), + **asdict(deployment.metadata), created_metadata=deployment.metadata, - created_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, - updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + created_round=create_response.confirmation.get("confirmed-round", 0) + if isinstance(create_response.confirmation, dict) + else 0, + updated_round=create_response.confirmation.get("confirmed-round", 0) + if isinstance(create_response.confirmation, dict) + else 0, deleted=False, ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - app_metadata_dict = app_metadata.__dict__ - app_metadata_dict["operation_performed"] = OperationPerformed.Create - app_metadata_dict["app_id"] = result.app_id - app_metadata_dict["app_address"] = get_application_address(result.app_id) - - return AppDeployResult( - **app_metadata_dict, - tx_id=result.tx_id, - tx_ids=result.tx_ids, - transaction=result.transaction, - transactions=result.transactions, - confirmation=result.confirmation, - confirmations=result.confirmations, - return_value=result.return_value, + return AppDeployResponse( + app=app_metadata, + operation_performed=OperationPerformed.Create, + create_response=create_response, ) - def _handle_schema_break( - self, - deployment: AppDeployParams, - existing_app: AppMetaData, - approval_program: bytes, - clear_program: bytes, - ) -> AppDeployResult: - if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): - raise ValueError( - "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " - "If you want to try deleting and recreating the app then " - "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp" - ) - - if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"): - return self._create_app(deployment, approval_program, clear_program) - - if existing_app.deletable: - return self._replace_app(deployment, existing_app, approval_program, clear_program) - else: - raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app") - - def _handle_update( - self, - deployment: AppDeployParams, - existing_app: AppMetaData, - approval_program: bytes, - clear_program: bytes, - ) -> AppDeployResult: - if deployment.on_update in (OnUpdate.Fail, "fail"): - raise ValueError( - "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." - ) - - if deployment.on_update in (OnUpdate.AppendApp, "append"): - return self._create_app(deployment, approval_program, clear_program) - - if deployment.on_update in (OnUpdate.UpdateApp, "update"): - if existing_app.updatable: - return self._update_app(deployment, existing_app, approval_program, clear_program) - else: - raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app") - - if deployment.on_update in (OnUpdate.ReplaceApp, "replace"): - if existing_app.deletable: - return self._replace_app(deployment, existing_app, approval_program, clear_program) - else: - raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app") - - raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") - def _replace_app( self, deployment: AppDeployParams, existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResult: + ) -> AppDeployResponse: composer = self._transaction_sender.new_group() # Add create transaction - if isinstance(deployment.create_params, AppCreateMethodCall): + if isinstance(deployment.create_params, AppCreateMethodCallParams): composer.add_app_create_method_call( - AppCreateMethodCall( + AppCreateMethodCallParams( **{ **deployment.create_params.__dict__, "approval_program": approval_program, @@ -414,10 +386,11 @@ def _replace_app( } ) ) + create_txn_index = composer.count() - 1 # Add delete transaction - if isinstance(deployment.delete_params, AppDeleteMethodCall): - delete_call_params = AppDeleteMethodCall( + if isinstance(deployment.delete_params, AppDeleteMethodCallParams): + delete_call_params = AppDeleteMethodCallParams( **{ **deployment.delete_params.__dict__, "app_id": existing_app.app_id, @@ -432,9 +405,13 @@ def _replace_app( } ) composer.add_app_delete(delete_params) + delete_txn_index = composer.count() - 1 result = composer.send() + create_response = SendAppCreateTransactionResult.from_composer_result(result, create_txn_index) + delete_response = SendAppTransactionResult.from_composer_result(result, delete_txn_index) + app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] app_metadata = AppMetaData( app_id=app_id, @@ -447,31 +424,12 @@ def _replace_app( ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - app_metadata_dict = app_metadata.__dict__ - app_metadata_dict["operation_performed"] = OperationPerformed.Replace - app_metadata_dict["app_id"] = app_id - app_metadata_dict["app_address"] = get_application_address(app_id) - - # Extract return_value and delete_return_value from ABIResult - return_value = result.returns[0] if result.returns and isinstance(result.returns[0], ABIResult) else None - delete_return_value = ( - result.returns[-1] if len(result.returns) > 1 and isinstance(result.returns[-1], ABIResult) else None - ) - - return AppDeployResult( - **app_metadata_dict, - tx_id=result.tx_ids[0], - tx_ids=result.tx_ids, - transaction=result.transactions[0], - transactions=result.transactions, - confirmation=result.confirmations[0], - confirmations=result.confirmations, - return_value=return_value, - delete_return_value=delete_return_value, - delete_result=ConfirmedTransactionResult( - transaction=result.transactions[-1], - confirmation=result.confirmations[-1], - ), + return AppDeployResponse( + app=app_metadata, + operation_performed=OperationPerformed.Replace, + create_response=create_response, + update_response=None, + delete_response=delete_response, ) def _update_app( @@ -480,12 +438,12 @@ def _update_app( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResult: + ) -> AppDeployResponse: """Update an existing application""" - if isinstance(deployment.update_params, AppUpdateMethodCall): + if isinstance(deployment.update_params, AppUpdateMethodCallParams): result = self._transaction_sender.app_update_method_call( - AppUpdateMethodCall( + AppUpdateMethodCallParams( **{ **deployment.update_params.__dict__, "app_id": existing_app.app_id, @@ -518,16 +476,63 @@ def _update_app( self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeployResult( - **app_metadata.__dict__, + return AppDeployResponse( + app=app_metadata, operation_performed=OperationPerformed.Update, - transaction=result.transaction, - transactions=result.transactions, - confirmation=result.confirmation, - confirmations=result.confirmations, - return_value=result.return_value, + update_response=result, ) + def _handle_schema_break( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResponse: + if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): + raise ValueError( + "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp" + ) + + if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app") + + def _handle_update( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResponse: + if deployment.on_update in (OnUpdate.Fail, "fail"): + raise ValueError( + "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." + ) + + if deployment.on_update in (OnUpdate.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if deployment.on_update in (OnUpdate.UpdateApp, "update"): + if existing_app.updatable: + return self._update_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app") + + if deployment.on_update in (OnUpdate.ReplaceApp, "replace"): + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app") + + raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") + def _update_app_lookup(self, sender: str, app_metadata: AppMetaData) -> None: """Update the app lookup cache""" diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 2c8c3a94..2316a57b 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -1,17 +1,24 @@ import base64 from collections.abc import Callable -from dataclasses import dataclass -from typing import Any, TypeGuard, TypeVar +from dataclasses import asdict, dataclass, replace +from typing import Any, TypeVar -import algosdk from algosdk import transaction from algosdk.abi import Method -from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner +from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.source_map import SourceMap from algosdk.transaction import OnComplete, Transaction +from typing_extensions import Self from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils._legacy_v2.deploy import AppDeployMetaData, AppLookup, OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.abi import ( + ABIReturn, + ABIStruct, + ABIValue, + Arc56ReturnValueType, + get_abi_decoded_value, + get_abi_tuple_from_abi_struct, +) from algokit_utils.applications.app_client import ( AppClient, AppClientBareCallParams, @@ -19,45 +26,57 @@ AppClientCompilationResult, AppClientMethodCallParams, AppClientParams, - AppSourceMaps, - ExposedLogicErrorDetails, ) from algokit_utils.applications.app_deployer import ( + AppDeployMetaData, AppDeployParams, - ConfirmedTransactionResult, - DeployAppDeleteParams, - DeployAppUpdateParams, -) -from algokit_utils.applications.app_manager import TealTemplateParams -from algokit_utils.applications.utils import ( - get_abi_decoded_value, - get_abi_tuple_from_abi_struct, - get_arc56_method, - get_arc56_return_value, + AppDeployResponse, + AppLookup, + AppMetaData, + OnSchemaBreak, + OnUpdate, + OperationPerformed, ) -from algokit_utils.models.abi import ABIStruct, ABIValue +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.applications.app_spec.arc56 import Arc56Contract from algokit_utils.models.application import ( - DELETABLE_TEMPLATE_NAME, - UPDATABLE_TEMPLATE_NAME, - Arc56Contract, - Arc56Method, - CompiledTeal, - MethodArg, + AppSourceMaps, ) +from algokit_utils.models.state import TealTemplateParams from algokit_utils.models.transaction import SendParams -from algokit_utils.protocols.application import AlgorandClientProtocol -from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.protocols.client import AlgorandClientProtocol from algokit_utils.transactions.transaction_composer import ( - AppCreateMethodCall, + AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCall, - AppUpdateMethodCall, + AppDeleteMethodCallParams, + AppDeleteParams, + AppUpdateMethodCallParams, + AppUpdateParams, BuiltTransactions, ) -from algokit_utils.transactions.transaction_sender import SendAppCreateTransactionResult, SendAppTransactionResult +from algokit_utils.transactions.transaction_sender import ( + SendAppCreateTransactionResult, + SendAppTransactionResult, + SendAppUpdateTransactionResult, + SendSingleTransactionResult, +) T = TypeVar("T") +__all__ = [ + "AppFactory", + "AppFactoryCreateMethodCallParams", + "AppFactoryCreateMethodCallResult", + "AppFactoryCreateMethodCallWithSendParams", + "AppFactoryCreateParams", + "AppFactoryCreateWithSendParams", + "AppFactoryDeployResponse", + "AppFactoryParams", + "SendAppCreateFactoryTransactionResult", + "SendAppFactoryTransactionResult", + "SendAppUpdateFactoryTransactionResult", +] + @dataclass(kw_only=True, frozen=True) class AppFactoryParams: @@ -91,55 +110,99 @@ class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompi extra_program_pages: int | None = None +@dataclass(frozen=True, kw_only=True) +class AppFactoryCreateMethodCallResult(SendSingleTransactionResult): + app_id: int + app_address: str + compiled_approval: Any | None = None + compiled_clear: Any | None = None + abi_return: ABIValue | ABIStruct | None = None + + @dataclass(kw_only=True, frozen=True) class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, SendParams): pass -@dataclass(frozen=True, kw_only=True) -class AppFactoryCreateResult(SendAppTransactionResult): - """Result from creating an application via AppFactory""" +@dataclass(frozen=True) +class SendAppFactoryTransactionResult(SendAppTransactionResult): + abi_value: Arc56ReturnValueType | None = None - app_id: int - """The ID of the created application""" - app_address: str - """The address of the created application""" - compiled_approval: CompiledTeal | None = None - """The compiled approval program if source was provided""" - compiled_clear: CompiledTeal | None = None - """The compiled clear program if source was provided""" +@dataclass(frozen=True) +class SendAppUpdateFactoryTransactionResult(SendAppUpdateTransactionResult): + abi_value: Arc56ReturnValueType | None = None -@dataclass(kw_only=True, frozen=True) -class AppFactoryDeployResult: - """Represents the result object from app deployment""" - app_address: str - app_id: int - approval_program: bytes # Uint8Array - clear_state_program: bytes # Uint8Array - compiled_approval: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap - compiled_clear: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap - confirmation: algosdk.v2client.algod.AlgodResponseType - confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None - created_metadata: dict # {name: str, version: str, updatable: bool, deletable: bool} - created_round: int - deletable: bool - deleted: bool - delete_return_value: ABIValue | ABIStruct | None = None - delete_result: ConfirmedTransactionResult | None = None - group_id: str | None = None - name: str +@dataclass(frozen=True, kw_only=True) +class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult): + abi_value: Arc56ReturnValueType | None = None + + +@dataclass(frozen=True) +class AppFactoryDeployResponse: + """Result from deploying an application via AppFactory""" + + app: AppMetaData operation_performed: OperationPerformed - return_value: ABIValue | ABIStruct | None = None - returns: list[Any] | None = None - transaction: TransactionWrapper - transactions: list[TransactionWrapper] - tx_id: str - tx_ids: list[str] - updatable: bool - updated_round: int - version: str + create_response: SendAppCreateFactoryTransactionResult | None = None + update_response: SendAppUpdateFactoryTransactionResult | None = None + delete_response: SendAppFactoryTransactionResult | None = None + + @classmethod + def from_deploy_response( + cls, + response: AppDeployResponse, + deploy_params: AppDeployParams, + app_spec: Arc56Contract, + app_compilation_data: AppClientCompilationResult | None = None, + ) -> Self: + def to_factory_response( + response_data: SendAppTransactionResult + | SendAppCreateTransactionResult + | SendAppUpdateTransactionResult + | None, + params: Any, # noqa: ANN401 + ) -> Any | None: # noqa: ANN401 + if not response_data: + return None + + abi_value = None + abi_return = response_data.abi_return + if abi_return and abi_return.method: + abi_value = abi_return.get_arc56_value(params.method, app_spec.structs) + + match response_data: + case SendAppCreateTransactionResult(): + return SendAppCreateFactoryTransactionResult(**asdict(response_data), abi_value=abi_value) + case SendAppUpdateTransactionResult(): + raw_response = asdict(response_data) + raw_response["compiled_approval"] = ( + app_compilation_data.compiled_approval if app_compilation_data else None + ) + raw_response["compiled_clear"] = ( + app_compilation_data.compiled_clear if app_compilation_data else None + ) + return SendAppUpdateFactoryTransactionResult(**raw_response, abi_value=abi_value) + case SendAppTransactionResult(): + return SendAppFactoryTransactionResult(**asdict(response_data), abi_value=abi_value) + + return cls( + app=response.app, + operation_performed=response.operation_performed, + create_response=to_factory_response( + response.create_response, + deploy_params.create_params, + ), + update_response=to_factory_response( + response.update_response, + deploy_params.update_params, + ), + delete_response=to_factory_response( + response.delete_response, + deploy_params.delete_params, + ), + ) class _AppFactoryBareParamsAccessor: @@ -148,45 +211,57 @@ def __init__(self, factory: "AppFactory") -> None: self._algorand = factory._algorand def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParams: - create_args = {} - if params: - create_args = {**params.__dict__.copy()} - del create_args["schema"] - del create_args["sender"] - del create_args["on_complete"] - del create_args["deploy_time_params"] - del create_args["updatable"] - del create_args["deletable"] - compiled = self._factory.compile(params) - create_args["approval_program"] = compiled.approval_program - create_args["clear_state_program"] = compiled.clear_state_program + base_params = params or AppFactoryCreateParams() + + compiled = self._factory.compile(base_params) return AppCreateParams( - **create_args, - schema=(params.schema if params else None) + approval_program=compiled.approval_program, + clear_state_program=compiled.clear_state_program, + schema=base_params.schema or { - "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], - "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], - "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], - "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + "global_bytes": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_bytes": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, }, - sender=self._factory._get_sender(params.sender if params else None), - on_complete=(params.on_complete if params else None) or OnComplete.NoOpOC, + sender=self._factory._get_sender(base_params.sender), + signer=self._factory._get_signer(base_params.sender, base_params.signer), + on_complete=base_params.on_complete or OnComplete.NoOpOC, + extra_program_pages=base_params.extra_program_pages, ) - def deploy_update(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: - return { - **(params.__dict__ if params else {}), - "sender": self._factory._get_sender(params.sender if params else None), - "on_complete": OnComplete.UpdateApplicationOC, - } + def deploy_update(self, params: AppClientBareCallParams | None = None) -> AppUpdateParams: + return AppUpdateParams( + app_id=0, + approval_program="", + clear_state_program="", + sender=self._factory._get_sender(params.sender if params else None), + on_complete=OnComplete.UpdateApplicationOC, + signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), + note=params.note if params else None, + lease=params.lease if params else None, + rekey_to=params.rekey_to if params else None, + account_references=params.account_references if params else None, + app_references=params.app_references if params else None, + asset_references=params.asset_references if params else None, + box_references=params.box_references if params else None, + ) - def deploy_delete(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: - return { - **(params.__dict__ if params else {}), - "sender": self._factory._get_sender(params.sender if params else None), - "on_complete": OnComplete.DeleteApplicationOC, - } + def deploy_delete(self, params: AppClientBareCallParams | None = None) -> AppDeleteParams: + return AppDeleteParams( + app_id=0, + sender=self._factory._get_sender(params.sender if params else None), + signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), + on_complete=OnComplete.DeleteApplicationOC, + note=params.note if params else None, + lease=params.lease if params else None, + rekey_to=params.rekey_to if params else None, + account_references=params.account_references if params else None, + app_references=params.app_references if params else None, + asset_references=params.asset_references if params else None, + box_references=params.box_references if params else None, + ) class _AppFactoryParamsAccessor: @@ -198,44 +273,57 @@ def __init__(self, factory: "AppFactory") -> None: def bare(self) -> _AppFactoryBareParamsAccessor: return self._bare - def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCall: + def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCallParams: compiled = self._factory.compile(params) - params_dict = params.__dict__ - params_dict["schema"] = params.schema or { - "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], - "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], - "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], - "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], - } - params_dict["sender"] = self._factory._get_sender(params.sender) - params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) - params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) - params_dict["on_complete"] = params.on_complete or OnComplete.NoOpOC - del params_dict["deploy_time_params"] - del params_dict["updatable"] - del params_dict["deletable"] - return AppCreateMethodCall( - **params_dict, + + return AppCreateMethodCallParams( app_id=0, approval_program=compiled.approval_program, clear_state_program=compiled.clear_state_program, + schema=params.schema + or { + "global_bytes": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_bytes": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, + }, + sender=self._factory._get_sender(params.sender), + signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), + method=self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), + on_complete=params.on_complete or OnComplete.NoOpOC, + note=params.note, + lease=params.lease, + rekey_to=params.rekey_to, ) - def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCall: - params_dict = params.__dict__.copy() - params_dict["sender"] = self._factory._get_sender(params.sender) - params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) - params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) - params_dict["on_complete"] = OnComplete.UpdateApplicationOC - return AppUpdateMethodCall(**params_dict, app_id=0, approval_program="", clear_state_program="") + def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCallParams: + return AppUpdateMethodCallParams( + app_id=0, + approval_program="", + clear_state_program="", + sender=self._factory._get_sender(params.sender), + signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), + method=self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), + on_complete=OnComplete.UpdateApplicationOC, + note=params.note, + lease=params.lease, + rekey_to=params.rekey_to, + ) - def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: - params_dict = params.__dict__.copy() - params_dict["sender"] = self._factory._get_sender(params.sender) - params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) - params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) - params_dict["on_complete"] = OnComplete.DeleteApplicationOC - return AppDeleteMethodCall(**params_dict, app_id=0) + def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: + return AppDeleteMethodCallParams( + app_id=0, + sender=self._factory._get_sender(params.sender), + signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), + method=self._factory.app_spec.get_arc56_method(params.method).to_abi_method(), + args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), + on_complete=OnComplete.DeleteApplicationOC, + note=params.note, + lease=params.lease, + rekey_to=params.rekey_to, + ) class _AppFactoryBareCreateTransactionAccessor: @@ -264,48 +352,51 @@ def __init__(self, factory: "AppFactory") -> None: self._factory = factory self._algorand = factory._algorand - def create(self, params: AppFactoryCreateWithSendParams | None = None) -> tuple[AppClient, AppFactoryCreateResult]: - updatable = params.updatable if params and params.updatable is not None else self._factory._updatable - deletable = params.deletable if params and params.deletable is not None else self._factory._deletable - deploy_time_params = ( - params.deploy_time_params - if params and params.deploy_time_params is not None - else self._factory._deploy_time_params + def create( + self, params: AppFactoryCreateWithSendParams | None = None + ) -> tuple[AppClient, SendAppCreateTransactionResult]: + base_params = params or AppFactoryCreateWithSendParams() + + # Use replace() to create new instance with overridden values + create_params = replace( + base_params, + updatable=base_params.updatable if base_params.updatable is not None else self._factory._updatable, + deletable=base_params.deletable if base_params.deletable is not None else self._factory._deletable, + deploy_time_params=( + base_params.deploy_time_params + if base_params.deploy_time_params is not None + else self._factory._deploy_time_params + ), ) compiled = self._factory.compile( AppClientCompilationParams( - deploy_time_params=deploy_time_params, - updatable=updatable, - deletable=deletable, + deploy_time_params=create_params.deploy_time_params, + updatable=create_params.updatable, + deletable=create_params.deletable, ) ) - create_args = {} - if params: - create_args = {**params.__dict__} - del create_args["max_rounds_to_wait"] - del create_args["suppress_log"] - del create_args["populate_app_call_resources"] - - create_args["updatable"] = updatable - create_args["deletable"] = deletable - create_args["deploy_time_params"] = deploy_time_params - result = self._factory._handle_call_errors( - lambda: self._algorand.send.app_create( - self._factory.params.bare.create(AppFactoryCreateParams(**create_args)) - ) - ).__dict__ - - result["compiled_approval"] = compiled.compiled_approval - result["compiled_clear"] = compiled.compiled_clear + lambda: self._algorand.send.app_create(self._factory.params.bare.create(create_params)) + ) return ( self._factory.get_app_client_by_id( - app_id=result["app_id"], + app_id=result.app_id, + ), + SendAppCreateTransactionResult( + transaction=result.transaction, + confirmation=result.confirmation, + app_id=result.app_id, + app_address=result.app_address, + compiled_approval=compiled.compiled_approval if compiled else None, + compiled_clear=compiled.compiled_clear if compiled else None, + group_id=result.group_id, + tx_ids=result.tx_ids, + transactions=result.transactions, + confirmations=result.confirmations, ), - AppFactoryCreateResult(**result), ) @@ -319,28 +410,30 @@ def __init__(self, factory: "AppFactory") -> None: def bare(self) -> _AppFactoryBareSendAccessor: return self._bare - def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, SendAppCreateTransactionResult]: - updatable = params.updatable if params.updatable is not None else self._factory._updatable - deletable = params.deletable if params.deletable is not None else self._factory._deletable - deploy_time_params = ( - params.deploy_time_params if params.deploy_time_params is not None else self._factory._deploy_time_params + def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, AppFactoryCreateMethodCallResult]: + create_params = replace( + params, + updatable=params.updatable if params.updatable is not None else self._factory._updatable, + deletable=params.deletable if params.deletable is not None else self._factory._deletable, + deploy_time_params=( + params.deploy_time_params + if params.deploy_time_params is not None + else self._factory._deploy_time_params + ), ) compiled = self._factory.compile( AppClientCompilationParams( - deploy_time_params=deploy_time_params, - updatable=updatable, - deletable=deletable, + deploy_time_params=create_params.deploy_time_params, + updatable=create_params.updatable, + deletable=create_params.deletable, ) ) - create_params_dict = params.__dict__.copy() - create_params_dict["updatable"] = updatable - create_params_dict["deletable"] = deletable - create_params_dict["deploy_time_params"] = deploy_time_params result = self._factory._handle_call_errors( - lambda: self._algorand.send.app_create_method_call( - self._factory.params.create(AppFactoryCreateMethodCallParams(**create_params_dict)) + lambda: self._factory._parse_method_call_return( + lambda: self._algorand.send.app_create_method_call(self._factory.params.create(create_params)), + self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), ) ) @@ -348,15 +441,20 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, S self._factory.get_app_client_by_id( app_id=result.app_id, ), - SendAppCreateTransactionResult( - **{ - **result.__dict__, - **( - {"compiled_approval": compiled.compiled_approval, "compiled_clear": compiled.compiled_clear} - if compiled - else {} - ), - } + AppFactoryCreateMethodCallResult( + transaction=result.transaction, + confirmation=result.confirmation, + tx_id=result.tx_id, + app_id=result.app_id, + app_address=result.app_address, + abi_return=result.abi_return, + compiled_approval=compiled.compiled_approval if compiled else None, + compiled_clear=compiled.compiled_clear if compiled else None, + group_id=result.group_id, + tx_ids=result.tx_ids, + transactions=result.transactions, + confirmations=result.confirmations, + returns=result.returns, ), ) @@ -416,119 +514,102 @@ def deploy( # noqa: PLR0913 updatable: bool | None = None, deletable: bool | None = None, app_name: str | None = None, - max_rounds_to_wait: int | None = None, # noqa: ARG002 TODO: revisit - suppress_log: bool = False, # noqa: ARG002 TODO: revisit - populate_app_call_resources: bool = False, # noqa: ARG002 TODO: revisit - ) -> tuple[AppClient, AppFactoryDeployResult]: - updatable = ( + max_rounds_to_wait: int | None = None, + suppress_log: bool = False, + populate_app_call_resources: bool = False, + ) -> tuple[AppClient, AppFactoryDeployResponse]: + """Deploy the application with the specified parameters.""" + + # Resolve control parameters with factory defaults + resolved_updatable = ( updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") ) - deletable = ( + resolved_deletable = ( deletable if deletable is not None else self._deletable or self._get_deploy_time_control("deletable") ) - deploy_time_params = deploy_time_params if deploy_time_params is not None else self._deploy_time_params + resolved_deploy_time_params = deploy_time_params or self._deploy_time_params - compiled = self.compile( - AppClientCompilationParams( - deploy_time_params=deploy_time_params, - updatable=updatable, - deletable=deletable, - ) - ) + def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: + """Prepare create arguments based on parameter type.""" + if create_params and isinstance(create_params, AppClientMethodCallParams): + return self.params.create( + AppFactoryCreateMethodCallParams( + **asdict(create_params), + updatable=resolved_updatable, + deletable=resolved_deletable, + deploy_time_params=resolved_deploy_time_params, + ) + ) - def _is_method_call_params( - params: AppClientMethodCallParams | AppClientBareCallParams | None, - ) -> TypeGuard[AppClientMethodCallParams]: - return params is not None and hasattr(params, "method") - - update_args: DeployAppUpdateParams | AppUpdateMethodCall - if _is_method_call_params(update_params): - update_args = self.params.deploy_update(update_params) - else: - update_args = DeployAppUpdateParams( - **self.params.bare.deploy_update( - update_params if isinstance(update_params, AppClientBareCallParams) else None + base_params = create_params or AppClientBareCallParams() + return self.params.bare.create( + AppFactoryCreateParams( + **asdict(base_params) if base_params else {}, + updatable=resolved_updatable, + deletable=resolved_deletable, + deploy_time_params=resolved_deploy_time_params, ) ) - delete_args: DeployAppDeleteParams | AppDeleteMethodCall - if _is_method_call_params(delete_params): - delete_args = self.params.deploy_delete(delete_params) - else: - delete_args = DeployAppDeleteParams( - **self.params.bare.deploy_delete( - delete_params if isinstance(delete_params, AppClientBareCallParams) else None - ) + def prepare_update_args() -> AppUpdateMethodCallParams | AppUpdateParams: + """Prepare update arguments based on parameter type.""" + return ( + self.params.deploy_update(update_params) + if isinstance(update_params, AppClientMethodCallParams) + else self.params.bare.deploy_update(update_params) ) - app_deploy_params = AppDeployParams( - deploy_time_params=deploy_time_params, + def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: + """Prepare delete arguments based on parameter type.""" + return ( + self.params.deploy_delete(delete_params) + if isinstance(delete_params, AppClientMethodCallParams) + else self.params.bare.deploy_delete(delete_params) + ) + + # Execute deployment + deploy_params = AppDeployParams( + deploy_time_params=resolved_deploy_time_params, on_schema_break=on_schema_break, on_update=on_update, existing_deployments=existing_deployments, ignore_cache=ignore_cache, - create_params=( - self.params.create( - AppFactoryCreateMethodCallParams( - **create_params.__dict__, - updatable=updatable, - deletable=deletable, - deploy_time_params=deploy_time_params, - ) - ) - if create_params and hasattr(create_params, "method") - else self.params.bare.create( - AppFactoryCreateParams( - **create_params.__dict__ if create_params else {}, - updatable=updatable, - deletable=deletable, - deploy_time_params=deploy_time_params, - ) - ) - ), - update_params=update_args, - delete_params=delete_args, + create_params=prepare_create_args(), + update_params=prepare_update_args(), + delete_params=prepare_delete_args(), metadata=AppDeployMetaData( name=app_name or self._app_name, version=self._version, - updatable=updatable, - deletable=deletable, + updatable=resolved_updatable, + deletable=resolved_deletable, ), + suppress_log=suppress_log, + max_rounds_to_wait=max_rounds_to_wait, + populate_app_call_resources=populate_app_call_resources, ) - deploy_result = self._algorand.app_deployer.deploy(app_deploy_params) + deploy_response = self._algorand.app_deployer.deploy(deploy_params) + # Prepare app client and factory deploy response app_client = self.get_app_client_by_id( - app_id=deploy_result.app_id or 0, + app_id=deploy_response.app.app_id, app_name=app_name, default_sender=self._default_sender, default_signer=self._default_signer, ) - - result = {**deploy_result.__dict__, **(compiled.__dict__ if compiled else {})} - - if "return_value" in result: - if result["operation_performed"] == OperationPerformed.Update: - if update_params and isinstance(update_params, AppClientMethodCallParams): - result["return_value"] = get_arc56_return_value( - result["return_value"], - get_arc56_method(update_params.method, self._app_spec), - self._app_spec.structs, - ) - elif create_params and isinstance(create_params, AppClientMethodCallParams): - result["return_value"] = get_arc56_return_value( - result["return_value"], - get_arc56_method(create_params.method, self._app_spec), - self._app_spec.structs, + factory_deploy_response = AppFactoryDeployResponse.from_deploy_response( + response=deploy_response, + deploy_params=deploy_params, + app_spec=app_client.app_spec, + app_compilation_data=self.compile( + AppClientCompilationParams( + deploy_time_params=resolved_deploy_time_params, + updatable=resolved_updatable, + deletable=resolved_deletable, ) + ), + ) - if "delete_return_value" in result and delete_params and isinstance(delete_params, AppClientMethodCallParams): - result["delete_return_value"] = get_arc56_return_value( - result["delete_return_value"], - get_arc56_method(delete_params.method, self._app_spec), - self._app_spec.structs, - ) - - return app_client, AppFactoryDeployResult(**result) + return app_client, factory_deploy_response def get_app_client_by_id( self, @@ -552,26 +633,28 @@ def get_app_client_by_id( ) ) - def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT002 FBT001 TODO: revisit - return AppClient.expose_logic_error_static( - e, - self._app_spec, - ExposedLogicErrorDetails( - is_clear_state_program=is_clear_state_program, - approval_source_map=self._approval_source_map, - clear_source_map=self._clear_source_map, - program=None, - approval_source_info=( - self._app_spec.source_info.get("approval") - if self._app_spec.source_info and hasattr(self._app_spec, "source_info") - else None - ), - clear_source_info=( - self._app_spec.source_info.get("clear") - if self._app_spec.source_info and hasattr(self._app_spec, "source_info") - else None - ), - ), + def get_app_client_by_creator_and_name( + self, + creator_address: str, + app_name: str, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient.from_creator_and_name( + creator_address=creator_address, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + app_spec=self._app_spec, + algorand=self._algorand, ) def export_source_maps(self) -> AppSourceMaps: @@ -591,8 +674,8 @@ def import_source_maps(self, source_maps: AppSourceMaps) -> None: def compile(self, compilation: AppClientCompilationParams | None = None) -> AppClientCompilationResult: result = AppClient.compile( - self._app_spec, - self._algorand.app, + app_spec=self._app_spec, + app_manager=self._algorand.app, deploy_time_params=compilation.deploy_time_params if compilation else None, updatable=compilation.updatable if compilation else None, deletable=compilation.deletable if compilation else None, @@ -605,17 +688,27 @@ def compile(self, compilation: AppClientCompilationParams | None = None) -> AppC return result - def _get_deploy_time_control(self, control: str) -> bool | None: - approval = ( - self._app_spec.source["approval"] if self._app_spec.source and "approval" in self._app_spec.source else None + def _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT002 FBT001 + return AppClient._expose_logic_error_static( + e=e, + app_spec=self._app_spec, + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=None, + approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None), + clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None), ) + def _get_deploy_time_control(self, control: str) -> bool | None: + approval = self._app_spec.source.get_decoded_approval() if self._app_spec.source else None + template_name = UPDATABLE_TEMPLATE_NAME if control == "updatable" else DELETABLE_TEMPLATE_NAME if not approval or template_name not in approval: return None on_complete = "UpdateApplication" if control == "updatable" else "DeleteApplication" - return on_complete in self._app_spec.bare_actions.get("call", []) or any( + return on_complete in self._app_spec.bare_actions.call or any( on_complete in m.actions.call for m in self._app_spec.methods if m.actions and m.actions.call ) @@ -626,65 +719,73 @@ def _get_sender(self, sender: str | bytes | None) -> str: ) return str(sender or self._default_sender) + def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: + return signer or (self._default_signer if not sender or sender == self._default_sender else None) + def _handle_call_errors(self, call: Callable[[], T]) -> T: try: return call() except Exception as e: - raise self.expose_logic_error(e) from None + raise self._expose_logic_error(e) from None - def _parse_method_call_return(self, result: SendAppTransactionResult, method: Method) -> SendAppTransactionResult: - return SendAppTransactionResult( + def _parse_method_call_return( + self, + result: Callable[ + [], SendAppTransactionResult | SendAppCreateTransactionResult | SendAppUpdateTransactionResult + ], + method: Method, + ) -> AppFactoryCreateMethodCallResult: + result_value = result() + return AppFactoryCreateMethodCallResult( **{ - **result.__dict__, - "return_value": get_arc56_return_value(result.return_value, method, self._app_spec.structs) - if isinstance(result.return_value, ABIResult) + **result_value.__dict__, + "abi_return": result_value.abi_return.get_arc56_value(method, self._app_spec.structs) + if isinstance(result_value.abi_return, ABIReturn) else None, } ) def _get_create_abi_args_with_default_values( self, - method_name_or_signature: str | Arc56Method, - args: list[Any] | None, + method_name_or_signature: str, + user_args: list[Any] | None, ) -> list[Any]: - method = ( - get_arc56_method(method_name_or_signature, self._app_spec) - if isinstance(method_name_or_signature, str) - else method_name_or_signature - ) - result = [] - - def _has_struct(arg: Any) -> TypeGuard[MethodArg]: # noqa: ANN401 - return hasattr(arg, "struct") - - for i, method_arg in enumerate(method.args): - arg = method_arg - arg_value = args[i] if args and i < len(args) else None - - if arg_value is not None: - if _has_struct(arg) and arg.struct and isinstance(arg_value, dict): + """ + Builds a list of ABI argument values for creation calls, applying default + argument values when not provided. + """ + method = self._app_spec.get_arc56_method(method_name_or_signature) + + results: list[Any] = [] + + for i, param in enumerate(method.args): + if user_args and i < len(user_args): + arg_value = user_args[i] + if param.struct and isinstance(arg_value, dict): arg_value = get_abi_tuple_from_abi_struct( arg_value, - self._app_spec.structs[arg.struct], + self._app_spec.structs[param.struct], self._app_spec.structs, ) - result.append(arg_value) + results.append(arg_value) continue - default_value = getattr(arg, "default_value", None) + default_value = getattr(param, "default_value", None) if default_value: if default_value.source == "literal": - value_raw = base64.b64decode(default_value.data) - value_type = default_value.type or str(arg.type) - result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) + raw_value = base64.b64decode(default_value.data) + value_type = default_value.type or str(param.type) + decoded_value = get_abi_decoded_value(raw_value, value_type, self._app_spec.structs) + results.append(decoded_value) else: raise ValueError( - f"Can't provide default value for {default_value.source} for a contract creation call" + f"Cannot provide default value from source={default_value.source} " + "for a contract creation call." ) else: + param_name = param.name or f"arg{i + 1}" raise ValueError( - f"No value provided for required argument " - f"{arg.name or f'arg{i+1}'} in call to method {method.name}" + f"No value provided for required argument {param_name} " f"in call to method {method.name}" ) - return result + return results diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index 9ad6c5fe..ad198a81 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -1,74 +1,36 @@ import base64 from collections.abc import Mapping -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, TypeAlias, cast +from typing import Any, cast import algosdk import algosdk.atomic_transaction_composer import algosdk.box_reference -from algosdk.atomic_transaction_composer import ABIResult, AccountTransactionSigner +from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.box_reference import BoxReference as AlgosdkBoxReference from algosdk.logic import get_application_address from algosdk.source_map import SourceMap from algosdk.v2client import algod -from algokit_utils.models.abi import ABIType, ABIValue +from algokit_utils.applications.abi import ABIReturn, ABIType, ABIValue from algokit_utils.models.application import ( - DELETABLE_TEMPLATE_NAME, - UPDATABLE_TEMPLATE_NAME, AppInformation, AppState, CompiledTeal, ) +from algokit_utils.models.state import BoxIdentifier, BoxName, BoxReference, DataTypeFlag, TealTemplateParams +__all__ = [ + "DELETABLE_TEMPLATE_NAME", + "UPDATABLE_TEMPLATE_NAME", + "AppManager", +] -@dataclass(kw_only=True, frozen=True) -class BoxName: - name: str - name_raw: bytes - name_base64: str +UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" +"""The name of the TEAL template variable for deploy-time immutability control.""" -@dataclass(kw_only=True, frozen=True) -class BoxValue: - name: BoxName - value: bytes - - -@dataclass(kw_only=True, frozen=True) -class BoxABIValue: - name: BoxName - value: ABIValue - - -class DataTypeFlag(IntEnum): - BYTES = 1 - UINT = 2 - - -TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] - - -BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner - - -class BoxReference(AlgosdkBoxReference): - def __init__(self, app_id: int, name: bytes | str): - super().__init__(app_index=app_id, name=self._b64_decode(name)) - - def __eq__(self, other: object) -> bool: - if isinstance(other, (BoxReference | AlgosdkBoxReference)): - return self.app_index == other.app_index and self.name == other.name - return False - - def _b64_decode(self, value: str | bytes) -> bytes: - if isinstance(value, str): - try: - return base64.b64decode(value) - except Exception: - return value.encode("utf-8") - return value +DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" +"""The name of the TEAL template variable for deploy-time permanence control.""" def _is_valid_token_character(char: str) -> bool: @@ -286,7 +248,7 @@ def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes] @staticmethod def get_abi_return( confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None - ) -> ABIResult | None: + ) -> ABIReturn | None: """Get the ABI return value from a transaction confirmation.""" if not method: return None @@ -302,7 +264,7 @@ def get_abi_return( if not abi_result: return None - return abi_result + return ABIReturn(abi_result) @staticmethod def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: diff --git a/src/algokit_utils/applications/app_spec/__init__.py b/src/algokit_utils/applications/app_spec/__init__.py new file mode 100644 index 00000000..dbbb41fb --- /dev/null +++ b/src/algokit_utils/applications/app_spec/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.applications.app_spec.arc32 import * # noqa: F403 +from algokit_utils.applications.app_spec.arc56 import * # noqa: F403 diff --git a/src/algokit_utils/applications/app_spec/arc32.py b/src/algokit_utils/applications/app_spec/arc32.py new file mode 100644 index 00000000..3be8a42c --- /dev/null +++ b/src/algokit_utils/applications/app_spec/arc32.py @@ -0,0 +1,204 @@ +import base64 +import dataclasses +import json +from enum import IntFlag +from pathlib import Path +from typing import Any, Literal, TypeAlias, TypedDict + +from algosdk.abi import Contract +from algosdk.abi.method import MethodDict +from algosdk.transaction import StateSchema + +__all__ = [ + "AppSpecStateDict", + "Arc32Contract", + "CallConfig", + "DefaultArgumentDict", + "DefaultArgumentType", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", + "StateDict", + "StructArgDict", +] + + +AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] +"""Type defining Application Specification state entries""" + + +class CallConfig(IntFlag): + """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" + + NEVER = 0 + """Never handle the specified on completion type""" + CALL = 1 + """Only handle the specified on completion type for application calls""" + CREATE = 2 + """Only handle the specified on completion type for application create calls""" + ALL = 3 + """Handle the specified on completion type for both create and normal application calls""" + + +class StructArgDict(TypedDict): + name: str + elements: list[list[str]] + + +OnCompleteActionName: TypeAlias = Literal[ + "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" +] +"""String literals representing on completion transaction types""" +MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] +"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" +DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] +"""Literal values describing the types of default argument sources""" + + +class DefaultArgumentDict(TypedDict): + """ + DefaultArgument is a container for any arguments that may + be resolved prior to calling some target method + """ + + source: DefaultArgumentType + data: int | str | bytes | MethodDict + + +StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword + "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} +) + + +@dataclasses.dataclass(kw_only=True) +class MethodHints: + """MethodHints provides hints to the caller about how to call the method""" + + #: hint to indicate this method can be called through Dryrun + read_only: bool = False + #: hint to provide names for tuple argument indices + #: method_name=>param_name=>{name:str, elements:[str,str]} + structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) + #: defaults + default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) + call_config: MethodConfigDict = dataclasses.field(default_factory=dict) + + def empty(self) -> bool: + return not self.dictify() + + def dictify(self) -> dict[str, Any]: + d: dict[str, Any] = {} + if self.read_only: + d["read_only"] = True + if self.default_arguments: + d["default_arguments"] = self.default_arguments + if self.structs: + d["structs"] = self.structs + if any(v for v in self.call_config.values() if v != CallConfig.NEVER): + d["call_config"] = _encode_method_config(self.call_config) + return d + + @staticmethod + def undictify(data: dict[str, Any]) -> "MethodHints": + return MethodHints( + read_only=data.get("read_only", False), + default_arguments=data.get("default_arguments", {}), + structs=data.get("structs", {}), + call_config=_decode_method_config(data.get("call_config", {})), + ) + + +def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: + return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} + + +def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: + return {k: CallConfig[v] for k, v in data.items()} + + +def _encode_source(teal_text: str) -> str: + return base64.b64encode(teal_text.encode()).decode("utf-8") + + +def _decode_source(b64_text: str) -> str: + return base64.b64decode(b64_text).decode("utf-8") + + +def _encode_state_schema(schema: StateSchema) -> dict[str, int]: + return { + "num_byte_slices": schema.num_byte_slices, + "num_uints": schema.num_uints, + } # type: ignore[unused-ignore] + + +def _decode_state_schema(data: dict[str, int]) -> StateSchema: + return StateSchema( + num_byte_slices=data.get("num_byte_slices", 0), + num_uints=data.get("num_uints", 0), + ) + + +@dataclasses.dataclass(kw_only=True) +class Arc32Contract: + """ARC-0032 application specification + + See """ + + approval_program: str + clear_program: str + contract: Contract + hints: dict[str, MethodHints] + schema: StateDict + global_state_schema: StateSchema + local_state_schema: StateSchema + bare_call_config: MethodConfigDict + + def dictify(self) -> dict: + return { + "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, + "source": { + "approval": _encode_source(self.approval_program), + "clear": _encode_source(self.clear_program), + }, + "state": { + "global": _encode_state_schema(self.global_state_schema), + "local": _encode_state_schema(self.local_state_schema), + }, + "schema": self.schema, + "contract": self.contract.dictify(), + "bare_call_config": _encode_method_config(self.bare_call_config), + } + + def to_json(self) -> str: + return json.dumps(self.dictify(), indent=4) + + @staticmethod + def from_json(application_spec: str) -> "Arc32Contract": + json_spec = json.loads(application_spec) + return Arc32Contract( + approval_program=_decode_source(json_spec["source"]["approval"]), + clear_program=_decode_source(json_spec["source"]["clear"]), + schema=json_spec["schema"], + global_state_schema=_decode_state_schema(json_spec["state"]["global"]), + local_state_schema=_decode_state_schema(json_spec["state"]["local"]), + contract=Contract.undictify(json_spec["contract"]), + hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, + bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), + ) + + def export(self, directory: Path | str | None = None) -> None: + """write out the artifacts generated by the application to disk + + Args: + directory(optional): path to the directory where the artifacts should be written + """ + if directory is None: + output_dir = Path.cwd() + else: + output_dir = Path(directory) + output_dir.mkdir(exist_ok=True, parents=True) + + (output_dir / "approval.teal").write_text(self.approval_program) + (output_dir / "clear.teal").write_text(self.clear_program) + (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) + (output_dir / "application.json").write_text(self.to_json()) diff --git a/src/algokit_utils/applications/app_spec/arc56.py b/src/algokit_utils/applications/app_spec/arc56.py new file mode 100644 index 00000000..80d13d54 --- /dev/null +++ b/src/algokit_utils/applications/app_spec/arc56.py @@ -0,0 +1,777 @@ +from __future__ import annotations + +import base64 +import json +from base64 import b64encode +from collections.abc import Callable, Sequence +from dataclasses import asdict, dataclass +from enum import Enum +from typing import Any, Literal, overload + +import algosdk +from algosdk.abi import Method as AlgosdkMethod + +from algokit_utils.applications.app_spec.arc32 import Arc32Contract + +__all__ = [ + "Actions", + "Arc56Contract", + "BareActions", + "Boxes", + "ByteCode", + "CallEnum", + "Compiler", + "CompilerInfo", + "CompilerVersion", + "CreateEnum", + "DefaultValue", + "Event", + "EventArg", + "Global", + "Keys", + "Local", + "Maps", + "Method", + "MethodArg", + "Network", + "PcOffsetMethod", + "ProgramSourceInfo", + "Recommendations", + "Returns", + "Schema", + "ScratchVariables", + "Source", + "SourceInfo", + "SourceInfoModel", + "State", + "StorageKey", + "StorageMap", + "StructField", + "TemplateVariables", +] + + +class _ActionType(str, Enum): + CALL = "CALL" + CREATE = "CREATE" + + +@dataclass +class StructField: + name: str + type: list[StructField] | str + + @staticmethod + def from_dict(data: dict[str, Any]) -> StructField: + if isinstance(data["type"], list): + data["type"] = [StructField.from_dict(item) for item in data["type"]] + return StructField(**data) + + +class CallEnum(str, Enum): + CLEAR_STATE = "ClearState" + CLOSE_OUT = "CloseOut" + DELETE_APPLICATION = "DeleteApplication" + NO_OP = "NoOp" + OPT_IN = "OptIn" + UPDATE_APPLICATION = "UpdateApplication" + + +class CreateEnum(str, Enum): + DELETE_APPLICATION = "DeleteApplication" + NO_OP = "NoOp" + OPT_IN = "OptIn" + + +@dataclass +class BareActions: + call: list[CallEnum] + create: list[CreateEnum] + + @staticmethod + def from_dict(data: dict[str, Any]) -> BareActions: + return BareActions(**data) + + +@dataclass +class ByteCode: + approval: str + clear: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> ByteCode: + return ByteCode(**data) + + +class Compiler(str, Enum): + ALGOD = "algod" + PUYA = "puya" + + +@dataclass +class CompilerVersion: + commit_hash: str | None = None + major: int | None = None + minor: int | None = None + patch: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> CompilerVersion: + return CompilerVersion(**data) + + +@dataclass +class CompilerInfo: + compiler: Compiler + compiler_version: CompilerVersion + + @staticmethod + def from_dict(data: dict[str, Any]) -> CompilerInfo: + data["compiler_version"] = CompilerVersion.from_dict(data["compiler_version"]) + return CompilerInfo(**data) + + +@dataclass +class Network: + app_id: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Network: + return Network(**data) + + +@dataclass +class ScratchVariables: + slot: int + type: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> ScratchVariables: + return ScratchVariables(**data) + + +@dataclass +class Source: + approval: str + clear: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> Source: + return Source(**data) + + def get_decoded_approval(self) -> str: + return self._decode_source(self.approval) + + def get_decoded_clear(self) -> str: + return self._decode_source(self.clear) + + def _decode_source(self, b64_text: str) -> str: + return base64.b64decode(b64_text).decode("utf-8") + + +@dataclass +class Global: + bytes: int + ints: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Global: + return Global(**data) + + +@dataclass +class Local: + bytes: int + ints: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Local: + return Local(**data) + + +@dataclass +class Schema: + global_state: Global # actual schema field is "global" since it's a reserved word + local_state: Local # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Schema: + global_state = Global.from_dict(data["global"]) + local_state = Local.from_dict(data["local"]) + return Schema(global_state=global_state, local_state=local_state) + + +@dataclass +class TemplateVariables: + type: str + value: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> TemplateVariables: + return TemplateVariables(**data) + + +@dataclass +class EventArg: + type: str + desc: str | None = None + name: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> EventArg: + return EventArg(**data) + + +@dataclass +class Event: + args: list[EventArg] + name: str + desc: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Event: + data["args"] = [EventArg.from_dict(item) for item in data["args"]] + return Event(**data) + + +@dataclass +class Actions: + call: list[CallEnum] | None = None + create: list[CreateEnum] | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Actions: + return Actions(**data) + + +@dataclass +class DefaultValue: + data: str + source: Literal["box", "global", "local", "literal", "method"] + type: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> DefaultValue: + return DefaultValue(**data) + + +@dataclass +class MethodArg: + type: str + default_value: DefaultValue | None = None + desc: str | None = None + name: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> MethodArg: + if data.get("default_value"): + data["default_value"] = DefaultValue.from_dict(data["default_value"]) + return MethodArg(**data) + + +@dataclass +class Boxes: + key: str + read_bytes: int + write_bytes: int + app: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Boxes: + return Boxes(**data) + + +@dataclass +class Recommendations: + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + boxes: Boxes | None = None + inner_transaction_count: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Recommendations: + if data.get("boxes"): + data["boxes"] = Boxes.from_dict(data["boxes"]) + return Recommendations(**data) + + +@dataclass +class Returns: + type: str + desc: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Returns: + return Returns(**data) + + +@dataclass +class Method: + actions: Actions + args: list[MethodArg] + name: str + returns: Returns + desc: str | None = None + events: list[Event] | None = None + readonly: bool | None = None + recommendations: Recommendations | None = None + + _abi_method: AlgosdkMethod | None = None + + def __post_init__(self) -> None: + self._abi_method = AlgosdkMethod.undictify(asdict(self)) + + def to_abi_method(self) -> AlgosdkMethod: + if self._abi_method is None: + raise ValueError("Underlying core ABI method class is not initialized!") + return self._abi_method + + @staticmethod + def from_dict(data: dict[str, Any]) -> Method: + data["actions"] = Actions.from_dict(data["actions"]) + data["args"] = [MethodArg.from_dict(item) for item in data["args"]] + data["returns"] = Returns.from_dict(data["returns"]) + if data.get("events"): + data["events"] = [Event.from_dict(item) for item in data["events"]] + if data.get("recommendations"): + data["recommendations"] = Recommendations.from_dict(data["recommendations"]) + return Method(**data) + + +class PcOffsetMethod(str, Enum): + CBLOCKS = "cblocks" + NONE = "none" + + +@dataclass +class SourceInfo: + pc: list[int] + error_message: str | None = None + source: str | None = None + teal: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SourceInfo: + return SourceInfo(**data) + + +@dataclass +class StorageKey: + key: str + key_type: str + value_type: str + desc: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> StorageKey: + return StorageKey(**data) + + +@dataclass +class StorageMap: + key_type: str + value_type: str + desc: str | None = None + prefix: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> StorageMap: + return StorageMap(**data) + + +@dataclass +class Keys: + box: dict[str, StorageKey] + global_state: dict[str, StorageKey] # actual schema field is "global" since it's a reserved word + local_state: dict[str, StorageKey] # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Keys: + box = {key: StorageKey.from_dict(value) for key, value in data["box"].items()} + global_state = {key: StorageKey.from_dict(value) for key, value in data["global"].items()} + local_state = {key: StorageKey.from_dict(value) for key, value in data["local"].items()} + return Keys(box=box, global_state=global_state, local_state=local_state) + + +@dataclass +class Maps: + box: dict[str, StorageMap] + global_state: dict[str, StorageMap] # actual schema field is "global" since it's a reserved word + local_state: dict[str, StorageMap] # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Maps: + box = {key: StorageMap.from_dict(value) for key, value in data["box"].items()} + global_state = {key: StorageMap.from_dict(value) for key, value in data["global"].items()} + local_state = {key: StorageMap.from_dict(value) for key, value in data["local"].items()} + return Maps(box=box, global_state=global_state, local_state=local_state) + + +@dataclass +class State: + keys: Keys + maps: Maps + schema: Schema + + @staticmethod + def from_dict(data: dict[str, Any]) -> State: + data["keys"] = Keys.from_dict(data["keys"]) + data["maps"] = Maps.from_dict(data["maps"]) + data["schema"] = Schema.from_dict(data["schema"]) + return State(**data) + + +@dataclass +class ProgramSourceInfo: + pc_offset_method: PcOffsetMethod + source_info: list[SourceInfo] + + @staticmethod + def from_dict(data: dict[str, Any]) -> ProgramSourceInfo: + data["source_info"] = [SourceInfo.from_dict(item) for item in data["source_info"]] + return ProgramSourceInfo(**data) + + +@dataclass +class SourceInfoModel: + approval: ProgramSourceInfo + clear: ProgramSourceInfo + + @staticmethod + def from_dict(data: dict[str, Any]) -> SourceInfoModel: + data["approval"] = ProgramSourceInfo.from_dict(data["approval"]) + data["clear"] = ProgramSourceInfo.from_dict(data["clear"]) + return SourceInfoModel(**data) + + +def _dict_keys_to_snake_case( + value: Any, # noqa: ANN401 +) -> Any: # noqa: ANN401 + def camel_to_snake(s: str) -> str: + return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_") + + match value: + case dict(): + new_dict: dict[str, Any] = {} + for key, val in value.items(): + new_dict[camel_to_snake(str(key))] = _dict_keys_to_snake_case(val) + return new_dict + case list(): + return [_dict_keys_to_snake_case(item) for item in value] + case _: + return value + + +class _Arc32ToArc56Converter: + def __init__(self, arc32_application_spec: str): + self.arc32 = json.loads(arc32_application_spec) + + def convert(self) -> Arc56Contract: + source_data = self.arc32.get("source") + return Arc56Contract( + name=self.arc32["contract"]["name"], + desc=self.arc32["contract"].get("desc"), + arcs=[], + methods=self._convert_methods(self.arc32), + structs=self._convert_structs(self.arc32), + state=self._convert_state(self.arc32), + source=Source(**source_data) if source_data else None, + bare_actions=BareActions( + call=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CALL), + create=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CREATE), + ), + ) + + def _convert_storage_keys(self, schema: dict) -> dict[str, StorageKey]: + """Convert ARC32 schema declared fields to ARC56 storage keys.""" + return { + name: StorageKey( + key=b64encode(field["key"].encode()).decode(), + key_type="AVMString", + value_type="AVMUint64" if field["type"] == "uint64" else "AVMBytes", + desc=field.get("descr"), + ) + for name, field in schema.items() + } + + def _convert_state(self, arc32: dict) -> State: + """Convert ARC32 state and schema to ARC56 state specification.""" + state_data = arc32.get("state", {}) + return State( + schema=Schema( + global_state=Global( + ints=state_data.get("global", {}).get("num_uints", 0), + bytes=state_data.get("global", {}).get("num_byte_slices", 0), + ), + local_state=Local( + ints=state_data.get("local", {}).get("num_uints", 0), + bytes=state_data.get("local", {}).get("num_byte_slices", 0), + ), + ), + keys=Keys( + global_state=self._convert_storage_keys(arc32.get("schema", {}).get("global", {}).get("declared", {})), + local_state=self._convert_storage_keys(arc32.get("schema", {}).get("local", {}).get("declared", {})), + box={}, + ), + maps=Maps(global_state={}, local_state={}, box={}), + ) + + def _convert_structs(self, arc32: dict) -> dict[str, list[StructField]]: + """Extract and convert struct definitions from hints.""" + return { + struct["name"]: [StructField(name=elem[0], type=elem[1]) for elem in struct["elements"]] + for hint in arc32.get("hints", {}).values() + for struct in hint.get("structs", {}).values() + } + + def _convert_default_value(self, arg_type: str, default_arg: dict[str, Any] | None) -> DefaultValue | None: + """Convert ARC32 default argument to ARC56 format.""" + if not default_arg or not default_arg.get("source"): + return None + + source_mapping = { + "constant": "literal", + "global-state": "global", + "local-state": "local", + "abi-method": "method", + } + + mapped_source = source_mapping.get(default_arg["source"]) + if not mapped_source: + return None + elif mapped_source == "method": + return DefaultValue( + source=mapped_source, # type: ignore[arg-type] + data=default_arg.get("data", {}).get("name"), + ) + + arg_data = default_arg.get("data") + + if isinstance(arg_data, int): + arg_data = algosdk.abi.ABIType.from_string("uint64").encode(arg_data) + elif isinstance(arg_data, str): + arg_data = arg_data.encode() + else: + raise ValueError(f"Invalid default argument data type: {type(arg_data)}") + + return DefaultValue( + source=mapped_source, # type: ignore[arg-type] + data=base64.b64encode(arg_data).decode("utf-8"), + type=arg_type if arg_type != "string" else "AVMString", + ) + + @overload + def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CALL]) -> list[CallEnum]: ... + + @overload + def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CREATE]) -> list[CreateEnum]: ... + + def _convert_actions(self, config: dict | None, action_type: _ActionType) -> Sequence[CallEnum | CreateEnum]: + """Extract supported actions from call config.""" + if not config: + return [] + + actions: list[CallEnum | CreateEnum] = [] + mappings = { + "no_op": (CallEnum.NO_OP, CreateEnum.NO_OP), + "opt_in": (CallEnum.OPT_IN, CreateEnum.OPT_IN), + "close_out": (CallEnum.CLOSE_OUT, None), + "delete_application": (CallEnum.DELETE_APPLICATION, CreateEnum.DELETE_APPLICATION), + "update_application": (CallEnum.UPDATE_APPLICATION, None), + } + + for action, (call_enum, create_enum) in mappings.items(): + if action in config and config[action] in ["ALL", action_type]: + if action_type == "CALL" and call_enum: + actions.append(call_enum) + elif action_type == "CREATE" and create_enum: + actions.append(create_enum) + + return actions + + def _convert_method_actions(self, hint: dict | None) -> Actions: + """Convert method call config to ARC56 actions.""" + config = hint.get("call_config", {}) if hint else {} + return Actions( + call=self._convert_actions(config, _ActionType.CALL), + create=self._convert_actions(config, _ActionType.CREATE), + ) + + def _convert_methods(self, arc32: dict) -> list[Method]: + """Convert ARC32 methods to ARC56 format.""" + methods = [] + contract = arc32["contract"] + hints = arc32.get("hints", {}) + + for method in contract["methods"]: + args_sig = ",".join(a["type"] for a in method["args"]) + signature = f"{method['name']}({args_sig}){method['returns']['type']}" + hint = hints.get(signature, {}) + + methods.append( + Method( + name=method["name"], + desc=method.get("desc"), + readonly=hint.get("read_only"), + args=[ + MethodArg( + name=arg.get("name"), + type=arg["type"], + desc=arg.get("desc"), + struct=hint.get("structs", {}).get(arg.get("name", ""), {}).get("name"), + default_value=self._convert_default_value( + arg["type"], hint.get("default_arguments", {}).get(arg.get("name")) + ), + ) + for arg in method["args"] + ], + returns=Returns( + type=method["returns"]["type"], + desc=method["returns"].get("desc"), + struct=hint.get("structs", {}).get("output", {}).get("name"), + ), + actions=self._convert_method_actions(hint), + events=[], # ARC32 doesn't specify events + ) + ) + return methods + + +def _arc56_dict_factory() -> Callable[[list[tuple[str, Any]]], dict[str, Any]]: + """Creates a dict factory that handles ARC-56 JSON field naming conventions.""" + + word_map = {"global_state": "global", "local_state": "local"} + blocklist = ["_abi_method"] + + def to_camel(key: str) -> str: + key = word_map.get(key, key) + words = key.split("_") + return words[0] + "".join(word.capitalize() for word in words[1:]) + + def dict_factory(entries: list[tuple[str, Any]]) -> dict[str, Any]: + return {to_camel(k): v for k, v in entries if v is not None and k not in blocklist} + + return dict_factory + + +@dataclass +class Arc56Contract: + """ARC-0056 application specification + See https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md + """ + + arcs: list[int] + bare_actions: BareActions + methods: list[Method] + name: str + state: State + structs: dict[str, list[StructField]] + byte_code: ByteCode | None = None + compiler_info: CompilerInfo | None = None + desc: str | None = None + events: list[Event] | None = None + networks: dict[str, Network] | None = None + scratch_variables: dict[str, ScratchVariables] | None = None + source: Source | None = None + source_info: SourceInfoModel | None = None + template_variables: dict[str, TemplateVariables] | None = None + + @staticmethod + def from_dict(application_spec: dict) -> Arc56Contract: + data = _dict_keys_to_snake_case(application_spec) + data["bare_actions"] = BareActions.from_dict(data["bare_actions"]) + data["methods"] = [Method.from_dict(item) for item in data["methods"]] + data["state"] = State.from_dict(data["state"]) + data["structs"] = { + key: [StructField.from_dict(item) for item in value] for key, value in data["structs"].items() + } + if data.get("byte_code"): + data["byte_code"] = ByteCode.from_dict(data["byte_code"]) + if data.get("compiler_info"): + data["compiler_info"] = CompilerInfo.from_dict(data["compiler_info"]) + if data.get("events"): + data["events"] = [Event.from_dict(item) for item in data["events"]] + if data.get("networks"): + data["networks"] = {key: Network.from_dict(value) for key, value in data["networks"].items()} + if data.get("scratch_variables"): + data["scratch_variables"] = { + key: ScratchVariables.from_dict(value) for key, value in data["scratch_variables"].items() + } + if data.get("source"): + data["source"] = Source.from_dict(data["source"]) + if data.get("source_info"): + data["source_info"] = SourceInfoModel.from_dict(data["source_info"]) + if data.get("template_variables"): + data["template_variables"] = { + key: TemplateVariables.from_dict(value) for key, value in data["template_variables"].items() + } + return Arc56Contract(**data) + + @staticmethod + def from_json(application_spec: str) -> Arc56Contract: + return Arc56Contract.from_dict(json.loads(application_spec)) + + @staticmethod + def from_arc32(arc32_application_spec: str | Arc32Contract) -> Arc56Contract: + return _Arc32ToArc56Converter( + arc32_application_spec.to_json() + if isinstance(arc32_application_spec, Arc32Contract) + else arc32_application_spec + ).convert() + + @staticmethod + def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], + ) -> dict[str, Any]: + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = Arc56Contract.get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = Arc56Contract.get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + def to_json(self) -> str: + return json.dumps(self.dictify(), indent=4) + + def dictify(self) -> dict: + return asdict(self, dict_factory=_arc56_dict_factory()) + + def get_arc56_method(self, method_name_or_signature: str) -> Method: + if "(" not in method_name_or_signature: + # Filter by method name + methods = [m for m in self.methods if m.name == method_name_or_signature] + if not methods: + raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.") + if len(methods) > 1: + signatures = [AlgosdkMethod.undictify(m.__dict__).get_signature() for m in self.methods] + raise ValueError( + f"Received a call to method {method_name_or_signature} in contract {self.name}, " + f"but this resolved to multiple methods; please pass in an ABI signature instead: " + f"{', '.join(signatures)}" + ) + method = methods[0] + else: + # Find by signature + method = None + for m in self.methods: + abi_method = AlgosdkMethod.undictify(asdict(m)) + if abi_method.get_signature() == method_name_or_signature: + method = m + break + + if method is None: + raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.") + + return method diff --git a/src/algokit_utils/applications/utils.py b/src/algokit_utils/applications/utils.py deleted file mode 100644 index 05bc4650..00000000 --- a/src/algokit_utils/applications/utils.py +++ /dev/null @@ -1,428 +0,0 @@ -import base64 -from typing import Any, Literal, TypeVar - -from algosdk.abi import Method as AlgorandABIMethod -from algosdk.abi import TupleType -from algosdk.atomic_transaction_composer import ABIResult - -from algokit_utils._legacy_v2.application_specification import ( - ApplicationSpecification, - AppSpecStateDict, - DefaultArgumentDict, - MethodConfigDict, - MethodHints, -) -from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue -from algokit_utils.models.application import ( - ABIArgumentType, - ABITypeAlias, - Arc56Contract, - Arc56ContractState, - Arc56Method, - CallConfig, - DefaultValue, - Method, - MethodActions, - MethodArg, - MethodReturns, - OnCompleteAction, - StorageKey, - StructField, - StructName, -) - -T = TypeVar("T", bound=ABIValue | bytes | ABIStruct | None) - - -def get_arc56_method(method_name_or_signature: str, app_spec: Arc56Contract) -> Arc56Method: - if "(" not in method_name_or_signature: - # Filter by method name - methods = [m for m in app_spec.methods if m.name == method_name_or_signature] - if not methods: - raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") - if len(methods) > 1: - signatures = [AlgorandABIMethod.undictify(m.__dict__).get_signature() for m in app_spec.methods] - raise ValueError( - f"Received a call to method {method_name_or_signature} in contract {app_spec.name}, " - f"but this resolved to multiple methods; please pass in an ABI signature instead: " - f"{', '.join(signatures)}" - ) - method = methods[0] - else: - # Find by signature - method = None - for m in app_spec.methods: - abi_method = AlgorandABIMethod.undictify(m.to_dict()) - if abi_method.get_signature() == method_name_or_signature: - method = m - break - - if method is None: - raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") - - return Arc56Method(method) - - -def get_arc56_return_value( - return_value: ABIResult | None, - method: Method | AlgorandABIMethod, - structs: dict[str, list[StructField]], -) -> ABIValue | ABIStruct | None: - """Checks for decode errors on the return value and maps it to the specified type. - - Args: - return_value: The smart contract response - method: The method that was called - structs: The struct fields from the app spec - - Returns: - The smart contract response with an updated return value - - Raises: - ValueError: If there is a decode error - """ - - # Get method returns info - if isinstance(method, AlgorandABIMethod): - type_str = method.returns.type - struct = None # AlgorandABIMethod doesn't have struct info - else: - type_str = method.returns.type - struct = method.returns.struct - - # Handle void/undefined returns - if type_str == "void" or return_value is None: - return None - - # Handle decode errors - if return_value.decode_error: - raise ValueError(return_value.decode_error) - - # Get raw return value - raw_value = return_value.raw_value - - # Handle AVM types - if type_str == "AVMBytes": - return raw_value - if type_str == "AVMString" and raw_value: - return raw_value.decode("utf-8") - if type_str == "AVMUint64" and raw_value: - return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return] - - # Handle structs - if struct and struct in structs: - return_tuple = return_value.return_value - return get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs) - - # Return as-is - return return_value.return_value # type: ignore[no-any-return] - - -def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: ANN401, PLR0911 - if isinstance(value, (bytes | bytearray)): - return value - if type_str == "AVMUint64": - return ABIType.from_string("uint64").encode(value) - if type_str in ("AVMBytes", "AVMString"): - if isinstance(value, str): - return value.encode("utf-8") - if not isinstance(value, (bytes | bytearray)): - raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}") - return value - if type_str in structs: - tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs) - if isinstance(value, (list | tuple)): - return tuple_type.encode(value) # type: ignore[arg-type] - else: - tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs) - return tuple_type.encode(tuple_values) - else: - abi_type = ABIType.from_string(type_str) - return abi_type.encode(value) - - -def get_abi_decoded_value( - value: bytes | int | str, type_str: str | ABITypeAlias | ABIArgumentType, structs: dict[str, list[StructField]] -) -> ABIValue: - type_value = str(type_str) - - if type_value == "AVMBytes" or not isinstance(value, bytes): - return value - if type_value == "AVMString": - return value.decode("utf-8") - if type_value == "AVMUint64": - return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return] - if type_value in structs: - tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs) - decoded_tuple = tuple_type.decode(value) - return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs) - return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return] - - -def get_abi_tuple_from_abi_struct( - struct_value: dict[str, Any], - struct_fields: list[StructField], - structs: dict[str, list[StructField]], -) -> list[Any]: - result = [] - for field in struct_fields: - key = field.name - if key not in struct_value: - raise ValueError(f"Missing value for field '{key}'") - value = struct_value[key] - field_type = field.type - if isinstance(field_type, str): - if field_type in structs: - value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs) - elif isinstance(field_type, list): - value = get_abi_tuple_from_abi_struct(value, field_type, structs) - result.append(value) - return result - - -def get_abi_tuple_type_from_abi_struct_definition( - struct_def: list[StructField], structs: dict[str, list[StructField]] -) -> TupleType: - types = [] - for field in struct_def: - field_type = field.type - if isinstance(field_type, str): - if field_type in structs: - types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs)) - else: - types.append(ABIType.from_string(field_type)) # type: ignore[arg-type] - elif isinstance(field_type, list): - types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs)) - else: - raise ValueError(f"Invalid field type: {field_type}") - return TupleType(types) - - -def get_abi_struct_from_abi_tuple( - decoded_tuple: Any, # noqa: ANN401 - struct_fields: list[StructField], - structs: dict[str, list[StructField]], -) -> dict[str, Any]: - result = {} - for i, field in enumerate(struct_fields): - key = field.name - field_type = field.type - value = decoded_tuple[i] - if isinstance(field_type, str): - if field_type in structs: - value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs) - elif isinstance(field_type, list): - value = get_abi_struct_from_abi_tuple(value, field_type, structs) - result[key] = value - return result - - -def arc32_to_arc56(app_spec: ApplicationSpecification) -> Arc56Contract: # noqa: C901 - """ - Convert ARC-32 application specification to ARC-56 contract format. - - Args: - app_spec: ARC-32 application specification - - Returns: - ARC-56 contract specification - """ - - def convert_structs() -> dict[StructName, list[StructField]]: - structs: dict[StructName, list[StructField]] = {} - for hint in app_spec.hints.values(): - if not hint.structs: - continue - for struct in hint.structs.values(): - fields = [ - StructField( - name=name, - type=type_, - ) - for name, type_ in struct["elements"] - ] - structs[struct["name"]] = fields - return structs - - def get_hint(method: AlgorandABIMethod) -> MethodHints | None: - sig = method.get_signature() - return app_spec.hints.get(sig) - - def get_default_value( - type: str | ABIType, # noqa: A002 TODO: revisit - default_arg: DefaultArgumentDict, - ) -> DefaultValue | None: - if not default_arg or default_arg["source"] == "abi-method": - return None - - source_map = { - "constant": "literal", - "global-state": "global", - "local-state": "local", - } - - data = default_arg["data"] - if isinstance(data, str): - data = base64.b64encode(data.encode()).decode() - elif isinstance(data, bytes): - data = base64.b64encode(data).decode() - else: - data = str(data) - - return DefaultValue( - data=data, - type="AVMString" if type == "string" else str(type), - source=source_map.get(default_arg["source"], "literal"), # type: ignore[arg-type] - ) - - def convert_method(method: AlgorandABIMethod) -> Method: - hint = get_hint(method) - - args: list[MethodArg] = [] - for arg in method.args: - if not arg.name: - continue - struct_name = None - if hint and hint.structs and arg.name in hint.structs: - struct_name = hint.structs[arg.name].get("name") - - default_value = None - if hint and hint.default_arguments and arg.name in hint.default_arguments: - default_value = get_default_value(str(arg.type), hint.default_arguments[arg.name]) - - method_arg = MethodArg( - type=arg.type, # type: ignore[arg-type] - struct=struct_name, - name=arg.name, - desc=arg.desc, - default_value=default_value, - ) - args.append(method_arg) - - method_returns = MethodReturns( - type=str(method.returns.type), - struct=hint.structs.get("output", {}).get("name") if hint and hint.structs else None, # type: ignore[call-overload] - desc=method.returns.desc, - ) - - method_actions = MethodActions( - create=convert_actions(hint.call_config, "CREATE") if hint and hint.call_config else [], # type: ignore # noqa: PGH003 - call=convert_actions(hint.call_config, "CALL") if hint and hint.call_config else [], - ) - - return Method( - name=method.name, - desc=method.desc, - args=args, - returns=method_returns, - actions=method_actions, - readonly=hint.read_only if hint else False, - events=[], - recommendations=None, - ) - - def convert_storage_keys(schema_dict: AppSpecStateDict) -> dict[str, StorageKey]: - return { - name: StorageKey( - desc=spec.get("descr"), - key_type=spec["type"], - value_type="AVMUint64" if spec["type"] == "uint64" else "AVMBytes", - key=base64.b64encode(spec["key"].encode()).decode(), - ) - for name, spec in schema_dict.get("declared", {}).items() - } - - def convert_actions( - call_config: CallConfig | MethodConfigDict, action_type: Literal["CREATE", "CALL"] - ) -> list[OnCompleteAction | Literal["NoOp", "OptIn", "DeleteApplication"]]: - """ - Converts method configuration into a list of on-complete action literals. - - Args: - call_config (CallConfig | MethodConfigDict): Configuration dictionary or CallConfig object for method - actions. - action_type (Literal["CREATE", "CALL"]): The type of action to convert. - - Returns: - List[OnCompleteAction]: A list of on-complete action literals. - """ - config_action_map = { - "no_op": "NoOp", - "opt_in": "OptIn", - "close_out": "CloseOut", - "clear_state": "ClearState", - "update_application": "UpdateApplication", - "delete_application": "DeleteApplication", - } - - def get_action_value(key: str) -> str | None: - if isinstance(call_config, dict): - config_value = call_config.get(key) # type: ignore[call-overload] - # Handle legacy CallConfig enum - return config_value.name if hasattr(config_value, "name") else config_value # type: ignore[no-any-return] - # Handle new CallConfig dataclass - return getattr(call_config, key, None) - - return [action for key, action in config_action_map.items() if get_action_value(key) in ("ALL", action_type)] # type: ignore # noqa: PGH003 - - # Convert structs - structs = convert_structs() - - # Get schema information from app_spec - global_schema = app_spec.schema.get("global", {}) - local_schema = app_spec.schema.get("local", {}) - - state = Arc56ContractState( - schemas={ - "global": { - "ints": int(app_spec.global_state_schema.num_uints) if app_spec.global_state_schema.num_uints else 0, - "bytes": int(app_spec.global_state_schema.num_byte_slices) - if app_spec.global_state_schema.num_byte_slices - else 0, - }, - "local": { - "ints": int(app_spec.local_state_schema.num_uints) if app_spec.local_state_schema.num_uints else 0, - "bytes": int(app_spec.local_state_schema.num_byte_slices) - if app_spec.local_state_schema.num_byte_slices - else 0, - }, - }, - keys={ - "global": convert_storage_keys(global_schema), - "local": convert_storage_keys(local_schema), - "box": {}, - }, - maps={ - "global": {}, - "local": {}, - "box": {}, - }, - ) - - contract_source = { - "approval": app_spec.approval_program, - "clear": app_spec.clear_program, - } - - bare_actions = { - "create": convert_actions(app_spec.bare_call_config, "CREATE"), - "call": convert_actions(app_spec.bare_call_config, "CALL"), - } - - return Arc56Contract( - arcs=[], - name=app_spec.contract.name, - desc=app_spec.contract.desc, - structs=structs, - methods=[convert_method(m) for m in app_spec.contract.methods], - state=state, - source=contract_source, - bare_actions=bare_actions, - byte_code=None, - compiler_info=None, - events=None, - networks=None, - scratch_variables=None, - source_info=None, - template_variables=None, - ) diff --git a/src/algokit_utils/asset.py b/src/algokit_utils/asset.py index 4d9c8522..c7087f0c 100644 --- a/src/algokit_utils/asset.py +++ b/src/algokit_utils/asset.py @@ -1 +1,32 @@ -from algokit_utils._legacy_v2.asset import * # noqa: F403 +import warnings + +warnings.warn( + """The legacy v2 asset module is deprecated and will be removed in a future version. + +Replacements for opt_in/opt_out functionality: + +1. Using TransactionComposer: + composer.add_asset_opt_in(AssetOptInParams( + sender=account.address, + asset_id=123 + )) + composer.add_asset_opt_out(AssetOptOutParams( + sender=account.address, + asset_id=123, + creator=creator_address + )) + +2. Using AlgorandClient: + client.asset.opt_in(AssetOptInParams(...)) + client.asset.opt_out(AssetOptOutParams(...)) + +3. For bulk operations: + client.asset.bulk_opt_in(account, [asset_ids]) + client.asset.bulk_opt_out(account, [asset_ids]) + +Refer to AssetManager class from algokit_utils for more functionality.""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.asset import * # noqa: F403, E402 diff --git a/src/algokit_utils/assets/__init__.py b/src/algokit_utils/assets/__init__.py index e69de29b..ec7116dd 100644 --- a/src/algokit_utils/assets/__init__.py +++ b/src/algokit_utils/assets/__init__.py @@ -0,0 +1 @@ +from algokit_utils.assets.asset_manager import * # noqa: F403 diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 18184715..bfbe79b7 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -13,6 +13,8 @@ TransactionComposer, ) +__all__ = ["AccountAssetInformation", "AssetInformation", "AssetManager", "BulkAssetOptInOutResult"] + @dataclass(kw_only=True, frozen=True) class AccountAssetInformation: diff --git a/src/algokit_utils/clients/__init__.py b/src/algokit_utils/clients/__init__.py index e69de29b..2224016e 100644 --- a/src/algokit_utils/clients/__init__.py +++ b/src/algokit_utils/clients/__init__.py @@ -0,0 +1,3 @@ +from algokit_utils.clients.algorand_client import * # noqa: F403 +from algokit_utils.clients.client_manager import * # noqa: F403 +from algokit_utils.clients.dispenser_api_client import * # noqa: F403 diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index eb4ef73e..24ca220e 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -1,10 +1,9 @@ import copy import time -from typing import Any -from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner -from algosdk.transaction import SuggestedParams, wait_for_confirmation -from typing_extensions import Self +import typing_extensions +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.transaction import SuggestedParams from algokit_utils.accounts.account_manager import AccountManager from algokit_utils.applications.app_deployer import AppDeployer @@ -13,16 +12,6 @@ from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager from algokit_utils.models.network import AlgoClientConfigs from algokit_utils.transactions.transaction_composer import ( - AppCallParams, - AppMethodCallParams, - AssetConfigParams, - AssetCreateParams, - AssetDestroyParams, - AssetFreezeParams, - AssetOptInParams, - AssetTransferParams, - OnlineKeyRegistrationParams, - PaymentParams, TransactionComposer, ) from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator @@ -30,16 +19,6 @@ __all__ = [ "AlgorandClient", - "AppCallParams", - "AppMethodCallParams", - "AssetConfigParams", - "AssetCreateParams", - "AssetDestroyParams", - "AssetFreezeParams", - "AssetOptInParams", - "AssetTransferParams", - "OnlineKeyRegistrationParams", - "PaymentParams", ] @@ -70,7 +49,7 @@ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): self._default_validity_window: int = 10 - def set_default_validity_window(self, validity_window: int) -> Self: + def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self: """ Sets the default validity window for transactions. @@ -80,7 +59,7 @@ def set_default_validity_window(self, validity_window: int) -> Self: self._default_validity_window = validity_window return self - def set_default_signer(self, signer: TransactionSigner) -> Self: + def set_default_signer(self, signer: TransactionSigner) -> typing_extensions.Self: """ Sets the default signer to use if no other signer is specified. @@ -90,7 +69,7 @@ def set_default_signer(self, signer: TransactionSigner) -> Self: self._account_manager.set_default_signer(signer) return self - def set_signer(self, sender: str, signer: TransactionSigner) -> Self: + def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extensions.Self: """ Tracks the given account for later signing. @@ -101,7 +80,9 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: self._account_manager.set_signer(sender, signer) return self - def set_suggested_params(self, suggested_params: SuggestedParams, until: float | None = None) -> Self: + def set_suggested_params( + self, suggested_params: SuggestedParams, until: float | None = None + ) -> typing_extensions.Self: """ Sets a cache value to use for suggested params. @@ -113,7 +94,7 @@ def set_suggested_params(self, suggested_params: SuggestedParams, until: float | self._cached_suggested_params_expiry = until or time.time() + self._cached_suggested_params_timeout return self - def set_suggested_params_timeout(self, timeout: int) -> Self: + def set_suggested_params_timeout(self, timeout: int) -> typing_extensions.Self: """ Sets the timeout for caching suggested params. @@ -178,12 +159,6 @@ def create_transaction(self) -> AlgorandClientTransactionCreator: """Methods for building transactions""" return self._transaction_creator - def _unwrap_single_send_result(self, results: AtomicTransactionResponse) -> dict[str, Any]: - return { - "confirmation": wait_for_confirmation(self._client_manager.algod, results.tx_ids[0]), - "tx_id": results.tx_ids[0], - } - @staticmethod def default_local_net() -> "AlgorandClient": """ diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 3f41f668..978cb0c8 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -13,12 +13,19 @@ # from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams from algokit_utils._legacy_v2.application_specification import ApplicationSpecification from algokit_utils.applications.app_client import AppClient, AppClientParams +from algokit_utils.applications.app_deployer import AppLookup from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams -from algokit_utils.applications.app_manager import TealTemplateParams +from algokit_utils.applications.app_spec.arc56 import Arc56Contract from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient -from algokit_utils.models.application import Arc56Contract from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs -from algokit_utils.protocols.application import AlgorandClientProtocol +from algokit_utils.models.state import TealTemplateParams +from algokit_utils.protocols.client import AlgorandClientProtocol + +__all__ = [ + "AlgoSdkClients", + "ClientManager", + "NetworkDetail", +] class AlgoSdkClients: @@ -42,10 +49,6 @@ class NetworkDetail: genesis_hash: str -def genesis_id_is_localnet(genesis_id: str) -> bool: - return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] - - def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: server = os.getenv(f"{environment_prefix}_SERVER") if server is None: @@ -202,6 +205,31 @@ def get_app_client_by_network( algorand=self._algorand, ) + def get_app_client_by_creator_and_name( + self, + creator_address: str, + app_name: str, + app_spec: Arc56Contract | ApplicationSpecification | str, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient.from_creator_and_name( + creator_address=creator_address, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + app_spec=app_spec, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + @staticmethod def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment @@ -243,7 +271,7 @@ def get_indexer_client_from_environment() -> IndexerClient: @staticmethod def genesis_id_is_local_net(genesis_id: str) -> bool: - return genesis_id_is_localnet(genesis_id) + return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] @staticmethod def get_config_from_environment_or_localnet() -> AlgoClientConfigs: @@ -268,7 +296,9 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: # Include KMD config only for local networks (not mainnet/testnet) kmd_config = ( - ClientManager.get_kmd_config_from_environment() + AlgoClientConfig( + server=algod_config.server, token=algod_config.token, port=os.getenv("KMD_PORT", "4002") + ) if not any(net in algod_server.lower() for net in ["mainnet", "testnet"]) else None ) diff --git a/src/algokit_utils/clients/dispenser_api_client.py b/src/algokit_utils/clients/dispenser_api_client.py index b8a3ef78..e471989e 100644 --- a/src/algokit_utils/clients/dispenser_api_client.py +++ b/src/algokit_utils/clients/dispenser_api_client.py @@ -7,6 +7,19 @@ from algokit_utils.config import config +__all__ = [ + "DISPENSER_ACCESS_TOKEN_KEY", + "DISPENSER_ASSETS", + "DISPENSER_REQUEST_TIMEOUT", + "DispenserApiConfig", + "DispenserAsset", + "DispenserAssetName", + "DispenserFundResponse", + "DispenserLimitResponse", + "TestNetDispenserApiClient", +] + + logger = config.logger diff --git a/src/algokit_utils/common.py b/src/algokit_utils/common.py index 45c54a87..c0274574 100644 --- a/src/algokit_utils/common.py +++ b/src/algokit_utils/common.py @@ -1 +1,10 @@ -from algokit_utils._legacy_v2.common import * # noqa: F403 +import warnings + +warnings.warn( + "The legacy v2 common module is deprecated and will be removed in a future version. " + "Refer to `CompiledTeal` class from `algokit_utils` instead.", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.common import * # noqa: F403, E402 diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index eb788910..6fd99446 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any -logger = logging.getLogger(__name__) - # Environment variable to override the project root ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT") ALGOKIT_CONFIG_FILENAME = ".algokit.toml" diff --git a/src/algokit_utils/deploy.py b/src/algokit_utils/deploy.py index 7543c6c1..9991b3ab 100644 --- a/src/algokit_utils/deploy.py +++ b/src/algokit_utils/deploy.py @@ -1 +1,10 @@ -from algokit_utils._legacy_v2.deploy import * # noqa: F403 +import warnings + +warnings.warn( + "The legacy v2 deploy module is deprecated and will be removed in a future version. " + "Refer to `AppFactory` and `AppDeployer` abstractions from `algokit_utils` module instead.", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.deploy import * # noqa: F403, E402 diff --git a/src/algokit_utils/dispenser_api.py b/src/algokit_utils/dispenser_api.py index 1dc9e175..a338badc 100644 --- a/src/algokit_utils/dispenser_api.py +++ b/src/algokit_utils/dispenser_api.py @@ -1 +1,10 @@ -from algokit_utils.clients.dispenser_api_client import * # noqa: F403 +import warnings + +warnings.warn( + "The legacy v2 dispenser api module is deprecated and will be removed in a future version. " + "Import from 'algokit_utils.clients.dispenser_api_client' instead.", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils.clients.dispenser_api_client import * # noqa: F403, E402 diff --git a/src/algokit_utils/errors/__init__.py b/src/algokit_utils/errors/__init__.py index e69de29b..1575d19e 100644 --- a/src/algokit_utils/errors/__init__.py +++ b/src/algokit_utils/errors/__init__.py @@ -0,0 +1 @@ +from algokit_utils.errors.logic_error import * # noqa: F403 diff --git a/src/algokit_utils/errors/logic_error.py b/src/algokit_utils/errors/logic_error.py index 24fb40a0..755b89e4 100644 --- a/src/algokit_utils/errors/logic_error.py +++ b/src/algokit_utils/errors/logic_error.py @@ -1,5 +1,4 @@ import base64 -import dataclasses import re from collections.abc import Callable from copy import copy @@ -9,20 +8,21 @@ SimulateAtomicTransactionResponse, ) +from algokit_utils.models.simulate import SimulationTrace + if TYPE_CHECKING: from algosdk.source_map import SourceMap as AlgoSourceMap - __all__ = [ "LogicError", + "LogicErrorData", "parse_logic_error", ] + LOGIC_ERROR = ( ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" ) -DEFAULT_BLAST_RADIUS = 5 - class LogicErrorData(TypedDict): transaction_id: str @@ -30,14 +30,6 @@ class LogicErrorData(TypedDict): pc: int -@dataclasses.dataclass -class SimulationTrace: - app_budget_added: int | None - app_budget_consumed: int | None - failure_message: str | None - exec_trace: dict[str, object] - - def parse_logic_error( error_str: str, ) -> LogicErrorData | None: diff --git a/src/algokit_utils/logic_error.py b/src/algokit_utils/logic_error.py index 2b750b56..462895f7 100644 --- a/src/algokit_utils/logic_error.py +++ b/src/algokit_utils/logic_error.py @@ -1 +1,10 @@ -from algokit_utils._legacy_v2.logic_error import * # noqa: F403 +import warnings + +warnings.warn( + "The legacy v2 logic error module is deprecated and will be removed in a future version. " + "Use 'from algokit_utils.errors import LogicError' instead.", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils.errors.logic_error import * # noqa: F403, E402 diff --git a/src/algokit_utils/models/__init__.py b/src/algokit_utils/models/__init__.py index baf4664d..52d4ce8f 100644 --- a/src/algokit_utils/models/__init__.py +++ b/src/algokit_utils/models/__init__.py @@ -1,4 +1,7 @@ from algokit_utils._legacy_v2.models import * # noqa: F403 - -from .abi import * # noqa: F403 -from .account import * # noqa: F403 +from algokit_utils.models.account import * # noqa: F403 +from algokit_utils.models.amount import * # noqa: F403 +from algokit_utils.models.application import * # noqa: F403 +from algokit_utils.models.simulate import * # noqa: F403 +from algokit_utils.models.state import * # noqa: F403 +from algokit_utils.models.transaction import * # noqa: F403 diff --git a/src/algokit_utils/models/abi.py b/src/algokit_utils/models/abi.py deleted file mode 100644 index 4e837274..00000000 --- a/src/algokit_utils/models/abi.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import TypeAlias - -import algosdk - -from algokit_utils.models.application import StructField - -ABIPrimitiveValue = bool | int | str | bytes | bytearray - -# NOTE: This is present in js-algorand-sdk, but sadly not in untyped py-algorand-sdk -ABIValue: TypeAlias = ABIPrimitiveValue | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] -ABIStruct: TypeAlias = dict[str, list[StructField]] - - -ABIType: TypeAlias = algosdk.abi.ABIType diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index 8b5da485..d90a426b 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -5,6 +5,9 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner from algosdk.transaction import Multisig, MultisigTransaction +__all__ = ["DISPENSER_ACCOUNT_NAME", "Account", "MultiSigAccount", "MultisigMetadata"] + + DISPENSER_ACCOUNT_NAME = "DISPENSER" diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index adb7ffae..1d063060 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -5,6 +5,8 @@ import algosdk from typing_extensions import Self +__all__ = ["AlgoAmount"] + class AlgoAmount: def __init__(self, amount: dict[str, int | Decimal]): diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py index 6ab5d0ff..b2950f12 100644 --- a/src/algokit_utils/models/application.py +++ b/src/algokit_utils/models/application.py @@ -1,433 +1,19 @@ -import json -from dataclasses import asdict, dataclass, field, is_dataclass -from typing import Any, Literal, TypeAlias +from dataclasses import dataclass +from typing import TYPE_CHECKING import algosdk +from algosdk.source_map import SourceMap -UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" -"""The name of the TEAL template variable for deploy-time immutability control.""" +if TYPE_CHECKING: + pass -DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" -"""The name of the TEAL template variable for deploy-time permanence control.""" - - -# ===== ARCs ===== - -# Define type aliases -ABITypeAlias: TypeAlias = str -ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType -StructName: TypeAlias = str -AVMBytes = Literal["AVMBytes"] -AVMString = Literal["AVMString"] -AVMUint64 = Literal["AVMUint64"] -AVMType = AVMBytes | AVMString | AVMUint64 -OnCompleteAction = Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"] -DefaultValueSource = Literal["box", "global", "local", "literal", "method"] - - -def convert_key_to_snake_case(name: str) -> str: - import re - - return re.sub(r"(? Any: # noqa: ANN401 - if isinstance(obj, dict): - return {convert_key_to_snake_case(k): convert_keys_to_snake_case(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [convert_keys_to_snake_case(item) for item in obj] - return obj - - -class SerializableBaseClass: - """ - A base class that provides a generic `dictify` method to convert dataclass instances - into dictionaries recursively. - """ - - def to_dict(self) -> dict[str, Any]: - def serialize(obj: Any) -> dict[str, Any] | list[Any] | Any: # noqa: ANN401 - if is_dataclass(obj) and not isinstance(obj, type): - return {k: serialize(v) for k, v in asdict(obj).items()} - elif isinstance(obj, algosdk.abi.ABIType): - return str(obj) - elif isinstance(obj, list): - return [serialize(item) for item in obj] - elif isinstance(obj, dict): - return {k: serialize(v) for k, v in obj.items()} - else: - return obj - - result = serialize(self) - if not isinstance(result, dict): - raise TypeError("Serialized object is not a dictionary.") - return result - - -@dataclass -class CallConfig: - no_op: str | None = None - opt_in: str | None = None - close_out: str | None = None - clear_state: str | None = None - update_application: str | None = None - delete_application: str | None = None - - -@dataclass(kw_only=True) -class StructField: - name: str - type: ABITypeAlias | StructName | list["StructField"] - - -@dataclass(kw_only=True) -class StorageKey: - desc: str | None - key_type: ABITypeAlias | AVMType | StructName - value_type: ABITypeAlias | AVMType | StructName - key: str # base64 encoded bytes - - -@dataclass(kw_only=True) -class StorageMap: - desc: str | None - key_type: ABITypeAlias | AVMType | StructName - value_type: ABITypeAlias | AVMType | StructName - prefix: str | None # base64-encoded prefix - - -@dataclass(kw_only=True) -class DefaultValue: - data: str - type: ABITypeAlias | AVMType | None = None - source: DefaultValueSource - - -@dataclass(kw_only=True) -class MethodArg: - type: ABITypeAlias - struct: StructName | None = None - name: str | None = None - desc: str | None = None - default_value: DefaultValue | None = None - - -@dataclass -class MethodReturns: - type: ABITypeAlias - struct: StructName | None = None - desc: str | None = None - - -@dataclass(kw_only=True) -class MethodActions: - create: list[Literal["NoOp", "OptIn", "DeleteApplication"]] - call: list[Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"]] - - -@dataclass(kw_only=True) -class BoxRecommendation: - app: int | None = None - key: str = "" - read_bytes: int = 0 - write_bytes: int = 0 - - -@dataclass(kw_only=True) -class Recommendations: - inner_transaction_count: int | None = None - boxes: list[BoxRecommendation] | None = None - accounts: list[str] | None = None - apps: list[int] | None = None - assets: list[int] | None = None - - -@dataclass(kw_only=True) -class Method(SerializableBaseClass): - name: str - desc: str | None = None - args: list[MethodArg] = field(default_factory=list) - returns: MethodReturns = field(default_factory=lambda: MethodReturns(type="void")) - actions: MethodActions = field(default_factory=lambda: MethodActions(create=[], call=[])) - readonly: bool | None = False - events: list["Event"] | None = None - recommendations: Recommendations | None = None - - -@dataclass(kw_only=True) -class EventArg: - type: ABITypeAlias - name: str | None = None - desc: str | None = None - struct: StructName | None = None - - -@dataclass(kw_only=True) -class Event: - name: str - desc: str | None = None - args: list[EventArg] = field(default_factory=list) - - -@dataclass(kw_only=True) -class CompilerVersion: - major: int - minor: int - patch: int - commit_hash: str | None = None - - -@dataclass(kw_only=True) -class CompilerInfo: - compiler: Literal["algod", "puya"] - compiler_version: CompilerVersion - - -@dataclass -class SourceInfoDetail: - pc: list[int] - error_message: str | None = None - teal: int | None = None - source: str | None = None - - -@dataclass(kw_only=True) -class ProgramSourceInfo: - source_info: list[SourceInfoDetail] - pc_offset_method: Literal["none", "cblocks"] - - @staticmethod - def from_json(source_info: str | dict) -> "ProgramSourceInfo": - if "source_info" not in source_info: - raise ValueError("source_info is required") - source_dict: dict = json.loads(source_info) if isinstance(source_info, str) else source_info - parsed_source_dict = [SourceInfoDetail(**detail) for detail in source_dict["source_info"]] - return ProgramSourceInfo(source_info=parsed_source_dict, pc_offset_method=source_dict["pc_offset_method"]) - - -@dataclass(kw_only=True) -class Arc56ContractState: - keys: dict[str, dict[str, StorageKey]] - maps: dict[str, dict[str, StorageMap]] - schemas: dict[str, dict[str, int]] - - -@dataclass(kw_only=True) -class Arc56MethodArg: - """Represents an ARC-56 method argument with ABI type conversion.""" - - name: str | None = None - desc: str | None = None - struct: StructName | None = None - default_value: DefaultValue | None = None - type: ABIArgumentType - - @classmethod - def from_method_arg(cls, arg: MethodArg, converted_type: ABIArgumentType) -> "Arc56MethodArg": - """Create an Arc56MethodArg from a MethodArg with converted type.""" - return cls( - name=arg.name, - desc=arg.desc, - struct=arg.struct, - default_value=arg.default_value, - type=converted_type, - ) - - -@dataclass(kw_only=True) -class Arc56MethodReturnType: - """Represents an ARC-56 method return type with ABI type conversion.""" - - type: algosdk.abi.ABIType | Literal["void"] # Can be 'void' or ABIType - struct: StructName | None = None - desc: str | None = None - - -class Arc56Method(SerializableBaseClass, algosdk.abi.Method): - def __init__(self, method: Method) -> None: - # First, create the parent class with original arguments - super().__init__( - name=method.name, - args=method.args, # type: ignore[arg-type] - returns=algosdk.abi.Returns(arg_type=method.returns.type, desc=method.returns.desc), - desc=method.desc, - ) - self.method = method - - # Store our custom Arc56MethodArg list separately - - self._arc56_args = [ - Arc56MethodArg.from_method_arg( - arg, - algosdk.abi.ABIType.from_string(arg.type) - if not self._is_transaction_or_reference_type(arg.type) and isinstance(arg.type, str) - else arg.type, # type: ignore[arg-type] - ) - for arg in method.args - ] - - # Convert returns similar to TypeScript implementation, including struct support - converted_return_type: Literal["void"] | algosdk.abi.ABIType - if method.returns.type == "void": - converted_return_type = "void" - else: - converted_return_type = algosdk.abi.ABIType.from_string(str(method.returns.type)) - - self._arc56_returns = Arc56MethodReturnType( - type=converted_return_type, - struct=method.returns.struct, - desc=method.returns.desc, - ) - - def _is_transaction_or_reference_type(self, type_str: str) -> bool: - return type_str in [ - algosdk.constants.ASSETCONFIG_TXN, - algosdk.constants.PAYMENT_TXN, - algosdk.constants.KEYREG_TXN, - algosdk.constants.ASSETFREEZE_TXN, - algosdk.constants.ASSETTRANSFER_TXN, - algosdk.constants.APPCALL_TXN, - algosdk.constants.STATEPROOF_TXN, - algosdk.abi.ABIReferenceType.APPLICATION, - algosdk.abi.ABIReferenceType.ASSET, - algosdk.abi.ABIReferenceType.ACCOUNT, - ] - - @property - def arc56_args(self) -> list[Arc56MethodArg]: - """Get the ARC-56 specific argument representations.""" - return self._arc56_args - - @property - def arc56_returns(self) -> Arc56MethodReturnType: - """Get the ARC-56 specific returns type, including struct information.""" - return self._arc56_returns - - -@dataclass(kw_only=True) -class Arc56Contract(SerializableBaseClass): - arcs: list[int] - name: str - desc: str | None = None - networks: dict[str, dict[str, int]] | None = None - structs: dict[StructName, list[StructField]] = field(default_factory=dict) - methods: list[Method] = field(default_factory=list) - state: Arc56ContractState - bare_actions: dict[str, list[OnCompleteAction]] = field(default_factory=dict) - source_info: dict[str, ProgramSourceInfo] | None = None - source: dict[str, str] | None = None - byte_code: dict[str, str] | None = None - compiler_info: CompilerInfo | None = None - events: list[Event] | None = None - template_variables: dict[str, dict[str, ABITypeAlias | AVMType | StructName | str]] | None = None - scratch_variables: dict[str, dict[str, int | ABITypeAlias | AVMType | StructName]] | None = None - - @staticmethod - def from_json(application_spec: str | dict) -> "Arc56Contract": - """Convert a JSON dictionary into an Arc56Contract instance. - - Args: - json_data (dict): The JSON data representing an Arc56Contract - - Returns: - Arc56Contract: The constructed Arc56Contract instance - """ - # Convert networks if present - json_data = json.loads(application_spec) if isinstance(application_spec, str) else application_spec - json_data = convert_keys_to_snake_case(json_data) - networks = json_data.get("networks") - - # Convert structs - structs = { - name: [StructField(**field) if isinstance(field, dict) else field for field in struct_fields] - for name, struct_fields in json_data.get("structs", {}).items() - } - - # Convert methods - methods = [] - for method_data in json_data.get("methods", []): - # Convert method args - args = [MethodArg(**arg) for arg in method_data.get("args", [])] - - # Convert method returns - returns_data = method_data.get("returns", {"type": "void"}) - returns = MethodReturns(**returns_data) - - # Convert method actions - actions_data = method_data.get("actions", {"create": [], "call": []}) - actions = MethodActions(**actions_data) - - # Convert events if present - events = None - if "events" in method_data: - events = [Event(**event) for event in method_data["events"]] - - # Convert recommendations if present - recommendations = None - if "recommendations" in method_data: - recommendations = Recommendations(**method_data["recommendations"]) - - methods.append( - Method( - name=method_data["name"], - desc=method_data.get("desc"), - args=args, - returns=returns, - actions=actions, - readonly=method_data.get("readonly", False), - events=events, - recommendations=recommendations, - ) - ) - - # Convert state - state_data = json_data["state"] - state = Arc56ContractState( - keys={ - category: {name: StorageKey(**key_data) for name, key_data in keys.items()} - for category, keys in state_data.get("keys", {}).items() - }, - maps={ - category: {name: StorageMap(**map_data) for name, map_data in maps.items()} - for category, maps in state_data.get("maps", {}).items() - }, - schemas=state_data.get("schema", {}), - ) - - # Convert compiler info if present - compiler_info = None - if "compiler_info" in json_data: - compiler_version = CompilerVersion(**json_data["compiler_info"]["compiler_version"]) - compiler_info = CompilerInfo( - compiler=json_data["compiler_info"]["compiler"], compiler_version=compiler_version - ) - - # Convert events if present - events = None - if "events" in json_data: - events = [Event(**event) for event in json_data["events"]] - - source_info = {} - if "source_info" in json_data: - source_info = {key: ProgramSourceInfo.from_json(val) for key, val in json_data["source_info"].items()} - - return Arc56Contract( - arcs=json_data.get("arcs", []), - name=json_data["name"], - desc=json_data.get("desc"), - networks=networks, - structs=structs, - methods=methods, - state=state, - bare_actions=json_data.get("bare_actions", {}), - source_info=source_info, - source=json_data.get("source"), - byte_code=json_data.get("byte_code"), - compiler_info=compiler_info, - events=events, - template_variables=json_data.get("template_variables"), - scratch_variables=json_data.get("scratch_variables"), - ) +__all__ = [ + "AppCompilationResult", + "AppInformation", + "AppSourceMaps", + "AppState", + "CompiledTeal", +] @dataclass(kw_only=True, frozen=True) @@ -467,3 +53,9 @@ class CompiledTeal: class AppCompilationResult: compiled_approval: CompiledTeal compiled_clear: CompiledTeal + + +@dataclass(kw_only=True, frozen=True) +class AppSourceMaps: + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py index 8ee897e2..8cf07f3f 100644 --- a/src/algokit_utils/models/network.py +++ b/src/algokit_utils/models/network.py @@ -1,5 +1,10 @@ import dataclasses +__all__ = [ + "AlgoClientConfig", + "AlgoClientConfigs", +] + @dataclasses.dataclass class AlgoClientConfig: diff --git a/src/algokit_utils/models/simulate.py b/src/algokit_utils/models/simulate.py new file mode 100644 index 00000000..bd200495 --- /dev/null +++ b/src/algokit_utils/models/simulate.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +__all__ = ["SimulationTrace"] + + +@dataclass +class SimulationTrace: + app_budget_added: int | None + app_budget_consumed: int | None + failure_message: str | None + exec_trace: dict[str, object] diff --git a/src/algokit_utils/models/state.py b/src/algokit_utils/models/state.py new file mode 100644 index 00000000..f5d7804e --- /dev/null +++ b/src/algokit_utils/models/state.py @@ -0,0 +1,59 @@ +import base64 +from collections.abc import Mapping +from dataclasses import dataclass +from enum import IntEnum +from typing import TypeAlias + +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.box_reference import BoxReference as AlgosdkBoxReference + +__all__ = [ + "BoxIdentifier", + "BoxName", + "BoxReference", + "BoxValue", + "DataTypeFlag", + "TealTemplateParams", +] + + +@dataclass(kw_only=True, frozen=True) +class BoxName: + name: str + name_raw: bytes + name_base64: str + + +@dataclass(kw_only=True, frozen=True) +class BoxValue: + name: BoxName + value: bytes + + +class DataTypeFlag(IntEnum): + BYTES = 1 + UINT = 2 + + +TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] + + +BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner + + +class BoxReference(AlgosdkBoxReference): + def __init__(self, app_id: int, name: bytes | str): + super().__init__(app_index=app_id, name=self._b64_decode(name)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, (BoxReference | AlgosdkBoxReference)): + return self.app_index == other.app_index and self.name == other.name + return False + + def _b64_decode(self, value: str | bytes) -> bytes: + if isinstance(value, str): + try: + return base64.b64decode(value) + except Exception: + return value.encode("utf-8") + return value diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index ca8c0844..37a57fb4 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -1,4 +1,94 @@ from dataclasses import dataclass +from typing import Any, Literal, TypedDict, TypeVar + +import algosdk + +__all__ = [ + "Arc2TransactionNote", + "BaseArc2Note", + "JsonFormatArc2Note", + "StringFormatArc2Note", + "TransactionNote", + "TransactionNoteData", + "TransactionWrapper", +] + + +# Define specific types for different formats +class BaseArc2Note(TypedDict): + """Base ARC-0002 transaction note structure""" + + dapp_name: str + + +class StringFormatArc2Note(BaseArc2Note): + """ARC-0002 note for string-based formats (m/b/u)""" + + format: Literal["m", "b", "u"] + data: str + + +class JsonFormatArc2Note(BaseArc2Note): + """ARC-0002 note for JSON format""" + + format: Literal["j"] + data: str | dict[str, Any] | list[Any] | int | None + + +# Combined type for all valid ARC-0002 notes +# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md +Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note + +TransactionNoteData = str | None | int | list[Any] | dict[str, Any] +TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote + +TxnTypeT = TypeVar("TxnTypeT", bound=algosdk.transaction.Transaction) + + +class TransactionWrapper(algosdk.transaction.Transaction): + """Wrapper around algosdk.transaction.Transaction with optional property validators""" + + def __init__(self, transaction: algosdk.transaction.Transaction) -> None: + self._raw = transaction + + @property + def raw(self) -> algosdk.transaction.Transaction: + return self._raw + + @property + def payment(self) -> algosdk.transaction.PaymentTxn: + return self._return_if_type( + algosdk.transaction.PaymentTxn, + ) + + @property + def keyreg(self) -> algosdk.transaction.KeyregTxn: + return self._return_if_type(algosdk.transaction.KeyregTxn) + + @property + def asset_config(self) -> algosdk.transaction.AssetConfigTxn: + return self._return_if_type(algosdk.transaction.AssetConfigTxn) + + @property + def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn: + return self._return_if_type(algosdk.transaction.AssetTransferTxn) + + @property + def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn: + return self._return_if_type(algosdk.transaction.AssetFreezeTxn) + + @property + def application_call(self) -> algosdk.transaction.ApplicationCallTxn: + return self._return_if_type(algosdk.transaction.ApplicationCallTxn) + + @property + def state_proof(self) -> algosdk.transaction.StateProofTxn: + return self._return_if_type(algosdk.transaction.StateProofTxn) + + def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT: + if isinstance(self._raw, txn_type): + return self._raw + raise ValueError(f"Transaction is not of type {txn_type.__name__}") @dataclass(kw_only=True, frozen=True) @@ -6,3 +96,8 @@ class SendParams: max_rounds_to_wait: int | None = None suppress_log: bool | None = None populate_app_call_resources: bool | None = None + + +@dataclass(kw_only=True, frozen=True) +class TransactionConfirmation: + method: str diff --git a/src/algokit_utils/network_clients.py b/src/algokit_utils/network_clients.py index a9dc5de2..798100de 100644 --- a/src/algokit_utils/network_clients.py +++ b/src/algokit_utils/network_clients.py @@ -1 +1,9 @@ -from algokit_utils._legacy_v2.network_clients import * # noqa: F403 +import warnings + +warnings.warn( + "The legacy v2 network clients module is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.network_clients import * # noqa: F403, E402 diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py index e69de29b..ca59d88f 100644 --- a/src/algokit_utils/protocols/__init__.py +++ b/src/algokit_utils/protocols/__init__.py @@ -0,0 +1 @@ +from algokit_utils.protocols.client import * # noqa: F403 diff --git a/src/algokit_utils/protocols/application.py b/src/algokit_utils/protocols/client.py similarity index 58% rename from src/algokit_utils/protocols/application.py rename to src/algokit_utils/protocols/client.py index c4782162..4ae98b4b 100644 --- a/src/algokit_utils/protocols/application.py +++ b/src/algokit_utils/protocols/client.py @@ -1,14 +1,8 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Protocol - -from typing_extensions import runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient - from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager from algokit_utils.clients.client_manager import ClientManager @@ -16,12 +10,9 @@ from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender - -@dataclass -class NetworkDetails: - genesis_id: str - genesis_hash: str - network_name: str +__all__ = [ + "AlgorandClientProtocol", +] @runtime_checkable @@ -42,20 +33,3 @@ def new_group(self) -> TransactionComposer: ... @property def client(self) -> ClientManager: ... - - -@runtime_checkable -class ClientManagerProtocol(Protocol): - @property - def algod(self) -> AlgodClient: ... - - @property - def indexer(self) -> IndexerClient | None: ... - - async def network(self) -> NetworkDetails: ... - - async def is_local_net(self) -> bool: ... - - async def is_test_net(self) -> bool: ... - - async def is_main_net(self) -> bool: ... diff --git a/src/algokit_utils/transactions/__init__.py b/src/algokit_utils/transactions/__init__.py index e69de29b..6a540c0c 100644 --- a/src/algokit_utils/transactions/__init__.py +++ b/src/algokit_utils/transactions/__init__.py @@ -0,0 +1,4 @@ +from algokit_utils.transactions.transaction_composer import * # noqa: F403 +from algokit_utils.transactions.transaction_creator import * # noqa: F403 +from algokit_utils.transactions.transaction_sender import * # noqa: F403 +from algokit_utils.transactions.utils import * # noqa: F403 diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py deleted file mode 100644 index 33edd94c..00000000 --- a/src/algokit_utils/transactions/models.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Any, Literal, TypedDict, TypeVar, cast - -import algosdk - - -# Define specific types for different formats -class BaseArc2Note(TypedDict): - """Base ARC-0002 transaction note structure""" - - dapp_name: str - - -class StringFormatArc2Note(BaseArc2Note): - """ARC-0002 note for string-based formats (m/b/u)""" - - format: Literal["m", "b", "u"] - data: str - - -class JsonFormatArc2Note(BaseArc2Note): - """ARC-0002 note for JSON format""" - - format: Literal["j"] - data: str | dict[str, Any] | list[Any] | int | None - - -# Combined type for all valid ARC-0002 notes -# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md -Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note - -TransactionNoteData = str | None | int | list[Any] | dict[str, Any] -TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote - -T = TypeVar("T") - - -class TransactionWrapper(algosdk.transaction.Transaction): - """Wrapper around algosdk.transaction.Transaction with optional property validators""" - - def __init__(self, transaction: algosdk.transaction.Transaction) -> None: - self._raw = transaction - - @property - def raw(self) -> algosdk.transaction.Transaction: - return self._raw - - @property - def payment(self) -> algosdk.transaction.PaymentTxn | None: - return self._return_if_type( - algosdk.transaction.PaymentTxn, - ) - - @property - def keyreg(self) -> algosdk.transaction.KeyregTxn | None: - return self._return_if_type(algosdk.transaction.KeyregTxn) - - @property - def asset_config(self) -> algosdk.transaction.AssetConfigTxn | None: - return self._return_if_type(algosdk.transaction.AssetConfigTxn) - - @property - def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn | None: - return self._return_if_type(algosdk.transaction.AssetTransferTxn) - - @property - def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn | None: - return self._return_if_type(algosdk.transaction.AssetFreezeTxn) - - @property - def application_call(self) -> algosdk.transaction.ApplicationCallTxn | None: - return self._return_if_type(algosdk.transaction.ApplicationCallTxn) - - @property - def state_proof(self) -> algosdk.transaction.StateProofTxn | None: - return self._return_if_type(algosdk.transaction.StateProofTxn) - - def _return_if_type(self, txn_type: type[T]) -> T | None: - if isinstance(self._raw, txn_type): - return cast(T, self._raw) - return None diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index be78818f..b3062d39 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -17,10 +17,11 @@ from typing_extensions import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response +from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method from algokit_utils.config import config -from algokit_utils.models.transaction import SendParams -from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.models.transaction import SendParams, TransactionWrapper from algokit_utils.transactions.utils import encode_lease, populate_app_call_resources if TYPE_CHECKING: @@ -30,22 +31,39 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.models import SimulateTraceConfig - from algokit_utils.applications.app_manager import BoxReference - from algokit_utils.models.abi import ABIValue + from algokit_utils.applications.abi import ABIValue from algokit_utils.models.amount import AlgoAmount - from algokit_utils.transactions.models import Arc2TransactionNote + from algokit_utils.models.state import BoxIdentifier, BoxReference + from algokit_utils.models.transaction import Arc2TransactionNote + + +__all__ = [ + "AppCallParams", + "AppCreateParams", + "AppDeleteParams", + "AppUpdateParams", + "AssetConfigParams", + "AssetCreateParams", + "AssetDestroyParams", + "AssetFreezeParams", + "AssetOptInParams", + "AssetOptOutParams", + "AssetTransferParams", + "OnlineKeyRegistrationParams", + "PaymentParams", + "SendAtomicTransactionComposerResults", + "TransactionComposer", + "TransactionComposerBuildResult", + "TxnParams", + "send_atomic_transaction_composer", +] logger = config.logger @dataclass(kw_only=True, frozen=True) -class SenderParam: - sender: str - - -@dataclass(kw_only=True, frozen=True) -class CommonTxnParams(SendParams): +class _CommonTxnParams: """ Common transaction parameters. @@ -76,9 +94,14 @@ class CommonTxnParams(SendParams): last_valid_round: int | None = None +@dataclass(kw_only=True, frozen=True) +class _CommonTxnWithSendParams(_CommonTxnParams, SendParams): + pass + + @dataclass(kw_only=True, frozen=True) class PaymentParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Payment transaction parameters. @@ -95,7 +118,7 @@ class PaymentParams( @dataclass(kw_only=True, frozen=True) class AssetCreateParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset creation parameters. @@ -131,7 +154,7 @@ class AssetCreateParams( @dataclass(kw_only=True, frozen=True) class AssetConfigParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset configuration parameters. @@ -155,7 +178,7 @@ class AssetConfigParams( @dataclass(kw_only=True, frozen=True) class AssetFreezeParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset freeze parameters. @@ -172,7 +195,7 @@ class AssetFreezeParams( @dataclass(kw_only=True, frozen=True) class AssetDestroyParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset destruction parameters. @@ -185,7 +208,7 @@ class AssetDestroyParams( @dataclass(kw_only=True, frozen=True) class OnlineKeyRegistrationParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Online key registration parameters. @@ -211,7 +234,7 @@ class OnlineKeyRegistrationParams( @dataclass(kw_only=True, frozen=True) class AssetTransferParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset transfer parameters. @@ -232,7 +255,7 @@ class AssetTransferParams( @dataclass(kw_only=True, frozen=True) class AssetOptInParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset opt-in parameters. @@ -245,7 +268,7 @@ class AssetOptInParams( @dataclass(kw_only=True, frozen=True) class AssetOptOutParams( - CommonTxnParams, + _CommonTxnWithSendParams, ): """ Asset opt-out parameters. @@ -256,7 +279,7 @@ class AssetOptOutParams( @dataclass(kw_only=True, frozen=True) -class AppCallParams(CommonTxnParams, SenderParam): +class AppCallParams(_CommonTxnWithSendParams): """ Application call parameters. @@ -287,7 +310,7 @@ class AppCallParams(CommonTxnParams, SenderParam): @dataclass(kw_only=True, frozen=True) -class AppCreateParams(CommonTxnParams, SenderParam): +class AppCreateParams(_CommonTxnWithSendParams): """ Application create parameters. @@ -318,7 +341,9 @@ class AppCreateParams(CommonTxnParams, SenderParam): @dataclass(kw_only=True, frozen=True) -class AppUpdateParams(CommonTxnParams, SenderParam): +class AppUpdateParams( + _CommonTxnWithSendParams, +): """ Application update parameters. @@ -336,14 +361,13 @@ class AppUpdateParams(CommonTxnParams, SenderParam): account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None on_complete: OnComplete | None = None @dataclass(kw_only=True, frozen=True) class AppDeleteParams( - CommonTxnParams, - SenderParam, + _CommonTxnWithSendParams, ): """ Application delete parameters. @@ -356,12 +380,12 @@ class AppDeleteParams( account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None on_complete: OnComplete = OnComplete.DeleteApplicationOC @dataclass(kw_only=True, frozen=True) -class AppMethodCall(CommonTxnParams, SenderParam): +class _BaseAppMethodCall(_CommonTxnWithSendParams): """Base class for ABI method calls.""" app_id: int @@ -370,12 +394,12 @@ class AppMethodCall(CommonTxnParams, SenderParam): account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None schema: dict[str, int] | None = None @dataclass(kw_only=True, frozen=True) -class AppMethodCallParams(CommonTxnParams, SenderParam): +class AppMethodCallParams(_CommonTxnWithSendParams): """ Method call parameters. @@ -396,7 +420,7 @@ class AppMethodCallParams(CommonTxnParams, SenderParam): @dataclass(kw_only=True, frozen=True) -class AppCallMethodCall(AppMethodCall): +class AppCallMethodCallParams(_BaseAppMethodCall): """Parameters for a regular ABI method call. :param app_id: ID of the application @@ -415,7 +439,7 @@ class AppCallMethodCall(AppMethodCall): @dataclass(kw_only=True, frozen=True) -class AppCreateMethodCall(AppMethodCall): +class AppCreateMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that creates an application. :param approval_program: The program to execute for all OnCompletes other than ClearState @@ -433,7 +457,7 @@ class AppCreateMethodCall(AppMethodCall): @dataclass(kw_only=True, frozen=True) -class AppUpdateMethodCall(AppMethodCall): +class AppUpdateMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that updates an application. :param app_id: ID of the application @@ -448,7 +472,7 @@ class AppUpdateMethodCall(AppMethodCall): @dataclass(kw_only=True, frozen=True) -class AppDeleteMethodCall(AppMethodCall): +class AppDeleteMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that deletes an application. :param app_id: ID of the application @@ -459,16 +483,18 @@ class AppDeleteMethodCall(AppMethodCall): # Type alias for all possible method call types -MethodCallParams = AppCallMethodCall | AppCreateMethodCall | AppUpdateMethodCall | AppDeleteMethodCall +MethodCallParams = ( + AppCallMethodCallParams | AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams +) # Type alias for transaction arguments in method calls AppMethodCallTransactionArgument = ( TransactionWithSigner | algosdk.transaction.Transaction - | AppCreateMethodCall - | AppUpdateMethodCall - | AppCallMethodCall + | AppCreateMethodCallParams + | AppUpdateMethodCallParams + | AppCallMethodCallParams ) @@ -524,7 +550,7 @@ class SendAtomicTransactionComposerResults: """The transaction IDs that were sent""" transactions: list[TransactionWrapper] """The transactions that were sent""" - returns: list[Any] | list[algosdk.atomic_transaction_composer.ABIResult] + returns: list[ABIReturn] """The ABI return values from any ABI method calls""" simulate_response: dict[str, Any] | None = None @@ -536,7 +562,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, suppress_log: bool | None = None, - populate_resources: bool | None = None, # TODO: implement/clarify + populate_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group @@ -607,7 +633,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 confirmations=confirmations or [], tx_ids=[t.get_txid() for t in transactions_to_send], transactions=[TransactionWrapper(t) for t in transactions_to_send], - returns=result.abi_results, + returns=[ABIReturn(r) for r in result.abi_results], ) except Exception as e: @@ -692,123 +718,123 @@ def __init__( default_validity_window (Optional[int], optional): The default validity window for transactions. If not provided, it defaults to 10. Defaults to None. """ - self.txn_method_map: dict[str, algosdk.abi.Method] = {} - self.txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] - self.atc: AtomicTransactionComposer = AtomicTransactionComposer() - self.algod: AlgodClient = algod - self.default_get_send_params = lambda: self.algod.suggested_params() - self.get_suggested_params = get_suggested_params or self.default_get_send_params - self.get_signer: Callable[[str], TransactionSigner] = get_signer - self.default_validity_window: int = default_validity_window or 10 - self.app_manager = app_manager or AppManager(algod) + self._txn_method_map: dict[str, algosdk.abi.Method] = {} + self._txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] + self._atc: AtomicTransactionComposer = AtomicTransactionComposer() + self._algod: AlgodClient = algod + self._default_get_send_params = lambda: self._algod.suggested_params() + self._get_suggested_params = get_suggested_params or self._default_get_send_params + self._get_signer: Callable[[str], TransactionSigner] = get_signer + self._default_validity_window: int = default_validity_window or 10 + self._app_manager = app_manager or AppManager(algod) def add_transaction( self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None ) -> TransactionComposer: - self.txns.append(TransactionWithSigner(txn=transaction, signer=signer or self.get_signer(transaction.sender))) + self._txns.append(TransactionWithSigner(txn=transaction, signer=signer or self._get_signer(transaction.sender))) return self def add_payment(self, params: PaymentParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_create(self, params: AssetCreateParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_config(self, params: AssetConfigParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_freeze(self, params: AssetFreezeParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_destroy(self, params: AssetDestroyParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_transfer(self, params: AssetTransferParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_opt_in(self, params: AssetOptInParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_asset_opt_out(self, params: AssetOptOutParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_app_create(self, params: AppCreateParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_app_update(self, params: AppUpdateParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_app_delete(self, params: AppDeleteParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_app_call(self, params: AppCallParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self - def add_app_create_method_call(self, params: AppCreateMethodCall) -> TransactionComposer: - self.txns.append(params) + def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> TransactionComposer: + self._txns.append(params) return self - def add_app_update_method_call(self, params: AppUpdateMethodCall) -> TransactionComposer: - self.txns.append(params) + def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> TransactionComposer: + self._txns.append(params) return self - def add_app_delete_method_call(self, params: AppDeleteMethodCall) -> TransactionComposer: - self.txns.append(params) + def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> TransactionComposer: + self._txns.append(params) return self - def add_app_call_method_call(self, params: AppCallMethodCall) -> TransactionComposer: - self.txns.append(params) + def add_app_call_method_call(self, params: AppCallMethodCallParams) -> TransactionComposer: + self._txns.append(params) return self def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> TransactionComposer: - self.txns.append(params) + self._txns.append(params) return self def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer: - self.txns.append(atc) + self._txns.append(atc) return self def count(self) -> int: return len(self.build_transactions().transactions) def build(self) -> TransactionComposerBuildResult: - if self.atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: - suggested_params = self.get_suggested_params() + if self._atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: + suggested_params = self._get_suggested_params() txn_with_signers: list[TransactionWithSigner] = [] - for txn in self.txns: + for txn in self._txns: txn_with_signers.extend(self._build_txn(txn, suggested_params)) for ts in txn_with_signers: - self.atc.add_transaction(ts) - method = self.txn_method_map.get(ts.txn.get_txid()) + self._atc.add_transaction(ts) + method = self._txn_method_map.get(ts.txn.get_txid()) if method: - self.atc.method_dict[len(self.atc.txn_list) - 1] = method + self._atc.method_dict[len(self._atc.txn_list) - 1] = method return TransactionComposerBuildResult( - atc=self.atc, - transactions=self.atc.build_group(), - method_calls=self.atc.method_dict, + atc=self._atc, + transactions=self._atc.build_group(), + method_calls=self._atc.method_dict, ) def rebuild(self) -> TransactionComposerBuildResult: - self.atc = AtomicTransactionComposer() + self._atc = AtomicTransactionComposer() return self.build() def build_transactions(self) -> BuiltTransactions: - suggested_params = self.get_suggested_params() + suggested_params = self._get_suggested_params() transactions: list[algosdk.transaction.Transaction] = [] method_calls: dict[int, Method] = {} @@ -816,7 +842,7 @@ def build_transactions(self) -> BuiltTransactions: idx = 0 - for txn in self.txns: + for txn in self._txns: txn_with_signers: list[TransactionWithSigner] = [] if isinstance(txn, MethodCallParams): @@ -828,7 +854,7 @@ def build_transactions(self) -> BuiltTransactions: transactions.append(ts.txn) if ts.signer and ts.signer != self.NULL_SIGNER: signers[idx] = ts.signer - method = self.txn_method_map.get(ts.txn.get_txid()) + method = self._txn_method_map.get(ts.txn.get_txid()) if method: method_calls[idx] = method idx += 1 @@ -857,13 +883,13 @@ def send( wait_rounds = max_rounds_to_wait if wait_rounds is None: last_round = max(txn.txn.last_valid_round for txn in group) - first_round = self.get_suggested_params().first + first_round = self._get_suggested_params().first wait_rounds = last_round - first_round + 1 try: return send_atomic_transaction_composer( - self.atc, - self.algod, + self._atc, + self._algod, max_rounds_to_wait=wait_rounds, suppress_log=suppress_log, populate_resources=populate_app_call_resources, @@ -878,15 +904,13 @@ def simulate( allow_unnamed_resources: bool | None = None, extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, - round: int | None = None, # noqa: A002 TODO: revisit + simulation_round: int | None = None, skip_signatures: int | None = None, - fix_signers: bool | None = None, ) -> SendAtomicTransactionComposerResults: - atc = AtomicTransactionComposer() if skip_signatures else self.atc + atc = AtomicTransactionComposer() if skip_signatures else self._atc if skip_signatures: allow_empty_signatures = True - fix_signers = True transactions = self.build_transactions() for txn in transactions.transactions: atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer.NULL_SIGNER)) @@ -898,38 +922,38 @@ def simulate( response = simulate_and_persist_response( atc, config.project_root, - self.algod, + self._algod, config.trace_buffer_size_mb, allow_more_logs, allow_empty_signatures, allow_unnamed_resources, extra_opcode_budget, exec_trace_config, - round, + simulation_round, skip_signatures, - fix_signers, ) return SendAtomicTransactionComposerResults( - confirmations=[], # TODO: extract confirmations, + confirmations=response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ + "txn-results" + ], transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], tx_ids=response.tx_ids, group_id=atc.txn_list[-1].txn.group or "", simulate_response=response.simulate_response, - returns=response.abi_results, + returns=[ABIReturn(r) for r in response.abi_results], ) response = simulate_response( atc, - self.algod, + self._algod, allow_more_logs, allow_empty_signatures, allow_unnamed_resources, extra_opcode_budget, exec_trace_config, - round, + simulation_round, skip_signatures, - fix_signers, ) confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ @@ -942,7 +966,7 @@ def simulate( tx_ids=response.tx_ids, group_id=atc.txn_list[-1].txn.group or "", simulate_response=response.simulate_response, - returns=response.abi_results, + returns=[ABIReturn(r) for r in response.abi_results], ) @staticmethod @@ -966,14 +990,14 @@ def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSign method = atc.method_dict.get(len(group) - 1) if method: - self.txn_method_map[group[-1].txn.get_txid()] = method + self._txn_method_map[group[-1].txn.get_txid()] = method return group def _common_txn_build_step( self, build_txn: Callable[[dict], algosdk.transaction.Transaction], - params: CommonTxnParams, + params: _CommonTxnWithSendParams, txn_params: dict, ) -> algosdk.transaction.Transaction: # Clone suggested params @@ -992,6 +1016,9 @@ def _common_txn_build_step( txn_params["sp"].fee = params.static_fee.micro_algos txn_params["sp"].flat_fee = True + if isinstance(txn_params.get("method"), Arc56Method): + txn_params["method"] = txn_params["method"].to_abi_method() + txn = build_txn(txn_params) if params.extra_fee: @@ -1021,11 +1048,16 @@ def _build_method_call( # noqa: C901, PLR0912 if isinstance(arg, algosdk.transaction.Transaction): # Wrap in TransactionWithSigner method_args.append( - TransactionWithSigner(txn=arg, signer=params.signer or self.get_signer(params.sender)) + TransactionWithSigner(txn=arg, signer=params.signer or self._get_signer(params.sender)) ) continue match arg: - case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): + case ( + AppCreateMethodCallParams() + | AppCallMethodCallParams() + | AppUpdateMethodCallParams() + | AppDeleteMethodCallParams() + ): temp_txn_with_signers = self._build_method_call(arg, suggested_params) method_args.extend(temp_txn_with_signers) arg_offset += len(temp_txn_with_signers) - 1 @@ -1054,7 +1086,7 @@ def _build_method_call( # noqa: C901, PLR0912 raise ValueError(f"Unsupported method arg transaction type: {arg!s}") method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + TransactionWithSigner(txn=txn, signer=params.signer or self._get_signer(params.sender)) ) continue @@ -1066,7 +1098,7 @@ def _build_method_call( # noqa: C901, PLR0912 "method": params.method, "sender": params.sender, "sp": suggested_params, - "signer": params.signer or self.get_signer(params.sender), + "signer": params.signer or self._get_signer(params.sender), "method_args": method_args, "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, "note": params.note, @@ -1089,8 +1121,8 @@ def _build_method_call( # noqa: C901, PLR0912 ) if params.schema else None, - "approval_program": params.approval_program if hasattr(params, "approval_program") else None, - "clear_program": params.clear_state_program if hasattr(params, "clear_state_program") else None, + "approval_program": getattr(params, "approval_program", None), + "clear_program": getattr(params, "clear_state_program", None), "rekey_to": params.rekey_to, } @@ -1148,12 +1180,12 @@ def _build_app_call( if isinstance(params, AppUpdateParams | AppCreateParams): if isinstance(params.approval_program, str): - approval_program = self.app_manager.compile_teal(params.approval_program).compiled_base64_to_bytes + approval_program = self._app_manager.compile_teal(params.approval_program).compiled_base64_to_bytes elif isinstance(params.approval_program, bytes): approval_program = params.approval_program if isinstance(params.clear_state_program, str): - clear_program = self.app_manager.compile_teal(params.clear_state_program).compiled_base64_to_bytes + clear_program = self._app_manager.compile_teal(params.clear_state_program).compiled_base64_to_bytes elif isinstance(params.clear_state_program, bytes): clear_program = params.clear_state_program @@ -1290,12 +1322,17 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 case AtomicTransactionComposer(): return self._build_atc(txn) case algosdk.transaction.Transaction(): - signer = self.get_signer(txn.sender) + signer = self._get_signer(txn.sender) return [TransactionWithSigner(txn=txn, signer=signer)] - case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): + case ( + AppCreateMethodCallParams() + | AppCallMethodCallParams() + | AppUpdateMethodCallParams() + | AppDeleteMethodCallParams() + ): return self._build_method_call(txn, suggested_params) - signer = txn.signer or self.get_signer(txn.sender) + signer = txn.signer or self._get_signer(txn.sender) match txn: case PaymentParams(): diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py index a5bc8926..7bd4899a 100644 --- a/src/algokit_utils/transactions/transaction_creator.py +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -4,13 +4,13 @@ from algosdk.transaction import Transaction from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCallParams, - AppCreateMethodCall, + AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCall, + AppDeleteMethodCallParams, AppDeleteParams, - AppUpdateMethodCall, + AppUpdateMethodCallParams, AppUpdateParams, AssetConfigParams, AssetCreateParams, @@ -25,6 +25,10 @@ TransactionComposer, ) +__all__ = [ + "AlgorandClientTransactionCreator", +] + TxnParam = TypeVar("TxnParam") TxnResult = TypeVar("TxnResult") @@ -125,22 +129,22 @@ def app_call(self) -> Callable[[AppCallParams], Transaction]: return self._transaction(lambda c: c.add_app_call) @property - def app_create_method_call(self) -> Callable[[AppCreateMethodCall], BuiltTransactions]: + def app_create_method_call(self) -> Callable[[AppCreateMethodCallParams], BuiltTransactions]: """Create an application create call with ABI method call transaction.""" return self._transactions(lambda c: c.add_app_create_method_call) @property - def app_update_method_call(self) -> Callable[[AppUpdateMethodCall], BuiltTransactions]: + def app_update_method_call(self) -> Callable[[AppUpdateMethodCallParams], BuiltTransactions]: """Create an application update call with ABI method call transaction.""" return self._transactions(lambda c: c.add_app_update_method_call) @property - def app_delete_method_call(self) -> Callable[[AppDeleteMethodCall], BuiltTransactions]: + def app_delete_method_call(self) -> Callable[[AppDeleteMethodCallParams], BuiltTransactions]: """Create an application delete call with ABI method call transaction.""" return self._transactions(lambda c: c.add_app_delete_method_call) @property - def app_call_method_call(self) -> Callable[[AppCallMethodCall], BuiltTransactions]: + def app_call_method_call(self) -> Callable[[AppCallMethodCallParams], BuiltTransactions]: """Create an application call with ABI method call transaction.""" return self._transactions(lambda c: c.add_app_call_method_call) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 31fa15a4..6c072edc 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,24 +1,25 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any, TypedDict, TypeVar +from typing import Any, TypeVar import algosdk import algosdk.atomic_transaction_composer -from algosdk.atomic_transaction_composer import ABIResult, AtomicTransactionResponse from algosdk.transaction import Transaction +from typing_extensions import Self +from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.config import config -from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.models.transaction import TransactionWrapper from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCallParams, - AppCreateMethodCall, + AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCall, + AppDeleteMethodCallParams, AppDeleteParams, - AppUpdateMethodCall, + AppUpdateMethodCallParams, AppUpdateParams, AssetConfigParams, AssetCreateParams, @@ -29,13 +30,26 @@ AssetTransferParams, OnlineKeyRegistrationParams, PaymentParams, + SendAtomicTransactionComposerResults, TransactionComposer, TxnParams, ) +__all__ = [ + "AlgorandClientTransactionSender", + "SendAppCreateTransactionResult", + "SendAppTransactionResult", + "SendAppUpdateTransactionResult", + "SendSingleAssetCreateTransactionResult", + "SendSingleTransactionResult", +] + logger = config.logger +T = TypeVar("T", bound=TxnParams) + + @dataclass(frozen=True, kw_only=True) class SendSingleTransactionResult: transaction: TransactionWrapper # Last transaction @@ -47,7 +61,40 @@ class SendSingleTransactionResult: tx_ids: list[str] # Full array of transaction IDs transactions: list[TransactionWrapper] confirmations: list[algosdk.v2client.algod.AlgodResponseType] - returns: list[algosdk.atomic_transaction_composer.ABIResult] | None = None + returns: list[ABIReturn] | None = None + + @classmethod + def from_composer_result(cls, result: SendAtomicTransactionComposerResults, index: int = -1) -> Self: + # Get base parameters + base_params = { + "transaction": result.transactions[index], + "confirmation": result.confirmations[index], + "group_id": result.group_id, + "tx_id": result.tx_ids[index], + "tx_ids": result.tx_ids, + "transactions": [result.transactions[index]], + "confirmations": result.confirmations, + "returns": result.returns, + } + + # For asset creation, extract asset_id from confirmation + if cls is SendSingleAssetCreateTransactionResult: + base_params["asset_id"] = result.confirmations[index]["asset-index"] # type: ignore[call-overload] + # For app creation, extract app_id and calculate app_address + elif cls is SendAppCreateTransactionResult: + app_id = result.confirmations[index]["application-index"] # type: ignore[call-overload] + base_params.update( + { + "app_id": app_id, + "app_address": algosdk.logic.get_application_address(app_id), + "abi_return": result.returns[index] if result.returns else None, # type: ignore[dict-item] + } + ) + # For regular app transactions, just add abi_return + elif cls is SendAppTransactionResult: + base_params["abi_return"] = result.returns[index] if result.returns else None # type: ignore[assignment] + + return cls(**base_params) # type: ignore[arg-type] @dataclass(frozen=True, kw_only=True) @@ -57,7 +104,7 @@ class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): @dataclass(frozen=True) class SendAppTransactionResult(SendSingleTransactionResult): - return_value: ABIResult | None = None + abi_return: ABIReturn | None = None @dataclass(frozen=True) @@ -72,14 +119,6 @@ class SendAppCreateTransactionResult(SendAppUpdateTransactionResult): app_address: str -class LogConfig(TypedDict, total=False): - pre_log: Callable[[TxnParams, Transaction], str] - post_log: Callable[[TxnParams, AtomicTransactionResponse], str] - - -T = TypeVar("T", bound=TxnParams) - - class AlgorandClientTransactionSender: """Orchestrates sending transactions for AlgorandClient.""" @@ -145,7 +184,7 @@ def send_app_call(params: T) -> SendAppTransactionResult: result = self._send(c, pre_log, post_log)(params) return SendAppTransactionResult( **result.__dict__, - return_value=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), + abi_return=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), ) return send_app_call @@ -159,7 +198,9 @@ def _send_app_update_call( def send_app_update_call(params: T) -> SendAppUpdateTransactionResult: result = self._send_app_call(c, pre_log, post_log)(params) - if not isinstance(params, AppCreateParams | AppUpdateParams | AppCreateMethodCall | AppUpdateMethodCall): + if not isinstance( + params, AppCreateParams | AppUpdateParams | AppCreateMethodCallParams | AppUpdateMethodCallParams + ): raise TypeError("Invalid parameter type") compiled_approval = ( @@ -332,19 +373,19 @@ def app_call(self, params: AppCallParams) -> SendAppTransactionResult: """Call an application.""" return self._send_app_call(lambda c: c.add_app_call)(params) - def app_create_method_call(self, params: AppCreateMethodCall) -> SendAppCreateTransactionResult: + def app_create_method_call(self, params: AppCreateMethodCallParams) -> SendAppCreateTransactionResult: """Call an application's create method.""" return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params) - def app_update_method_call(self, params: AppUpdateMethodCall) -> SendAppUpdateTransactionResult: + def app_update_method_call(self, params: AppUpdateMethodCallParams) -> SendAppUpdateTransactionResult: """Call an application's update method.""" return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params) - def app_delete_method_call(self, params: AppDeleteMethodCall) -> SendAppTransactionResult: + def app_delete_method_call(self, params: AppDeleteMethodCallParams) -> SendAppTransactionResult: """Call an application's delete method.""" return self._send_app_call(lambda c: c.add_app_delete_method_call)(params) - def app_call_method_call(self, params: AppCallMethodCall) -> SendAppTransactionResult: + def app_call_method_call(self, params: AppCallMethodCallParams) -> SendAppTransactionResult: """Call an application's call method.""" return self._send_app_call(lambda c: c.add_app_call_method_call)(params) diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py index 96a67c13..966d47c8 100644 --- a/src/algokit_utils/transactions/utils.py +++ b/src/algokit_utils/transactions/utils.py @@ -8,7 +8,12 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup -from algokit_utils.applications.app_manager import BoxReference +from algokit_utils.models.state import BoxReference + +__all__ = [ + "get_unnamed_app_call_resources_accessed", + "populate_app_call_resources", +] # Constants MAX_APP_CALL_ACCOUNT_REFERENCES = 4 diff --git a/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt new file mode 100644 index 00000000..bdedcb69 --- /dev/null +++ b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt @@ -0,0 +1 @@ +{"txn-group-sources": [{"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}, {"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}]} \ No newline at end of file diff --git a/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt new file mode 100644 index 00000000..bdedcb69 --- /dev/null +++ b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt @@ -0,0 +1 @@ +{"txn-group-sources": [{"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}, {"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}]} \ No newline at end of file diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py index ec56a007..ad78ac43 100644 --- a/tests/accounts/test_account_manager.py +++ b/tests/accounts/test_account_manager.py @@ -30,7 +30,7 @@ def test_new_account_is_retrieved_and_funded(algorand: AlgorandClient) -> None: # Assert account_info = algorand.account.get_information(account.address) - assert account_info["amount"] > 0 + assert account_info.amount > 0 def test_same_account_is_subsequently_retrieved(algorand: AlgorandClient) -> None: @@ -90,7 +90,7 @@ def test_ensure_funded_from_environment(algorand: AlgorandClient) -> None: assert result is not None assert result.amount_funded is not None account_info = algorand.account.get_information(account.address) - assert account_info["amount"] >= min_balance.micro_algos + assert account_info.amount_without_pending_rewards >= min_balance.micro_algos def test_get_account_information(algorand: AlgorandClient) -> None: @@ -101,8 +101,7 @@ def test_get_account_information(algorand: AlgorandClient) -> None: info = algorand.account.get_information(account.address) # Assert - assert isinstance(info, dict) - assert "amount" in info - assert "min-balance" in info - assert "address" in info - assert info["address"] == account.address + assert info.amount is not None + assert info.min_balance is not None + assert info.address is not None + assert info.address == account.address diff --git a/tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt b/tests/applications/_snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt similarity index 100% rename from tests/applications/snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt rename to tests/applications/_snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt diff --git a/tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt b/tests/applications/_snapshots/test_app_manager.approvals/test_template_substitution.approved.txt similarity index 100% rename from tests/applications/snapshots/test_app_manager.approvals/test_template_substitution.approved.txt rename to tests/applications/_snapshots/test_app_manager.approvals/test_template_substitution.approved.txt diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt new file mode 100644 index 00000000..d16ea4c2 --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt @@ -0,0 +1,58 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + } + ], + "name": "hello", + "returns": { + "type": "string" + }, + "events": [] + } + ], + "name": "HelloWorld", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + } +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt new file mode 100644 index 00000000..15f2f121 --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt @@ -0,0 +1,58 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + } + ], + "name": "hello", + "returns": { + "type": "string" + }, + "events": [] + } + ], + "name": "HelloWorld", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + } +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt new file mode 100644 index 00000000..4d490fec --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt @@ -0,0 +1,510 @@ +{ + "arcs": [ + 22, + 28 + ], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "name": "set_governor", + "returns": { + "type": "void" + }, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token.", + "name": "seed" + }, + { + "type": "asset", + "desc": "One of the two assets this pool should allow swapping between.", + "name": "a_asset" + }, + { + "type": "asset", + "desc": "The other of the two assets this pool should allow swapping between.", + "name": "b_asset" + } + ], + "name": "bootstrap", + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens.", + "name": "a_xfer" + }, + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens.", + "name": "b_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the pool token so that we may distribute it.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "name": "b_asset" + } + ], + "name": "mint", + "returns": { + "type": "void" + }, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem", + "name": "pool_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of the pool token so we may inspect balance.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "name": "b_asset" + } + ], + "name": "burn", + "returns": { + "type": "void" + }, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B", + "name": "swap_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "name": "b_asset" + } + ], + "name": "swap", + "returns": { + "type": "void" + }, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "readonly": false, + "recommendations": {} + } + ], + "name": "ConstantProductAMM", + "state": { + "keys": { + "box": {}, + "global": { + "asset_a": { + "key": "YXNzZXRfYQ==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "asset_b": { + "key": "YXNzZXRfYg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "governor": { + "key": "Z292ZXJub3I=", + "keyType": "AVMString", + "valueType": "AVMBytes" + }, + "pool_token": { + "key": "cG9vbF90b2tlbg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "ratio": { + "key": "cmF0aW8=", + "keyType": "AVMString", + "valueType": "AVMUint64" + } + }, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 1, + "ints": 4 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "networks": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "sourceInfo": { + "approval": { + "pcOffsetMethod": "none", + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ] + }, + "clear": { + "pcOffsetMethod": "none", + "sourceInfo": [] + } + }, + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt new file mode 100644 index 00000000..4d490fec --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt @@ -0,0 +1,510 @@ +{ + "arcs": [ + 22, + 28 + ], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "name": "set_governor", + "returns": { + "type": "void" + }, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token.", + "name": "seed" + }, + { + "type": "asset", + "desc": "One of the two assets this pool should allow swapping between.", + "name": "a_asset" + }, + { + "type": "asset", + "desc": "The other of the two assets this pool should allow swapping between.", + "name": "b_asset" + } + ], + "name": "bootstrap", + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens.", + "name": "a_xfer" + }, + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens.", + "name": "b_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the pool token so that we may distribute it.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "name": "b_asset" + } + ], + "name": "mint", + "returns": { + "type": "void" + }, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem", + "name": "pool_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of the pool token so we may inspect balance.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "name": "b_asset" + } + ], + "name": "burn", + "returns": { + "type": "void" + }, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B", + "name": "swap_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "name": "b_asset" + } + ], + "name": "swap", + "returns": { + "type": "void" + }, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "readonly": false, + "recommendations": {} + } + ], + "name": "ConstantProductAMM", + "state": { + "keys": { + "box": {}, + "global": { + "asset_a": { + "key": "YXNzZXRfYQ==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "asset_b": { + "key": "YXNzZXRfYg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "governor": { + "key": "Z292ZXJub3I=", + "keyType": "AVMString", + "valueType": "AVMBytes" + }, + "pool_token": { + "key": "cG9vbF90b2tlbg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "ratio": { + "key": "cmF0aW8=", + "keyType": "AVMString", + "valueType": "AVMUint64" + } + }, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 1, + "ints": 4 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "networks": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "sourceInfo": { + "approval": { + "pcOffsetMethod": "none", + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ] + }, + "clear": { + "pcOffsetMethod": "none", + "sourceInfo": [] + } + }, + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index c246924a..008b2558 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -9,20 +9,20 @@ from algosdk.atomic_transaction_composer import TransactionSigner, TransactionWithSigner from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.abi import ABIType from algokit_utils.applications.app_client import ( AppClient, AppClientMethodCallWithSendParams, AppClientParams, FundAppAccountParams, ) -from algokit_utils.applications.app_manager import AppManager, BoxReference -from algokit_utils.applications.utils import arc32_to_arc56, get_arc56_method +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Network from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.errors.logic_error import LogicError -from algokit_utils.models.abi import ABIType from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount -from algokit_utils.models.application import Arc56Contract +from algokit_utils.models.state import BoxReference from algokit_utils.transactions.transaction_composer import AppCreateParams, PaymentParams @@ -44,13 +44,13 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture def raw_hello_world_arc32_app_spec() -> str: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" return raw_json_spec.read_text() @pytest.fixture def hello_world_arc32_app_spec() -> ApplicationSpecification: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" return ApplicationSpecification.from_json(raw_json_spec.read_text()) @@ -78,13 +78,13 @@ def hello_world_arc32_app_id( @pytest.fixture def raw_testing_app_arc32_app_spec() -> str: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "app_spec.arc32.json" return raw_json_spec.read_text() @pytest.fixture def testing_app_arc32_app_spec() -> ApplicationSpecification: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "app_spec.arc32.json" return ApplicationSpecification.from_json(raw_json_spec.read_text()) @@ -161,7 +161,7 @@ def test_app_client_with_sourcemaps( @pytest.fixture def testing_app_puya_arc32_app_spec() -> ApplicationSpecification: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app_puya" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app_puya" / "app_spec.arc32.json" return ApplicationSpecification.from_json(raw_json_spec.read_text()) @@ -206,9 +206,6 @@ def test_app_client_puya( ) -# TODO: add variations around arc 56 contracts too - - def test_clone_overriding_default_sender_and_inheriting_app_name( algorand: AlgorandClient, funded_account: Account, @@ -294,8 +291,8 @@ def test_resolve_from_network( hello_world_arc32_app_id: int, hello_world_arc32_app_spec: ApplicationSpecification, ) -> None: - arc56_app_spec = arc32_to_arc56(hello_world_arc32_app_spec) - arc56_app_spec.networks = {"localnet": {"app_id": hello_world_arc32_app_id}} + arc56_app_spec = Arc56Contract.from_arc32(hello_world_arc32_app_spec) + arc56_app_spec.networks = {"localnet": Network(app_id=hello_world_arc32_app_id)} app_client = AppClient.from_network( algorand=algorand, app_spec=arc56_app_spec, @@ -352,14 +349,14 @@ def test_construct_transaction_with_abi_encoding_including_transaction( assert result.confirmation assert len(result.transactions) == 2 - return_value = AppManager.get_abi_return( - result.confirmation, get_arc56_method("call_abi_txn", test_app_client.app_spec) + response = AppManager.get_abi_return( + result.confirmation, test_app_client.app_spec.get_arc56_method("call_abi_txn").to_abi_method() ) expected_return = f"Sent {amount.micro_algos}. test" - assert result.return_value - assert result.return_value.return_value == expected_return - assert return_value - assert return_value.return_value == result.return_value.return_value + assert result.abi_return + assert result.abi_return.value == expected_return + assert response + assert response.value == result.abi_return.value def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( @@ -450,12 +447,13 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no # Assuming the method returns a string matching the format below expected_return = AppManager.get_abi_return( result.confirmations[0], - get_arc56_method("call_abi_foreign_refs", test_app_client.app_spec), + test_app_client.app_spec.get_arc56_method("call_abi_foreign_refs").to_abi_method(), ) - assert result.return_value - assert "App: 345, Asset: 567, Account: " in result.return_value.return_value + assert result.abi_return + assert result.abi_return.value + assert str(result.abi_return.value).startswith("App: 345, Asset: 567, Account: ") assert expected_return - assert expected_return.return_value == result.return_value.return_value + assert expected_return.value == result.abi_return.value def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: @@ -681,15 +679,14 @@ def test_box_methods_with_arc4_returns_parametrized( assert abi_decoded_boxes[0].value == arg_value -# TODO: see if needs moving into app factory tests file def test_abi_with_default_arg_method( algorand: AlgorandClient, funded_account: Account, testing_app_arc32_app_id: int, testing_app_arc32_app_spec: ApplicationSpecification, ) -> None: - arc56_app_spec = arc32_to_arc56(testing_app_arc32_app_spec) - arc56_app_spec.networks = {"localnet": {"app_id": testing_app_arc32_app_id}} + arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) + arc56_app_spec.networks = {"localnet": Network(app_id=testing_app_arc32_app_id)} app_client = AppClient.from_network( algorand=algorand, app_spec=arc56_app_spec, @@ -713,13 +710,14 @@ def test_abi_with_default_arg_method( AppClientMethodCallWithSendParams(method=method_signature, args=[defined_value]) ) - assert defined_value_result.return_value - assert defined_value_result.return_value.return_value == "Local state, defined value" + assert defined_value_result.abi_return + assert defined_value_result.abi_return.value == "Local state, defined value" # Test with default value default_value_result = app_client.send.call(AppClientMethodCallWithSendParams(method=method_signature, args=[None])) - assert default_value_result.return_value - assert default_value_result.return_value.return_value == "Local state, banana" + assert default_value_result + assert default_value_result.abi_return + assert default_value_result.abi_return.value == "Local state, banana" def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 8cf9e75a..d186e321 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -5,7 +5,6 @@ from algosdk.logic import get_application_address from algosdk.transaction import OnComplete -from algokit_utils import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_client import ( AppClient, AppClientMethodCallParams, @@ -13,6 +12,7 @@ AppClientMethodCallWithSendParams, AppClientParams, ) +from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_factory import ( AppFactory, AppFactoryCreateMethodCallParams, @@ -43,7 +43,7 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture def app_spec() -> str: - return (Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json").read_text() + return (Path(__file__).parent.parent / "artifacts" / "testing_app" / "app_spec.arc32.json").read_text() @pytest.fixture @@ -59,7 +59,7 @@ def arc56_factory( ) -> AppFactory: """Create AppFactory fixture""" arc56_raw_spec = ( - Path(__file__).parent.parent / "artifacts" / "testing_app_arc56" / "arc56_app_spec.json" + Path(__file__).parent.parent / "artifacts" / "testing_app_arc56" / "app_spec.arc56.json" ).read_text() return algorand.client.get_app_factory(app_spec=arc56_raw_spec, default_sender=funded_account.address) @@ -146,67 +146,79 @@ def test_deploy_when_immutable_and_permanent(factory: AppFactory) -> None: def test_deploy_app_create(factory: AppFactory) -> None: - app_client, result = factory.deploy( + app_client, deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, ) - assert result.operation_performed == OperationPerformed.Create - assert result.app_id > 0 - assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert deploy_result.operation_performed == OperationPerformed.Create + assert deploy_result.create_response + assert deploy_result.create_response.app_id > 0 + assert app_client.app_id == deploy_result.create_response.app_id assert app_client.app_address == get_application_address(app_client.app_id) def test_deploy_app_create_abi(factory: AppFactory) -> None: - app_client, result = factory.deploy( + app_client, deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), ) - assert result.operation_performed == OperationPerformed.Create - assert result.app_id > 0 - assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert deploy_result.operation_performed == OperationPerformed.Create + create_result = deploy_result.create_response + assert create_result is not None + assert deploy_result.app.app_id > 0 + app_index = create_result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_id == deploy_result.app.app_id == app_index assert app_client.app_address == get_application_address(app_client.app_id) def test_deploy_app_update(factory: AppFactory) -> None: - _, created_app = factory.deploy( + app_client, create_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, updatable=True, ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_response - _, updated_app = factory.deploy( + updated_app_client, update_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 2, }, on_update=OnUpdate.UpdateApp, ) + assert update_deploy_result.operation_performed == OperationPerformed.Update + assert update_deploy_result.update_response - assert updated_app.operation_performed == OperationPerformed.Update - assert created_app.app_id == updated_app.app_id - assert created_app.app_address == updated_app.app_address - assert created_app.confirmation - assert created_app.updatable - assert created_app.updatable == updated_app.updatable - assert created_app.updated_round != updated_app.updated_round - assert created_app.created_round == updated_app.created_round - assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] + assert create_deploy_result.app.app_id == update_deploy_result.app.app_id + assert create_deploy_result.app.app_address == update_deploy_result.app.app_address + assert create_deploy_result.create_response.confirmation + assert create_deploy_result.app.updatable + assert create_deploy_result.app.updatable == update_deploy_result.app.updatable + assert create_deploy_result.app.updated_round != update_deploy_result.app.updated_round + assert create_deploy_result.app.created_round == update_deploy_result.app.created_round + assert update_deploy_result.update_response.confirmation + confirmed_round = update_deploy_result.update_response.confirmation["confirmed-round"] # type: ignore[call-overload] + assert update_deploy_result.app.updated_round == confirmed_round def test_deploy_app_update_abi(factory: AppFactory) -> None: - _, created_app = factory.deploy( + _, create_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, updatable=True, ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_response + created_app = create_deploy_result.create_response - _, updated_app = factory.deploy( + _, update_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 2, }, @@ -214,47 +226,63 @@ def test_deploy_app_update_abi(factory: AppFactory) -> None: update_params=AppClientMethodCallParams(method="update_abi", args=["args_io"]), ) - assert updated_app.operation_performed == OperationPerformed.Update - assert updated_app.app_id == created_app.app_id - assert updated_app.app_address == created_app.app_address - assert updated_app.confirmation is not None - assert updated_app.created_round == created_app.created_round - assert updated_app.updated_round != updated_app.created_round - assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] - assert updated_app.transaction.application_call - assert updated_app.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC - assert updated_app.return_value == "args_io" + assert update_deploy_result.operation_performed == OperationPerformed.Update + assert update_deploy_result.update_response + assert update_deploy_result.app.app_id == created_app.app_id + assert update_deploy_result.app.app_address == created_app.app_address + assert update_deploy_result.update_response.confirmation is not None + assert update_deploy_result.app.created_round == create_deploy_result.app.created_round + assert update_deploy_result.app.updated_round != update_deploy_result.app.created_round + assert ( + update_deploy_result.app.updated_round == update_deploy_result.update_response.confirmation["confirmed-round"] # type: ignore[call-overload] + ) + assert update_deploy_result.update_response.transaction.application_call + assert ( + update_deploy_result.update_response.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC + ) + assert update_deploy_result.update_response.abi_return + assert update_deploy_result.update_response.abi_value == "args_io" def test_deploy_app_replace(factory: AppFactory) -> None: - _, created_app = factory.deploy( + _, create_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, deletable=True, ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_response - _, replaced_app = factory.deploy( + _, replace_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 2, }, on_update=OnUpdate.ReplaceApp, ) - assert replaced_app.operation_performed == OperationPerformed.Replace - assert replaced_app.app_id > created_app.app_id - assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) - assert replaced_app.confirmation is not None - assert replaced_app.delete_result is not None - assert replaced_app.delete_result.confirmation is not None - assert len(replaced_app.transactions) == 2 - assert replaced_app.delete_result.transaction.application_call - assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id - assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + assert replace_deploy_result.operation_performed == OperationPerformed.Replace + assert replace_deploy_result.app.app_id > create_deploy_result.app.app_id + assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address( + replace_deploy_result.app.app_id + ) + assert replace_deploy_result.create_response is not None + assert replace_deploy_result.delete_response is not None + assert replace_deploy_result.delete_response.confirmation is not None + assert ( + len(replace_deploy_result.create_response.transactions) + + len(replace_deploy_result.delete_response.transactions) + == 2 + ) + assert replace_deploy_result.delete_response.transaction.application_call + assert replace_deploy_result.delete_response.transaction.application_call.index == create_deploy_result.app.app_id + assert ( + replace_deploy_result.delete_response.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + ) def test_deploy_app_replace_abi(factory: AppFactory) -> None: - _, created_app = factory.deploy( + _, create_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 1, }, @@ -262,7 +290,7 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: populate_app_call_resources=False, ) - _, replaced_app = factory.deploy( + replaced_app_client, replace_deploy_result = factory.deploy( deploy_time_params={ "VALUE": 2, }, @@ -271,18 +299,26 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: delete_params=AppClientMethodCallParams(method="delete_abi", args=["arg2_io"]), ) - assert replaced_app.operation_performed == OperationPerformed.Replace - assert replaced_app.app_id > created_app.app_id - assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) - assert replaced_app.confirmation is not None - assert replaced_app.delete_result is not None - assert replaced_app.delete_result.confirmation is not None - assert len(replaced_app.transactions) == 2 - assert replaced_app.delete_result.transaction.application_call - assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id - assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC - assert replaced_app.return_value == "arg_io" - assert replaced_app.delete_return_value == "arg2_io" + assert replace_deploy_result.operation_performed == OperationPerformed.Replace + assert replace_deploy_result.app.app_id > create_deploy_result.app.app_id + assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address(replaced_app_client.app_id) + assert replace_deploy_result.create_response is not None + assert replace_deploy_result.delete_response is not None + assert replace_deploy_result.delete_response.confirmation is not None + assert ( + len(replace_deploy_result.create_response.transactions) + + len(replace_deploy_result.delete_response.transactions) + == 2 + ) + assert replace_deploy_result.delete_response.transaction.application_call + assert replace_deploy_result.delete_response.transaction.application_call.index == create_deploy_result.app.app_id + assert ( + replace_deploy_result.delete_response.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + ) + assert replace_deploy_result.create_response.abi_return + assert replace_deploy_result.create_response.abi_value == "arg_io" + assert replace_deploy_result.delete_response.abi_return + assert replace_deploy_result.delete_response.abi_value == "arg2_io" def test_create_then_call_app(factory: AppFactory) -> None: @@ -298,8 +334,8 @@ def test_create_then_call_app(factory: AppFactory) -> None: call = app_client.send.call(AppClientMethodCallWithSendParams(method="call_abi", args=["test"])) - assert call.return_value - assert call.return_value.return_value == "Hello, test" + assert call.abi_return + assert call.abi_return.value == "Hello, test" def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, factory: AppFactory) -> None: @@ -337,9 +373,8 @@ def test_create_app_with_abi(factory: AppFactory) -> None: ) ) - assert call_return.return_value - # Fix return value issues - assert call_return.return_value.return_value == "string_io" + assert call_return.abi_return + assert call_return.abi_return == "string_io" def test_update_app_with_abi(factory: AppFactory) -> None: @@ -362,10 +397,9 @@ def test_update_app_with_abi(factory: AppFactory) -> None: ) ) - assert call_return.return_value is not None - assert call_return.return_value.return_value == "string_io" - # TODO: fix this - # assert call_return.compiled_approval is not None + assert call_return.abi_return + assert call_return.abi_return.value == "string_io" + # assert call_return.compiled_approval is not None # TODO: centralize approval/clear compilation def test_delete_app_with_abi(factory: AppFactory) -> None: @@ -386,8 +420,8 @@ def test_delete_app_with_abi(factory: AppFactory) -> None: ) ) - assert call_return.return_value is not None - assert call_return.return_value.return_value == "string_io" + assert call_return.abi_return + assert call_return.abi_return.value == "string_io" def test_export_import_sourcemaps( @@ -396,17 +430,17 @@ def test_export_import_sourcemaps( funded_account: Account, ) -> None: # Export source maps from original client - client, app = factory.deploy(deploy_time_params={"VALUE": 1}) - old_sourcemaps = client.export_source_maps() + app_client, _ = factory.deploy(deploy_time_params={"VALUE": 1}) + old_sourcemaps = app_client.export_source_maps() # Create new client instance new_client = AppClient( AppClientParams( - app_id=app.app_id, + app_id=app_client.app_id, default_sender=funded_account.address, default_signer=funded_account.signer, algorand=algorand, - app_spec=client.app_spec, + app_spec=app_client.app_spec, ) ) @@ -436,7 +470,7 @@ def test_export_import_sourcemaps( def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( arc56_factory: AppFactory, ) -> None: - client, _ = arc56_factory.deploy( + app_client, _ = arc56_factory.deploy( create_params=AppClientMethodCallParams(method="createApplication"), deploy_time_params={ "bytes64TmplVar": "0" * 64, @@ -447,7 +481,7 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( ) with pytest.raises(Exception, match="this is an error"): - client.send.call(AppClientMethodCallWithSendParams(method="throwError")) + app_client.send.call(AppClientMethodCallWithSendParams(method="throwError")) def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( @@ -456,7 +490,7 @@ def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( funded_account: Account, ) -> None: # Deploy app with template parameters - client, result = arc56_factory.deploy( + app_client, _ = arc56_factory.deploy( create_params=AppClientMethodCallParams(method="createApplication"), deploy_time_params={ "bytes64TmplVar": "0" * 64, @@ -465,7 +499,7 @@ def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( "bytesTmplVar": "foo", }, ) - app_id = result.app_id + app_id = app_client.app_id # Create new client without source map from compilation app_client = AppClient( @@ -474,7 +508,7 @@ def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( default_sender=funded_account.address, default_signer=funded_account.signer, algorand=algorand, - app_spec=client.app_spec, + app_spec=app_client.app_spec, ) ) diff --git a/tests/applications/test_arc56.py b/tests/applications/test_arc56.py new file mode 100644 index 00000000..f5b6c2dc --- /dev/null +++ b/tests/applications/test_arc56.py @@ -0,0 +1,45 @@ +import json +from pathlib import Path + +from algokit_utils.applications.app_spec.arc56 import Arc56Contract +from tests.conftest import check_output_stability +from tests.utils import load_app_spec + +TEST_ARC32_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" +TEST_ARC56_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "amm_arc56_example" / "amm.arc56.json" + + +def test_arc56_from_arc32_json() -> None: + arc56_app_spec = Arc56Contract.from_arc32(TEST_ARC32_SPEC_FILE_PATH.read_text()) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json()) + + +def test_arc56_from_arc32_instance() -> None: + arc32_app_spec = load_app_spec( + TEST_ARC32_SPEC_FILE_PATH, arc=32, deletable=True, updatable=True, template_values={"VERSION": 1} + ) + + arc56_app_spec = Arc56Contract.from_arc32(arc32_app_spec) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json()) + + +def test_arc56_from_json() -> None: + arc56_app_spec = Arc56Contract.from_json(TEST_ARC56_SPEC_FILE_PATH.read_text()) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json()) + + +def test_arc56_from_dict() -> None: + arc56_app_spec = Arc56Contract.from_dict(json.loads(TEST_ARC56_SPEC_FILE_PATH.read_text())) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json()) diff --git a/tests/applications/test_utils.py b/tests/applications/test_utils.py deleted file mode 100644 index 4806216c..00000000 --- a/tests/applications/test_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path - -from algokit_utils.applications.utils import arc32_to_arc56 -from tests.utils import load_arc32_spec - -TEST_ARC32_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" - - -def test_arc32_to_arc56() -> None: - arc32_app_spec = load_arc32_spec( - TEST_ARC32_SPEC_FILE_PATH, deletable=True, updatable=True, template_values={"VERSION": 1} - ) - - arc56_app_spec = arc32_to_arc56(arc32_app_spec) - - assert arc56_app_spec diff --git a/tests/artifacts/amm_arc56_example/amm.arc56.json b/tests/artifacts/amm_arc56_example/amm.arc56.json new file mode 100644 index 00000000..42a8e366 --- /dev/null +++ b/tests/artifacts/amm_arc56_example/amm.arc56.json @@ -0,0 +1,510 @@ +{ + "name": "ConstantProductAMM", + "structs": {}, + "methods": [ + { + "name": "set_governor", + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "recommendations": {} + }, + { + "name": "bootstrap", + "args": [ + { + "type": "pay", + "name": "seed", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token." + }, + { + "type": "asset", + "name": "a_asset", + "desc": "One of the two assets this pool should allow swapping between." + }, + { + "type": "asset", + "name": "b_asset", + "desc": "The other of the two assets this pool should allow swapping between." + } + ], + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "recommendations": {} + }, + { + "name": "mint", + "args": [ + { + "type": "axfer", + "name": "a_xfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens." + }, + { + "type": "axfer", + "name": "b_xfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens." + }, + { + "type": "asset", + "name": "pool_asset", + "desc": "The asset ID of the pool token so that we may distribute it.", + "defaultValue": { + "source": "global", + "data": "cG9vbF90b2tlbg==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "a_asset", + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "recommendations": {} + }, + { + "name": "burn", + "args": [ + { + "type": "axfer", + "name": "pool_xfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem" + }, + { + "type": "asset", + "name": "pool_asset", + "desc": "Asset ID of the pool token so we may inspect balance.", + "defaultValue": { + "source": "global", + "data": "cG9vbF90b2tlbg==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "a_asset", + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "recommendations": {} + }, + { + "name": "swap", + "args": [ + { + "type": "axfer", + "name": "swap_xfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B" + }, + { + "type": "asset", + "name": "a_asset", + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 4, + "bytes": 1 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": { + "asset_a": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "YXNzZXRfYQ==" + }, + "asset_b": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "YXNzZXRfYg==" + }, + "governor": { + "keyType": "AVMString", + "valueType": "AVMBytes", + "key": "Z292ZXJub3I=" + }, + "pool_token": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "cG9vbF90b2tlbg==" + }, + "ratio": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "cmF0aW8=" + } + }, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/artifacts/hello_world/arc32_app_spec.json b/tests/artifacts/hello_world/app_spec.arc32.json similarity index 100% rename from tests/artifacts/hello_world/arc32_app_spec.json rename to tests/artifacts/hello_world/app_spec.arc32.json diff --git a/tests/artifacts/legacy_app_client_test/app_client_test.json b/tests/artifacts/legacy_app_client_test/app_client_test.json new file mode 100644 index 00000000..c85999d5 --- /dev/null +++ b/tests/artifacts/legacy_app_client_test/app_client_test.json @@ -0,0 +1,378 @@ +{ + "hints": { + "version()uint64": { + "call_config": { + "no_op": "CALL" + } + }, + "readonly(uint64)void": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "set_box(byte[4],string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box(byte[4])string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box_readonly(byte[4])string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "update()void": { + "call_config": { + "update_application": "CALL" + } + }, + "update_args(string)void": { + "call_config": { + "update_application": "CALL" + } + }, + "delete()void": { + "call_config": { + "delete_application": "CALL" + } + }, + "delete_args(string)void": { + "call_config": { + "delete_application": "CALL" + } + }, + "create_opt_in()void": { + "call_config": { + "opt_in": "CREATE" + } + }, + "update_greeting(string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "create()void": { + "call_config": { + "no_op": "CREATE" + } + }, + "create_args(string)void": { + "call_config": { + "no_op": "CREATE" + } + }, + "hello(string)string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "hello_remember(string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_last()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "opt_in_args(string)void": { + "call_config": { + "opt_in": "CALL" + } + }, + "close_out()void": { + "call_config": { + "close_out": "CALL" + } + }, + "close_out_args(string)void": { + "call_config": { + "close_out": "CALL" + } + }, + "call_with_payment(pay)string": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4=" + }, + "state": { + "global": { + "num_byte_slices": 1, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": { + "greeting": { + "type": "bytes", + "key": "greeting", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "last": { + "type": "bytes", + "key": "last", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "HelloWorldApp", + "methods": [ + { + "name": "version", + "args": [], + "returns": { + "type": "uint64" + } + }, + { + "name": "readonly", + "args": [ + { + "type": "uint64", + "name": "error" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "get_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_box_readonly", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "update", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "delete", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "delete_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create_opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_greeting", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "create_args", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "hello", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "hello_remember", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_last", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "opt_in_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "close_out", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "close_out_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "call_with_payment", + "args": [ + { + "type": "pay", + "name": "payment" + } + ], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "close_out": "CALL", + "delete_application": "CALL", + "no_op": "CREATE", + "opt_in": "CALL", + "update_application": "CALL" + } +} \ No newline at end of file diff --git a/tests/artifacts/legacy_app_client_test/app_client_test.py b/tests/artifacts/legacy_app_client_test/app_client_test.py new file mode 100644 index 00000000..34e04358 --- /dev/null +++ b/tests/artifacts/legacy_app_client_test/app_client_test.py @@ -0,0 +1,199 @@ +from typing import Literal + +import beaker +import pyteal +from beaker.lib.storage import BoxMapping + + +class State: + greeting = beaker.GlobalStateValue(pyteal.TealType.bytes) + last = beaker.LocalStateValue(pyteal.TealType.bytes, default=pyteal.Bytes("unset")) + box = BoxMapping(pyteal.abi.StaticBytes[Literal[4]], pyteal.abi.String) + + +app = beaker.Application("HelloWorldApp", state=State()) + + +@app.external +def version(*, output: pyteal.abi.Uint64) -> pyteal.Expr: + return output.set(pyteal.Tmpl.Int("TMPL_VERSION")) + + +@app.external(read_only=True) +def readonly(error: pyteal.abi.Uint64) -> pyteal.Expr: + return pyteal.If(error.get(), pyteal.Assert(pyteal.Int(0), comment="An error"), pyteal.Approve()) + + +@app.external() +def set_box(name: pyteal.abi.StaticBytes[Literal[4]], value: pyteal.abi.String) -> pyteal.Expr: + return app.state.box[name.get()].set(value.get()) + + +@app.external +def get_box(name: pyteal.abi.StaticBytes[Literal[4]], *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(app.state.box[name.get()].get()) + + +@app.external(read_only=True) +def get_box_readonly(name: pyteal.abi.StaticBytes[Literal[4]], *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(app.state.box[name.get()].get()) + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated ABI")), + ) + + +@app.update(bare=True, authorize=beaker.Authorize.only_creator()) +def update_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated Bare")), + ) + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes update check"), + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated Args")), + ) + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.delete(bare=True, authorize=beaker.Authorize.only_creator()) +def delete_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes delete check"), + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.external(method_config={"opt_in": pyteal.CallConfig.CREATE}) +def create_opt_in() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Opt In")), + pyteal.Approve(), + ) + + +@app.external +def update_greeting(greeting: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(greeting.get()), + ) + + +@app.create(bare=True) +def create_bare() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Hello Bare")), + pyteal.Approve(), + ) + + +@app.create +def create() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Hello ABI")), + pyteal.Approve(), + ) + + +@app.create +def create_args(greeting: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(greeting.get()), + pyteal.Approve(), + ) + + +@app.external(read_only=True) +def hello(name: pyteal.abi.String, *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(pyteal.Concat(app.state.greeting, pyteal.Bytes(", "), name.get())) + + +@app.external +def hello_remember(name: pyteal.abi.String, *, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(name.get()), output.set(pyteal.Concat(app.state.greeting, pyteal.Bytes(", "), name.get())) + ) + + +@app.external(read_only=True) +def get_last(*, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq(output.set(app.state.last.get())) + + +@app.clear_state +def clear_state() -> pyteal.Expr: + return pyteal.Approve() + + +@app.opt_in +def opt_in() -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(pyteal.Bytes("Opt In ABI")), + pyteal.Approve(), + ) + + +@app.opt_in(bare=True) +def opt_in_bare() -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(pyteal.Bytes("Opt In Bare")), + pyteal.Approve(), + ) + + +@app.opt_in +def opt_in_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes opt_in check"), + app.state.last.set(pyteal.Bytes("Opt In Args")), + pyteal.Approve(), + ) + + +@app.close_out +def close_out() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Approve(), + ) + + +@app.close_out(bare=True) +def close_out_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Approve(), + ) + + +@app.close_out +def close_out_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes close_out check"), + pyteal.Approve(), + ) + + +@app.external +def call_with_payment(payment: pyteal.abi.PaymentTransaction, *, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq(pyteal.Assert(payment.get().amount() > pyteal.Int(0)), output.set("Payment Successful")) diff --git a/tests/artifacts/legacy_hello_world/arc32_app_spec.json b/tests/artifacts/legacy_hello_world/app_spec.arc32.json similarity index 100% rename from tests/artifacts/legacy_hello_world/arc32_app_spec.json rename to tests/artifacts/legacy_hello_world/app_spec.arc32.json diff --git a/tests/artifacts/testing_app/arc32_app_spec.json b/tests/artifacts/testing_app/app_spec.arc32.json similarity index 100% rename from tests/artifacts/testing_app/arc32_app_spec.json rename to tests/artifacts/testing_app/app_spec.arc32.json diff --git a/tests/artifacts/testing_app_arc56/arc56_app_spec.json b/tests/artifacts/testing_app_arc56/app_spec.arc56.json similarity index 100% rename from tests/artifacts/testing_app_arc56/arc56_app_spec.json rename to tests/artifacts/testing_app_arc56/app_spec.arc56.json diff --git a/tests/artifacts/testing_app_puya/arc32_app_spec.json b/tests/artifacts/testing_app_puya/app_spec.arc32.json similarity index 100% rename from tests/artifacts/testing_app_puya/arc32_app_spec.json rename to tests/artifacts/testing_app_puya/app_spec.arc32.json diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py index a7637f58..77c56b29 100644 --- a/tests/clients/algorand_client/test_transfer.py +++ b/tests/clients/algorand_client/test_transfer.py @@ -48,7 +48,7 @@ def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_a assert result.transaction.payment.amt == 5_000_000 assert result.transaction.payment.sender == funded_account.address == result.confirmation["txn"]["txn"]["snd"] # type: ignore # noqa: PGH003 - assert account_info["amount"] == 5_000_000 + assert account_info.amount == 5_000_000 def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_account: Account) -> None: @@ -314,9 +314,7 @@ def test_ensure_funded(algorand: AlgorandClient, funded_account: Account) -> Non assert response is not None to_account_info = algorand.account.get_information(test_account) - assert isinstance(to_account_info, dict) - actual_amount = to_account_info.get("amount") - assert actual_amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + assert to_account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) def test_ensure_funded_uses_dispenser_by_default( @@ -336,7 +334,7 @@ def test_ensure_funded_uses_dispenser_by_default( assert result.transaction.payment.sender == dispenser.address account_info = algorand.account.get_information(second_account) - assert account_info["amount"] == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + assert account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClient, funded_account: Account) -> None: @@ -350,9 +348,7 @@ def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClie assert response is not None to_account_info = algorand.account.get_information(test_account) - assert isinstance(to_account_info, dict) - actual_amount = to_account_info.get("amount") - assert actual_amount == AlgoAmount.from_algos(1) + assert to_account_info.amount == AlgoAmount.from_algos(1) def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 9499465e..c2eb036a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,6 @@ from dotenv import load_dotenv from algokit_utils import ( - DELETABLE_TEMPLATE_NAME, - UPDATABLE_TEMPLATE_NAME, Account, ApplicationClient, ApplicationSpecification, @@ -19,6 +17,7 @@ ensure_funded, replace_template_variables, ) +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.transactions.transaction_composer import AssetCreateParams @@ -39,7 +38,7 @@ def check_output_stability(logs: str, *, test_name: str | None = None) -> None: caller_dir = caller_path.parent test_name = test_name or caller_frame.function caller_stem = Path(caller_frame.filename).stem - output_dir = caller_dir / "snapshots" / f"{caller_stem}.approvals" + output_dir = caller_dir / "_snapshots" / f"{caller_stem}.approvals" output_dir.mkdir(exist_ok=True) output_file = output_dir / f"{test_name}.approved.txt" output_file_str = str(output_file) diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py new file mode 100644 index 00000000..8bf4b085 --- /dev/null +++ b/tests/test_debug_utils.py @@ -0,0 +1,209 @@ +import json +from collections.abc import Generator +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + +from algokit_utils._debugging import ( + AVMDebuggerSourceMap, + PersistSourceMapInput, + persist_sourcemaps, + simulate_and_persist_response, +) +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallWithSendParams, +) +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.common import Program +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from tests.conftest import check_output_stability + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_micro_algos(1_000_000), + min_funding_increment=AlgoAmount.from_micro_algos(1_000_000), + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def client_fixture(algorand: AlgorandClient, funded_account: Account) -> AppClient: + app_spec = (Path(__file__).parent / "artifacts" / "legacy_app_client_test" / "app_client_test.json").read_text() + app_factory = algorand.client.get_app_factory( + app_spec=app_spec, default_sender=funded_account.address, default_signer=funded_account.signer + ) + app_client, _ = app_factory.send.create( + AppFactoryCreateMethodCallParams( + method="create", deletable=True, updatable=True, deploy_time_params={"VERSION": 1} + ) + ) + return app_client + + +@pytest.fixture +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.transactions.transaction_composer.config", new_callable=Mock) as mock_config: + mock_config.debug = True + mock_config.project_root = None + yield mock_config + + +def test_build_teal_sourcemaps(algorand: AlgorandClient, tmp_path_factory: pytest.TempPathFactory) -> None: + cwd = tmp_path_factory.mktemp("cwd") + + approval = """ +#pragma version 9 +int 1 +""" + clear = """ +#pragma version 9 +int 1 +""" + sources = [ + PersistSourceMapInput(raw_teal=approval, app_name="cool_app", file_name="approval.teal"), + PersistSourceMapInput(raw_teal=clear, app_name="cool_app", file_name="clear"), + ] + + persist_sourcemaps(sources=sources, project_root=cwd, client=algorand.client.algod) + + root_path = cwd / ".algokit" / "sources" + sourcemap_file_path = root_path / "sources.avm.json" + app_output_path = root_path / "cool_app" + + assert (sourcemap_file_path).exists() + assert (app_output_path / "approval.teal").exists() + assert (app_output_path / "approval.teal.tok.map").exists() + assert (app_output_path / "clear.teal").exists() + assert (app_output_path / "clear.teal.tok.map").exists() + + result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) + for item in result.txn_group_sources: + item.location = "dummy" + + check_output_stability(json.dumps(result.to_dict())) + + # check for updates in case of multiple runs + persist_sourcemaps(sources=sources, project_root=cwd, client=algorand.client.algod) + result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) + for item in result.txn_group_sources: + assert item.location != "dummy" + + +def test_build_teal_sourcemaps_without_sources( + algorand: AlgorandClient, tmp_path_factory: pytest.TempPathFactory +) -> None: + cwd = tmp_path_factory.mktemp("cwd") + + approval = """ +#pragma version 9 +int 1 +""" + clear = """ +#pragma version 9 +int 1 +""" + compiled_approval = Program(approval, algorand.client.algod) + compiled_clear = Program(clear, algorand.client.algod) + sources = [ + PersistSourceMapInput(compiled_teal=compiled_approval, app_name="cool_app", file_name="approval.teal"), + PersistSourceMapInput(compiled_teal=compiled_clear, app_name="cool_app", file_name="clear"), + ] + + persist_sourcemaps(sources=sources, project_root=cwd, client=algorand.client.algod, with_sources=False) + + root_path = cwd / ".algokit" / "sources" + sourcemap_file_path = root_path / "sources.avm.json" + app_output_path = root_path / "cool_app" + + assert (sourcemap_file_path).exists() + assert not (app_output_path / "approval.teal").exists() + assert (app_output_path / "approval.teal.tok.map").exists() + assert json.loads((app_output_path / "approval.teal.tok.map").read_text())["sources"] == [] + assert not (app_output_path / "clear.teal").exists() + assert (app_output_path / "clear.teal.tok.map").exists() + assert json.loads((app_output_path / "clear.teal.tok.map").read_text())["sources"] == [] + + result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) + for item in result.txn_group_sources: + item.location = "dummy" + check_output_stability(json.dumps(result.to_dict())) + + +def test_simulate_and_persist_response_via_app_call( + tmp_path_factory: pytest.TempPathFactory, + client_fixture: AppClient, + mock_config: Mock, +) -> None: + mock_config.debug = True + mock_config.trace_all = True + mock_config.trace_buffer_size_mb = 256 + cwd = tmp_path_factory.mktemp("cwd") + mock_config.project_root = cwd + + client_fixture.send.call(AppClientMethodCallWithSendParams(method="hello", args=["test"])) + + output_path = cwd / "debug_traces" + + content = list(output_path.iterdir()) + assert len(list(output_path.iterdir())) == 1 + trace_file_content = json.loads(content[0].read_text()) + simulated_txn = trace_file_content["txn-groups"][0]["txn-results"][0]["txn-result"]["txn"]["txn"] + assert simulated_txn["type"] == "appl" + assert simulated_txn["apid"] == client_fixture.app_id + + +def test_simulate_and_persist_response( + tmp_path_factory: pytest.TempPathFactory, algorand: AlgorandClient, mock_config: Mock, funded_account: Account +) -> None: + mock_config.debug = True + mock_config.trace_all = True + cwd = tmp_path_factory.mktemp("cwd") + mock_config.project_root = cwd + algod = algorand.client.algod + + payment = PaymentTxn( + sender=funded_account.address, + receiver=funded_account.address, + amt=1_000_000, + note=b"Payment", + sp=algod.suggested_params(), + ) + txn_with_signer = TransactionWithSigner(payment, AccountTransactionSigner(funded_account.private_key)) + atc = AtomicTransactionComposer() + atc.add_transaction(txn_with_signer) + + simulate_and_persist_response(atc, cwd, algod) + + output_path = cwd / "debug_traces" + content = list(output_path.iterdir()) + assert len(list(output_path.iterdir())) == 1 + trace_file_content = json.loads(content[0].read_text()) + simulated_txn = trace_file_content["txn-groups"][0]["txn-results"][0]["txn-result"]["txn"]["txn"] + assert simulated_txn["type"] == "pay" + + trace_file_path = content[0] + while trace_file_path.exists(): + tmp_atc = atc.clone() + simulate_and_persist_response(tmp_atc, cwd, algod, buffer_size_mb=0.003) diff --git a/tests/transactions/test_abi_return.py b/tests/transactions/test_abi_return.py new file mode 100644 index 00000000..dc3d50f5 --- /dev/null +++ b/tests/transactions/test_abi_return.py @@ -0,0 +1,105 @@ +from algosdk.abi import ABIType, Method +from algosdk.abi.method import Returns +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils.applications.abi import ABIReturn, ABIValue + + +def get_abi_result(type_str: str, value: ABIValue) -> ABIReturn: + """Helper function to simulate ABI method return value""" + abi_type = ABIType.from_string(type_str) + encoded = abi_type.encode(value) + decoded = abi_type.decode(encoded) + result = ABIResult( + method=Method(name="", args=[], returns=Returns(arg_type=type_str)), + raw_value=encoded, + return_value=decoded, + tx_id="", + tx_info={}, + decode_error=None, + ) + + return ABIReturn(result) + + +class TestABIReturn: + def test_uint32(self) -> None: + assert get_abi_result("uint32", 0).value == 0 + assert get_abi_result("uint32", 0).value == 0 + assert get_abi_result("uint32", 1).value == 1 + assert get_abi_result("uint32", 1).value == 1 + assert get_abi_result("uint32", 2**32 - 1).value == 2**32 - 1 + assert get_abi_result("uint32", 2**32 - 1).value == 2**32 - 1 + + def test_uint64(self) -> None: + assert get_abi_result("uint64", 0).value == 0 + assert get_abi_result("uint64", 1).value == 1 + assert get_abi_result("uint64", 2**32 - 1).value == 2**32 - 1 + assert get_abi_result("uint64", 2**64 - 1).value == 2**64 - 1 + + def test_uint32_array(self) -> None: + assert get_abi_result("uint32[]", [0]).value == [0] + assert get_abi_result("uint32[]", [0]).value == [0] + assert get_abi_result("uint32[]", [1]).value == [1] + assert get_abi_result("uint32[]", [1]).value == [1] + assert get_abi_result("uint32[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint32[]", [2**32 - 1, 1]).value == [2**32 - 1, 1] + + def test_uint32_fixed_array(self) -> None: + assert get_abi_result("uint32[1]", [0]).value == [0] + assert get_abi_result("uint32[1]", [0]).value == [0] + assert get_abi_result("uint32[1]", [1]).value == [1] + assert get_abi_result("uint32[1]", [1]).value == [1] + assert get_abi_result("uint32[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[1]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint32[2]", [2**32 - 1, 1]).value == [2**32 - 1, 1] + + def test_uint64_array(self) -> None: + assert get_abi_result("uint64[]", [0]).value == [0] + assert get_abi_result("uint64[]", [0]).value == [0] + assert get_abi_result("uint64[]", [1]).value == [1] + assert get_abi_result("uint64[]", [1]).value == [1] + assert get_abi_result("uint64[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint64[]", [2**64 - 1, 1]).value == [2**64 - 1, 1] + + def test_uint64_fixed_array(self) -> None: + assert get_abi_result("uint64[1]", [0]).value == [0] + assert get_abi_result("uint64[1]", [0]).value == [0] + assert get_abi_result("uint64[1]", [1]).value == [1] + assert get_abi_result("uint64[1]", [1]).value == [1] + assert get_abi_result("uint64[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[1]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint64[2]", [2**64 - 1, 1]).value == [2**64 - 1, 1] + + def test_tuple(self) -> None: + type_str = "(uint32,uint64,(uint32,uint64),uint32[],uint64[])" + assert get_abi_result(type_str, [0, 0, [0, 0], [0], [0]]).value == [ + 0, + 0, + [0, 0], + [0], + [0], + ] + assert get_abi_result(type_str, [1, 1, [1, 1], [1], [1]]).value == [ + 1, + 1, + [1, 1], + [1], + [1], + ] + assert get_abi_result( + type_str, + [2**32 - 1, 2**64 - 1, [2**32 - 1, 2**64 - 1], [1, 2, 3], [1, 2, 3]], + ).value == [ + 2**32 - 1, + 2**64 - 1, + [2**32 - 1, 2**64 - 1], + [1, 2, 3], + [1, 2, 3], + ] diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 21bbbcfe..8452be08 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -18,7 +18,7 @@ from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCreateParams, AssetConfigParams, AssetCreateParams, @@ -29,7 +29,7 @@ from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: - from algokit_utils.transactions.models import Arc2TransactionNote + from algokit_utils.models.transaction import Arc2TransactionNote @pytest.fixture @@ -210,7 +210,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco get_signer=lambda _: funded_account.signer, ) composer.add_app_call_method_call( - AppCallMethodCall( + AppCallMethodCallParams( sender=funded_account.address, app_id=app_id, method=algosdk.abi.Method.from_signature("hello(string)string"), @@ -224,7 +224,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco txn = built.transactions[0] assert txn.sender == funded_account.address response = composer.send(max_rounds_to_wait=20) - assert response.returns[-1].return_value == "Hello, world" + assert response.returns[-1].value == "Hello, world" def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: @@ -294,6 +294,7 @@ def test_transaction_cap_is_ignored_if_higher_than_fee(algorand: AlgorandClient, response = algorand.send.payment( PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1_000_000)) ) + assert isinstance(response.confirmation, dict) assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_micro_algo(1000) @@ -301,6 +302,7 @@ def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account response = algorand.send.payment( PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algos(1)) ) + assert isinstance(response.confirmation, dict) assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algos(1) @@ -313,8 +315,10 @@ def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: Acc composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(2)))) response = composer.send() - assert response.confirmations[0]["txn"]["txn"]["grp"] is not None - assert response.confirmations[1]["txn"]["txn"]["grp"] is not None + assert isinstance(response.confirmations[0], dict) + assert isinstance(response.confirmations[1], dict) + assert response.confirmations[0].get("txn", {}).get("txn", {}).get("grp") is not None + assert response.confirmations[1].get("txn", {}).get("txn", {}).get("grp") is not None assert response.transactions[0].payment.group is not None assert response.transactions[1].payment.group is not None assert len(response.confirmations) == 2 diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index e7cd9dcd..9bf2bbab 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -18,7 +18,7 @@ from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCreateParams, AssetConfigParams, AssetCreateParams, @@ -234,7 +234,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde # Then test creating a method call transaction result = algorand.create_transaction.app_call_method_call( - AppCallMethodCall( + AppCallMethodCallParams( sender=funded_account.address, app_id=app_id, method=algosdk.abi.Method.from_signature("hello(string)string"), diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 975ce5d2..1d1cf2a8 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -12,7 +12,7 @@ from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( - AppCallMethodCall, + AppCallMethodCallParams, AppCallParams, AppCreateParams, AssetConfigParams, @@ -62,13 +62,13 @@ def receiver(algorand: AlgorandClient) -> Account: @pytest.fixture def raw_hello_world_arc32_app_spec() -> str: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" return raw_json_spec.read_text() @pytest.fixture def test_hello_world_arc32_app_spec() -> ApplicationSpecification: - raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" return ApplicationSpecification.from_json(raw_json_spec.read_text()) @@ -408,13 +408,13 @@ def test_app_call( ) result = transaction_sender.app_call(params) - assert not result.return_value # TODO: improve checks + assert not result.abi_return # TODO: improve checks def test_app_call_method_call( test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account ) -> None: - params = AppCallMethodCall( + params = AppCallMethodCallParams( app_id=test_hello_world_arc32_app_id, sender=sender.address, method=algosdk.abi.Method.from_signature("hello(string)string"), @@ -422,8 +422,8 @@ def test_app_call_method_call( ) result = transaction_sender.app_call_method_call(params) - assert result.return_value - assert result.return_value.return_value == "Hello2, test" + assert result.abi_return + assert result.abi_return.value == "Hello2, test" @patch("logging.Logger.debug") diff --git a/tests/utils.py b/tests/utils.py index 612ea60d..fc71eb75 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,18 +1,42 @@ from pathlib import Path +from typing import Literal, overload -from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils.applications.app_manager import AppManager -from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, AppManager +from algokit_utils.applications.app_spec import Arc32Contract, Arc56Contract -def load_arc32_spec( +@overload +def load_app_spec( path: Path, + arc: Literal[32], *, updatable: bool | None = None, deletable: bool | None = None, template_values: dict | None = None, -) -> ApplicationSpecification: - spec = ApplicationSpecification.from_json(path.read_text(encoding="utf-8")) +) -> Arc32Contract: ... + + +@overload +def load_app_spec( + path: Path, + arc: Literal[56], + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> Arc56Contract: ... + + +def load_app_spec( + path: Path, + arc: Literal[32, 56], + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> Arc32Contract | Arc56Contract: + arc_class = Arc32Contract if arc == 32 else Arc56Contract + spec = arc_class.from_json(path.read_text(encoding="utf-8")) template_variables = template_values or {} if updatable is not None: @@ -21,9 +45,10 @@ def load_arc32_spec( if deletable is not None: template_variables["DELETABLE"] = int(deletable) - spec.approval_program = ( - AppManager.replace_template_variables(spec.approval_program, template_variables) - .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") - .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") - ) + if isinstance(spec, Arc32Contract): + spec.approval_program = ( + AppManager.replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) return spec From 86280753db88fd99bc540e3405da104d6b09d961 Mon Sep 17 00:00:00 2001 From: Al Date: Mon, 23 Dec 2024 16:46:40 +0100 Subject: [PATCH 09/31] feat: add offline_key_reg transaction to sender/creator/composer abstractions (#129) --- src/algokit_utils/applications/app_client.py | 4 -- .../transactions/transaction_composer.py | 71 ++++++++++++++----- .../transactions/transaction_creator.py | 6 ++ .../transactions/transaction_sender.py | 10 +++ tests/transactions/test_transaction_sender.py | 14 +++- 5 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index a3c5e586..85ef23e9 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -63,7 +63,6 @@ from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams from algokit_utils.protocols.client import AlgorandClientProtocol - from algokit_utils.transactions.transaction_composer import TransactionComposer __all__ = [ "AppClient", @@ -1299,9 +1298,6 @@ def get_box_values_from_abi_type( # Return list of BoxABIValue objects return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] - def new_group(self) -> TransactionComposer: - return self._algorand.new_group() - def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: return self.send.fund_app_account(params) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index b3062d39..7f7597cb 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -49,6 +49,7 @@ "AssetOptInParams", "AssetOptOutParams", "AssetTransferParams", + "OfflineKeyRegistrationParams", "OnlineKeyRegistrationParams", "PaymentParams", "SendAtomicTransactionComposerResults", @@ -232,6 +233,15 @@ class OnlineKeyRegistrationParams( state_proof_key: bytes | None = None +@dataclass(kw_only=True, frozen=True) +class OfflineKeyRegistrationParams(_CommonTxnWithSendParams): + """ + Offline key registration parameters. + """ + + prevent_account_from_ever_participating_again: bool + + @dataclass(kw_only=True, frozen=True) class AssetTransferParams( _CommonTxnWithSendParams, @@ -306,7 +316,7 @@ class AppCallParams(_CommonTxnWithSendParams): app_references: list[int] | None = None asset_references: list[int] | None = None extra_pages: int | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None @dataclass(kw_only=True, frozen=True) @@ -336,7 +346,7 @@ class AppCreateParams(_CommonTxnWithSendParams): account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None extra_program_pages: int | None = None @@ -416,7 +426,7 @@ class AppMethodCallParams(_CommonTxnWithSendParams): account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None @dataclass(kw_only=True, frozen=True) @@ -513,6 +523,7 @@ class AppDeleteMethodCallParams(_BaseAppMethodCall): AppUpdateParams, AppDeleteParams, MethodCallParams, + OfflineKeyRegistrationParams, ] @@ -802,6 +813,10 @@ def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> Tr self._txns.append(params) return self + def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> TransactionComposer: + self._txns.append(params) + return self + def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer: self._txns.append(atc) return self @@ -1080,7 +1095,7 @@ def _build_method_call( # noqa: C901, PLR0912 txn = self._build_asset_freeze(arg, suggested_params) case AssetTransferParams(): txn = self._build_asset_transfer(arg, suggested_params) - case OnlineKeyRegistrationParams(): + case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams(): txn = self._build_key_reg(arg, suggested_params) case _: raise ValueError(f"Unsupported method arg transaction type: {arg!s}") @@ -1288,22 +1303,40 @@ def _build_asset_transfer( return self._common_txn_build_step(lambda x: algosdk.transaction.AssetTransferTxn(**x), params, txn_params) def _build_key_reg( - self, params: OnlineKeyRegistrationParams, suggested_params: algosdk.transaction.SuggestedParams + self, + params: OnlineKeyRegistrationParams | OfflineKeyRegistrationParams, + suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: - txn_params = { - "sender": params.sender, - "sp": suggested_params, - "votekey": params.vote_key, - "selkey": params.selection_key, - "votefst": params.vote_first, - "votelst": params.vote_last, - "votekd": params.vote_key_dilution, - "rekey_to": params.rekey_to, - "nonpart": False, - "sprfkey": params.state_proof_key, - } + if isinstance(params, OnlineKeyRegistrationParams): + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "votekey": params.vote_key, + "selkey": params.selection_key, + "votefst": params.vote_first, + "votelst": params.vote_last, + "votekd": params.vote_key_dilution, + "rekey_to": params.rekey_to, + "nonpart": False, + "sprfkey": params.state_proof_key, + } - return self._common_txn_build_step(lambda x: algosdk.transaction.KeyregTxn(**x), params, txn_params) + return self._common_txn_build_step(lambda x: algosdk.transaction.KeyregTxn(**x), params, txn_params) + + return self._common_txn_build_step( + lambda x: algosdk.transaction.KeyregTxn(**x), + params, + { + "sender": params.sender, + "sp": suggested_params, + "nonpart": params.prevent_account_from_ever_participating_again, + "votekey": None, + "selkey": None, + "votefst": None, + "votelst": None, + "votekd": None, + }, + ) def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool: if isinstance(x, list | tuple): @@ -1369,7 +1402,7 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 suggested_params, ) return [TransactionWithSigner(txn=asset_transfer, signer=signer)] - case OnlineKeyRegistrationParams(): + case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams(): key_reg = self._build_key_reg(txn, suggested_params) return [TransactionWithSigner(txn=key_reg, signer=signer)] case _: diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py index 7bd4899a..f0c5397f 100644 --- a/src/algokit_utils/transactions/transaction_creator.py +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -20,6 +20,7 @@ AssetOptOutParams, AssetTransferParams, BuiltTransactions, + OfflineKeyRegistrationParams, OnlineKeyRegistrationParams, PaymentParams, TransactionComposer, @@ -152,3 +153,8 @@ def app_call_method_call(self) -> Callable[[AppCallMethodCallParams], BuiltTrans def online_key_registration(self) -> Callable[[OnlineKeyRegistrationParams], Transaction]: """Create an online key registration transaction.""" return self._transaction(lambda c: c.add_online_key_registration) + + @property + def offline_key_registration(self) -> Callable[[OfflineKeyRegistrationParams], Transaction]: + """Create an offline key registration transaction.""" + return self._transaction(lambda c: c.add_offline_key_registration) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 6c072edc..f0ffdf83 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -28,6 +28,7 @@ AssetOptInParams, AssetOptOutParams, AssetTransferParams, + OfflineKeyRegistrationParams, OnlineKeyRegistrationParams, PaymentParams, SendAtomicTransactionComposerResults, @@ -397,3 +398,12 @@ def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSi f"Registering online key for {params.sender} via transaction {transaction.get_txid()}" ), )(params) + + def offline_key_registration(self, params: OfflineKeyRegistrationParams) -> SendSingleTransactionResult: + """Register an offline key.""" + return self._send( + lambda c: c.add_offline_key_registration, + pre_log=lambda params, transaction: ( + f"Registering offline key for {params.sender} via transaction {transaction.get_txid()}" + ), + )(params) diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 1d1cf2a8..47000dde 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -22,6 +22,7 @@ AssetOptInParams, AssetOptOutParams, AssetTransferParams, + OfflineKeyRegistrationParams, OnlineKeyRegistrationParams, PaymentParams, TransactionComposer, @@ -449,7 +450,7 @@ def test_payment_logging( assert receiver.address in log_message -def test_online_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: +def test_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: sp = transaction_sender._algod.suggested_params() # noqa: SLF001 params = OnlineKeyRegistrationParams( @@ -465,3 +466,14 @@ def test_online_key_registration(transaction_sender: AlgorandClientTransactionSe result = transaction_sender.online_key_registration(params) assert len(result.tx_ids) == 1 assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + + sp = transaction_sender._algod.suggested_params() # noqa: SLF001 + + off_key_reg_params = OfflineKeyRegistrationParams( + sender=sender.address, + prevent_account_from_ever_participating_again=True, + ) + + result = transaction_sender.offline_key_registration(off_key_reg_params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] From b3dec7f4e4acb93fc3794a77deac03d878e661ff Mon Sep 17 00:00:00 2001 From: Al Date: Mon, 23 Dec 2024 18:16:10 +0100 Subject: [PATCH 10/31] docs: initial documentation for beta release (#128) * docs: adding rewritten app client amount and account sections; respective docstrings * docs: initial rewrites for appmanager; clients and debugger docs * docs: adding migration guide, refining remaining capabilities; adding missing exports * chore: addressing pr comments Renaming network related namings from *_net to *net * chore: update docs/source/capabilities/transfer.md Co-authored-by: Neil Campbell --------- Co-authored-by: Neil Campbell --- docs/source/capabilities/account.md | 222 +++++- docs/source/capabilities/algorand-client.md | 85 +++ docs/source/capabilities/amount.md | 69 ++ docs/source/capabilities/app-client.md | 639 +++++++++++++++--- docs/source/capabilities/app-manager.md | 160 +++++ docs/source/capabilities/client.md | 106 ++- docs/source/capabilities/debugger.md | 70 +- docs/source/capabilities/dispenser-client.md | 63 +- .../capabilities/transaction-composer.md | 227 +++++++ docs/source/capabilities/transaction.md | 147 ++++ docs/source/capabilities/transfer.md | 193 ++++-- docs/source/index.md | 140 +++- docs/source/migration-guide.md | 252 +++++++ src/algokit_utils/_legacy_v2/_transfer.py | 3 + .../_legacy_v2/network_clients.py | 12 +- src/algokit_utils/accounts/account_manager.py | 533 ++++++++++++--- .../accounts/kmd_account_manager.py | 4 +- src/algokit_utils/applications/app_client.py | 10 +- src/algokit_utils/beta/__init__.py | 72 ++ src/algokit_utils/clients/algorand_client.py | 41 +- src/algokit_utils/clients/client_manager.py | 34 +- src/algokit_utils/models/__init__.py | 1 + src/algokit_utils/models/amount.py | 62 +- .../transactions/transaction_composer.py | 5 + tests/accounts/test_account_manager.py | 2 +- tests/applications/test_app_client.py | 2 +- tests/applications/test_app_factory.py | 2 +- tests/applications/test_app_manager.py | 2 +- tests/assets/test_asset_manager.py | 2 +- .../clients/algorand_client/test_transfer.py | 6 +- tests/test_debug_utils.py | 2 +- tests/transactions/test_resource_packing.py | 2 +- .../transactions/test_transaction_composer.py | 2 +- .../transactions/test_transaction_creator.py | 2 +- tests/transactions/test_transaction_sender.py | 2 +- 35 files changed, 2762 insertions(+), 414 deletions(-) create mode 100644 docs/source/capabilities/algorand-client.md create mode 100644 docs/source/capabilities/amount.md create mode 100644 docs/source/capabilities/app-manager.md create mode 100644 docs/source/capabilities/transaction-composer.md create mode 100644 docs/source/capabilities/transaction.md create mode 100644 docs/source/migration-guide.md create mode 100644 src/algokit_utils/beta/__init__.py diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index d0c2b42f..cb7190a0 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -1,31 +1,195 @@ # Account management -Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, idempotent KMD and environment variable injected accounts -that can be used to sign transactions as well as representing a sender address at the same time. - -(account)= -## `Account` - -Encapsulates a private key with convenience properties for `address`, `signer` and `public_key`. - -There are various methods of obtaining an `Account` instance - -* `get_account`: Returns an `Account` instance with the private key loaded by convention based on the given name identifier: - * from an environment variable containing a mnemonic `{NAME}_MNEMONIC` OR - * loading the account from KMD ny name if it exists (LocalNet only) OR - * creating the account in KMD with associated name (LocalNet only) - - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against - TestNet/MainNet will automatically resolve from environment variables - -* `Account.new_account`: Returns a new `Account` using `algosdk.account.generate_account()` -* `Account(private_key)`: Load an existing account from a private key -* `Account(private_key, address)`: Load an existing account from a private key and address, useful for re-keyed accounts -* `get_account_from_mnemonic`: Load an existing account from a mnemonic -* `get_dispenser_account`: Gets a dispenser account that is funded by either: - * Using the LocalNet default account (LocalNet only) OR - * Loading an account from `DISPENSER_MNEMONIC` - -If working with a LocalNet instance, there are some additional functions that rely on a KMD service being exposed: -* `create_kmd_wallet_account`, `get_kmd_wallet_account` or `get_or_create_kmd_wallet_account`: These functions allow retrieving a KMD wallet account by name, -* `get_localnet_default_account`: Gets default localnet account that is funded with algos +Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, rekeyed, multisig, transaction signer, idempotent KMD and environment variable injected accounts that can be used to sign transactions as well as representing a sender address at the same time. This significantly simplifies management of transaction signing. + +## `AccountManager` + +The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using transaction composition to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! + +To get an instance of `AccountManager`, you can either use the `AlgorandClient` via `algorand.account` or instantiate it directly: + +```python +from algokit_utils import AccountManager + +account_manager = AccountManager(client_manager) +``` + +## `Account` and Transaction Signing + +The core type that holds information about a signer/sender pair for a transaction in Python is the `Account` class, which represents both the signing capability and sender address in one object. This is different from the TypeScript implementation which uses `TransactionSignerAccount` interface that combines an `algosdk.TransactionSigner` with a sender address. + +The Python `Account` class provides: + +- `address` - The encoded string address +- `private_key` - The private key for signing +- `signer` - An `AccountTransactionSigner` that can sign transactions +- `public_key` - The public key associated with this account + +## Registering a signer + +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by the transaction composition functionality to automatically sign transactions by that sender. Any of the [methods](#accounts) within `AccountManager` that return an account will automatically register the signer with the sender. + +There are two methods that can be used for this: + +```python +# Register an account object that has both signer and sender +account_manager.set_signer_from_account(account) + +# Register just a signer for a given sender address +account_manager.set_signer("SENDER_ADDRESS", transaction_signer) +``` + +## Default signer + +If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can register a default signer: + +```python +account_manager.set_default_signer(my_default_signer) +``` + +## Get a signer + +The library will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: + +```python +signer = account_manager.get_signer("SENDER_ADDRESS") +``` + +If there is no signer registered for that sender address it will either return the default signer (if registered) or raise an exception. + +## Accounts + +In order to get/register accounts for signing operations you can use the following methods on `AccountManager`: + +- `from_environment(name: str, fund_with: AlgoAmount | None = None) -> Account` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `{NAME}_MNEMONIC` and (optionally) `{NAME}_SENDER` (if account is rekeyed) + - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code + - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD +- `from_mnemonic(mnemonic_secret: str) -> Account` - Registers and returns an account with secret key loaded by taking the mnemonic secret +- `multisig(version: int, threshold: int, addrs: list[str], signing_accounts: list[Account]) -> MultisigAccount` - Registers and returns a multisig account with one or more signing keys loaded +- `rekeyed(sender: Account | str, account: Account) -> Account` - Registers and returns an account representing the given rekeyed sender/signer combination +- `random() -> Account` - Returns a new, cryptographically randomly generated account with private key loaded +- `from_kmd(name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an account with private key loaded from the given KMD wallet +- `logic_sig(program: bytes, args: list[bytes] | None = None) -> LogicSigAccount` - Returns an account that represents a logic signature + +### Underlying account classes + +While `Account` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer: + +- `Account` - The main account class that combines address and private key +- `LogicSigAccount` - An in-built algosdk `LogicSigAccount` object for logic signature accounts +- `MultisigAccount` - An abstraction around multisig accounts that supports multisig accounts with one or more signers present + +### Dispenser + +- `dispenser_from_environment() -> Account` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- `localnet_dispenser() -> Account` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account + +## Rekey account + +One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +```{warning} +Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. +``` + +You can issue a transaction to rekey an account by using the `rekey_account` method: + +```python +account_manager.rekey_account( + account="ACCOUNTADDRESS", # str | Account + rekey_to="NEWADDRESS", # str | Account + # Optional parameters + signer=None, # TransactionSigner + note=None, # bytes + lease=None, # bytes + static_fee=None, # AlgoAmount + extra_fee=None, # AlgoAmount + max_fee=None, # AlgoAmount + validity_window=None, # int + first_valid_round=None, # int + last_valid_round=None, # int + suppress_log=None # bool +) +``` + +You can also pass in `rekey_to` as a common transaction parameter to any transaction. + +### Examples + +```python +# Basic example (with string addresses) +account_manager.rekey_account(account="ACCOUNTADDRESS", rekey_to="NEWADDRESS") + +# Basic example (with signer accounts) +account_manager.rekey_account(account=account1, rekey_to=new_signer_account) + +# Advanced example +account_manager.rekey_account( + account="ACCOUNTADDRESS", + rekey_to="NEWADDRESS", + lease="lease", + note="note", + first_valid_round=1000, + validity_window=10, + extra_fee=1000, # microAlgos + static_fee=1000, # microAlgos + max_fee=3000, # microAlgos + max_rounds_to_wait_for_confirmation=5, + suppress_log=True, +) + +# Using a rekeyed account +# Note: if a signing account is passed into account_manager.rekey_account then you don't need to call rekeyed_account to register the new signer +rekeyed_account = account_manager.rekeyed(account, new_account) +# rekeyed_account can be used to sign transactions on behalf of account... +``` + +# KMD account management + +When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for: + +- Accessing the private key of the default accounts that are pre-seeded with Algo so that other accounts can be funded and it's possible to use LocalNet +- Idempotently creating new accounts against a name that will stay intact while the LocalNet instance is running without you needing to store private keys anywhere (i.e. completely automated) + +The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that's needed. This code has been abstracted away into the `KmdAccountManager` class. + +To get an instance of the `KmdAccountManager` class you can access it from `AccountManager` via `account_manager.kmd` or instantiate it directly (passing in a `ClientManager`): + +```python +from algokit_utils import KmdAccountManager + +# Algod client only +kmd_account_manager = KmdAccountManager(client_manager) +``` + +The methods that are available are: + +- `get_wallet_account(wallet_name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- `get_or_create_wallet_account(name: str, fund_with: AlgoAmount | None = None) -> Account` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- `get_localnet_dispenser_account() -> Account` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) + +```python +# Get a wallet account that seeded the LocalNet network +default_dispenser_account = kmd_account_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a.status != "Offline" and a.amount > 1_000_000_000, +) +# Same as above, but dedicated method call for convenience +localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +# Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD +# if creating it then fund it with 2 ALGO from the default dispenser account +new_account = kmd_account_manager.get_or_create_wallet_account("account1", AlgoAmount.from_algo(2)) +# This will return the same account as above since the name matches +existing_account = kmd_account_manager.get_or_create_wallet_account("account1") +``` + +Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions: + +```python +# Get and register LocalNet dispenser +localnet_dispenser = account_manager.localnet_dispenser() +# Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD +dispenser = account_manager.dispenser_from_environment() +# Get / create and register account from KMD idempotently by name +account1 = account_manager.from_kmd("account1", AlgoAmount.from_algo(2)) +``` diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md new file mode 100644 index 00000000..181751dc --- /dev/null +++ b/docs/source/capabilities/algorand-client.md @@ -0,0 +1,85 @@ +# Algorand client + +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the default entrypoint into AlgoKit Utils functionality. + +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: + +```python +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client(s) +algorand = AlgorandClient.from_clients( + AlgoSdkClients( + algod=..., + indexer=..., + kmd=..., + ) +) +# Point to custom configuration +algorand = AlgorandClient.from_config( + AlgoClientConfigs( + algod_config=AlgoClientConfig( + server="http://localhost:4001", token="my-token", port=4001 + ), + indexer_config=None, + kmd_config=None, + ) +) +``` + +## Accessing SDK clients + +Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. + +```python +algorand = AlgorandClient.default_localnet() + +algod_client = algorand.client.algod +indexer_client = algorand.client.indexer +kmd_client = algorand.client.kmd +``` + +## Accessing manager class instances + +The `AlgorandClient` has several manager class instances that help you quickly access advanced functionality: + +- `AccountManager` via `algorand.account`, with chainable convenience methods: + - `algorand.set_default_signer(signer)` + - `algorand.set_signer(sender, signer)` +- `AssetManager` via `algorand.asset` +- `ClientManager` via `algorand.client` +- `AppManager` via `algorand.app` +- `AppDeployer` via `algorand.app_deployer` + +## Creating and issuing transactions + +`AlgorandClient` exposes methods to create, execute, and compose groups of transactions via the `TransactionComposer`. + +### Transaction configuration + +AlgorandClient caches network provided transaction values automatically to reduce network traffic. You can configure this behavior: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds the transaction will be valid). Defaults to 10. +- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout for caching suggested network parameters (default 3 seconds) +- `algorand.get_suggested_params()` - Get current suggested network parameters + +### Creating transaction groups + +You can compose a group of transactions using the `new_group()` method which returns a `TransactionComposer` instance: + +```python +result = ( + algorand.new_group() + .add_payment(sender="SENDERADDRESS", receiver="RECEIVERADDRESS", amount=1_000) + .add_asset_opt_in(sender="SENDERADDRESS", asset_id=12345) + .send() +) +``` diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md new file mode 100644 index 00000000..e1c3a7be --- /dev/null +++ b/docs/source/capabilities/amount.md @@ -0,0 +1,69 @@ +# Algo amount handling + +Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. + +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function you can safely and explicitly convert to microAlgo or Algo. + +To see some usage examples check out the automated tests in the repository. Alternatively, you can refer to the reference documentation for `AlgoAmount`. + +## `AlgoAmount` + +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or exiting the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). + +To import the AlgoAmount class you can access it via: + +```python +from algokit_utils.models import AlgoAmount +``` + +### Creating an `AlgoAmount` + +There are several ways to create an `AlgoAmount`: + +- Algo + - Constructor: `AlgoAmount({"algo": 10})` + - Static helper: `AlgoAmount.from_algo(10)` + - Static helper (plural): `AlgoAmount.from_algos(10)` +- microAlgo + - Constructor: `AlgoAmount({"microAlgo": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` + - Static helper (plural): `AlgoAmount.from_micro_algos(10_000)` + +### Extracting a value from `AlgoAmount` + +The `AlgoAmount` class has properties to return Algo and microAlgo: + +- `amount.algo` or `amount.algos` - Returns the value in Algo +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo + +`AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. + +You can also call `str(amount)` or use an `AlgoAmount` directly in string interpolation to convert it to a nice user-facing formatted amount expressed in microAlgo. + +### Additional Features + +The `AlgoAmount` class also supports: + +- Arithmetic operations (`+`, `-`) with other `AlgoAmount` objects or integers +- Comparison operations (`<`, `<=`, `>`, `>=`, `==`, `!=`) +- In-place arithmetic (`+=`, `-=`) + +Example usage: + +```python +from algokit_utils.models import AlgoAmount + +# Create amounts +amount1 = AlgoAmount.from_algo(1.5) # 1.5 Algos +amount2 = AlgoAmount.from_micro_algos(500_000) # 0.5 Algos + +# Arithmetic +total = amount1 + amount2 # 2 Algos +difference = amount1 - amount2 # 1 Algo + +# Comparisons +is_greater = amount1 > amount2 # True + +# String representation +print(amount1) # "1,500,000 µALGO" +``` diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index d76f27b7..91a9982e 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -1,157 +1,590 @@ -# App client +# App Client and App Factory -Application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker). +> [!NOTE] +> This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client is a high productivity application client that works with ARC-0032 application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](./app-deploy.md) and [App management](./app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_app_client_call.py). +> [!NOTE] +> If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don't know the app ID (deferred knowledge or the instance doesn't exist yet on the blockchain) or you have multiple app IDs -## Design +## `AppFactory` -The design for the app client is based on a wrapper for parsing an [ARC-0032](https://github.com/algorandfoundation/ARCs/pull/150) application spec and wrapping the [App deployment](./app-deploy.md) functionality and corresponding [design](./app-deploy.md#design). +The `AppFactory` is a class that, for a given app spec, allows you to create and deploy one or more app instances and to create one or more app clients to interact with those (or other) app instances. -## Creating an application client +To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: -There are two key ways of instantiating an ApplicationClient: +```python +from algokit_utils.clients import AlgorandClient + +# Create an Algorand client +algorand = AlgorandClient.from_environment() + +# Minimal example +factory = algorand.get_app_factory( + app_spec=app_spec, # ARC-0032 or ARC-0056 app spec +) + +# Advanced example +factory = algorand.get_app_factory( + app_spec=app_spec, # ARC-0032 or ARC-0056 app spec + app_name="MyApp", + default_sender="SENDER_ADDRESS", + default_signer=signer, + version="1.0.0", + updatable=True, + deletable=True, + deploy_time_params={"TMPL_VALUE": "value"}, +) +``` -1. By app ID - When needing to call an existing app by app ID or unconditionally create a new app. -The signature `ApplicationClient(algod_client, app_spec, app_id=..., ...)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `app_id`: The app_id of an existing application, or 0 if creating a new app +## `AppClient` -2. By creator and app name - When needing to deploy or find an app associated with a specific creator account and app name. -The signature `ApplicationClient(algod_client, app_spec, creator=..., indexer=..., app_lookup)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `creator`: The address or `Account` of the creator of the app for which to search for the deployed app under - * `indexer`: - * `app_lookup`: Optional if an indexer is provided, - * `app_name`: An overridden name to identify the contract with, otherwise `contract.name` is used from the app spec +The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). -Both approaches also allow specifying the following parameters that will be used as defaults for all application calls: -* `signer`: `TransactionSigner` to sign transactions with. -* `sender`: Address to use for transaction signing, will be derived from the signer if not provided. -* `suggested_params`: Default `SuggestedParams` to use, will use current network suggested params by default - -Both approaches also allow specifying a mapping of template values via the `template_values` parameter, this will be used before compiling the application to replace any -`TMPL_` variables that may be in the TEAL. The `TMPL_UPDATABLE` and `TMPL_DELETABLE` variables used in some AlgoKit templates are handled by the `deploy` method, but should be included if -using `create` or `update` directly. +To get an instance of `AppClient` you can use `AlgorandClient` via `get_app_client_by_id` or use the factory methods: -## Calling methods on the app +```python +from algokit_utils.clients import AlgorandClient + +# Create an Algorand client +algorand = AlgorandClient.from_environment() + +# Get client by ID +client = algorand.get_app_client_by_id( + app_spec=app_spec, # ARC-0032 or ARC-0056 app spec + app_id=existing_app_id, # Use 0 for new app + app_name="MyApp", # Optional: Name of the app + default_sender="SENDER_ADDRESS", # Optional: Default sender address + default_signer=signer, # Optional: Default signer for transactions + approval_source_map=approval_map, # Optional: Source map for approval program + clear_source_map=clear_map, # Optional: Source map for clear program +) + +# Get client by creator and name using factory +factory = algorand.get_app_factory(app_spec=app_spec) +client = factory.get_app_client_by_creator_and_name( + creator_address="CREATOR_ADDRESS", + app_name="MyApp", + ignore_cache=False, # Optional: Whether to ignore app lookup cache +) +``` -There are various methods available on `ApplicationClient` that can be used to call an app: +You can get the `app_id` and `app_address` at any time as properties on the `AppClient` along with `app_name` and `app_spec`. -* `call`: Used to call methods with an on complete action of `no_op` -* `create`: Used to create an instance of the app, by using an `app_id` of 0, includes the approval and clear programs in the call -* `update`: Used to update an existing app, includes the approval and clear programs in the call, and is called with an on complete action of `update_application` -* `delete`: Used to remove an existing app, is called with an on complete action of `delete_application` -* `opt_in`: Used to opt in to an existing app, is called with an on complete action of `opt_in` -* `close_out`: Used to close out of an existing app, is called with an on complete action of `opt_in` -* `clear_state`: Used to unconditionally close out from an app, calls the clear program of an app +## Dynamically creating clients for a given app spec -### Specifying which method +As well as allowing you to control creation and deployment of apps, the `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. -All methods for calling an app that support ABI methods (everything except `clear_state`) take a parameter `call_abi_method` which can be used to specify which method to call. -The method selected can be specified explicitly, or allow the client to infer the method where possible, supported values are: +This is possible via two methods on the app factory: -* `None`: The default value, when `None` is passed the client will attempt to find any ABI method or bare method that is compatible with the provided arguments -* `False`: Indicates that an ABI method should not be used, and instead a bare method call is made -* `True`: Indicates that an ABI method should be used, and the client will attempt to find an ABI method that is compatible with the provided arguments -* `str`: If a string is provided, it will be interpreted as either an ABI signature specifying a method, or as an ABI method name -* `algosdk.abi.Method`: The specified ABI method will be called -* `ABIReturnSubroutine`: Any type that has a `method_spec` function that returns an `algosd.abi.Method` +- `factory.get_app_client_by_id` - Returns a new `AppClient` client for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. +- `factory.get_app_client_by_creator_and_name` - Returns a new `AppClient` client, resolving the app by creator address and name using AlgoKit app deployment semantics (i.e. looking for the app creation transaction note). Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. -### ABI arguments +```python +# Get clients by ID +client1 = factory.get_app_client_by_id(app_id=12345) +client2 = factory.get_app_client_by_id(app_id=12346) +client3 = factory.get_app_client_by_id( + app_id=12345, + default_sender="SENDER2_ADDRESS" +) + +# Get clients by creator and name +client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATOR_ADDRESS", +) +client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATOR_ADDRESS", + app_name="NonDefaultAppName", +) +client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATOR_ADDRESS", + app_name="NonDefaultAppName", + ignore_cache=True, # Perform fresh indexer lookups + default_sender="SENDER2_ADDRESS", +) +``` -ABI arguments are passed as python keyword arguments e.g. to pass the ABI parameter `name` for the ABI method `hello` the following syntax is used `client.call("hello", name="world")` +## Creating and deploying an app -### Transaction Parameters +Once you have an [app factory](#appfactory) you can perform the following actions: -All methods for calling an app take an optional `transaction_parameters` argument, with the following supported parameters: +- `factory.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.deploy(params)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app -* `signer`: The `TransactionSigner` to use on the call. This overrides any signer specified on the client -* `sender`: The address of the sender to use on the call, must be able to be signed for by the `signer`. This overrides any sender specified on the client -* `suggested_params`: `SuggestedParams` to use on the call. This overrides any suggested_params specified on the client -* `note`: Note to include in the transaction -* `lease`: Lease parameter for the transaction -* `boxes`: A sequence of boxes to use in the transaction, this is a list of (app_index, box_name) tuples `[(0, "box_name"), (0, ...)]` -* `accounts`: Account references to include in the transaction -* `foreign_apps`: Foreign apps to include in the transaction -* `foreign_assets`: Foreign assets to include in the transaction -* `on_complete`: The on complete action to use for the transaction, only available when using `call` or `create` -* `extra_pages`: Additional pages to allocate when calling `create`, by default a sufficient amount will be calculated based on the current approval and clear. This can be overridden, if more is required - for a future update +### Create + +The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: + +- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec (noting you can override the `schema`) +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control; these values can also be passed into the `AppFactory` constructor instead and if so will be used if not defined in the params to the create call -Parameters can be passed as one of the dataclasses `CommonCallParameters`, `OnCompleteCallParameters`, `CreateCallParameters` (exact type depends on method used) ```python -client.call("hello", transaction_parameters=algokit_utils.OnCompleteCallParameters(signer=...)) +# Use no-argument bare-call +create_response = factory.send.bare.create() + +# Specify parameters for bare-call and override other parameters +create_response = factory.send.bare.create( + params=factory.params.bare.create( + args=[bytes([1, 2, 3, 4])], + on_complete=OnComplete.OptInOC, + deploy_time_params={ + "ONE": 1, + "TWO": "two", + }, + updatable=True, + deletable=False, + populate_app_call_resources=True, + ) +) + +## Or passing params directly +create_response = factory.send.bare.create( + AppFactoryCreateWithSendParams( + args=[bytes([1, 2, 3, 4])], + on_complete=OnComplete.OptInOC, + deploy_time_params={ + "ONE": 1, + "TWO": "two", + }, + updatable=True, + deletable=False, + populate_app_call_resources=True, + ) +) + +# Specify parameters for ABI method call +create_response = factory.send.create( + params=factory.params.create( + method="create_application", + args=[1, "something"], + ) +) ``` -Alternatively, parameters can be passed as a dictionary e.g. +If you want to construct a custom create call, you can get params objects: + +- `factory.params.create(params)` - ABI method create call for deploy method +- `factory.params.bare.create(params)` - Bare create call for deploy method + +### Deploy + +The deploy method is a wrapper over the `AppDeployer`'s `deploy` method, with the following differences: + +- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` in the `create_params` because these are all specified or calculated from the app spec (noting you can override the `schema`) +- `sender` is optional for `create_params`, `update_params` and `delete_params` and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) +- You don't need to pass in `metadata` to the deploy params - it's calculated from: + - `updatable` and `deletable`, which you can optionally pass in directly to the method params + - `version` and `name`, which are optionally passed into the `AppFactory` constructor +- `deploy_time_params`, `updatable` and `deletable` can all be passed into the `AppFactory` and if so will be used if not defined in the params to the deploy call for deploy-time parameter replacements and deploy-time immutability and permanence control +- `create_params`, `update_params` and `delete_params` are optional, if they aren't specified then default values are used for everything and a no-argument bare call will be made for any create/update/delete calls +- If you want to call an ABI method for create/update/delete calls then you can pass in a string for `method`, which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature + ```python -client.call("hello", transaction_parameters={"signer":...}) +# Use no-argument bare-calls to deploy with default behaviour +# for when update or schema break detected (fail the deployment) +client, response = factory.deploy({}) + +# Specify parameters for bare-calls and override the schema break behaviour +client, response = factory.deploy( + create_params=factory.params.bare.create( + args=[bytes([1, 2, 3, 4])], + on_complete=OnComplete.OptIn, + ), + update_params=factory.params.bare.deploy_update( + args=[bytes([1, 2, 3])], + ), + delete_params=factory.params.bare.deploy_delete( + args=[bytes([1, 2])], + ), + deploy_time_params={ + "ONE": 1, + "TWO": "two", + }, + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.ReplaceApp, + updatable=True, + deletable=True, +) + +# Specify parameters for ABI method calls +client, response = factory.deploy( + create_params=factory.params.create( + method="create_application", + args=[1, "something"], + ), + update_params=factory.params.deploy_update( + method="update", + ), + delete_params=factory.params.deploy_delete( + method="delete_app(uint64,uint64,uint64)uint64", + args=[1, 2, 3], + ), +) ``` -## Composing calls +If you want to construct a custom deploy call, you can get params objects for the `create_params`, `update_params` and `delete_params`: + +- `factory.params.create(params)` - ABI method create call for deploy method +- `factory.params.deploy_update(params)` - ABI method update call for deploy method +- `factory.params.deploy_delete(params)` - ABI method delete call for deploy method +- `factory.params.bare.create(params)` - Bare create call for deploy method +- `factory.params.bare.deploy_update(params)` - Bare update call for deploy method +- `factory.params.bare.deploy_delete(params)` - Bare delete call for deploy method + +## Updating and deleting an app + +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than [other calls](#calling-the-app), with the caveat that the update call is a bit different to the others since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. + +## Calling the app + +You can construct a params object, transaction(s) and sign and send a transaction to call the app that a given `AppClient` instance is pointing to. + +This is done via the following properties: + +- `client.params.{on_complete}(params)` - Params for an ABI method call +- `client.params.bare.{on_complete}(params)` - Params for a bare call +- `client.create_transaction.{on_complete}(params)` - Transaction(s) for an ABI method call +- `client.create_transaction.bare.{on_complete}(params)` - Transaction for a bare call +- `client.send.{on_complete}(params)` - Sign and send an ABI method call +- `client.send.bare.{on_complete}(params)` - Sign and send a bare call + +To make one of these calls `{on_complete}` needs to be swapped with the [on complete action](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#the-lifecycle-of-a-smart-contract) that should be made: + +- `update` - An update call +- `opt_in` - An opt-in call +- `delete` - A delete application call +- `clear_state` - A clear state call (note: calls the clear program and only applies to bare calls) +- `close_out` - A close-out call +- `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) -If multiple calls need to be made in a single transaction, the `compose_` method variants can be used. All these methods take an `AtomicTransactionComposer` as their first argument. -Once all the calls have been added to the ATC, it can then be executed. For example: +The input payload for all of these calls is the same as the underlying app methods with the caveat that the `app_id` is not passed in (since the `AppClient` already knows the app ID), `sender` is optional (it uses `default_sender` from the `AppClient` constructor if it was specified) and `method` (for ABI method calls) is a string rather than an `ABIMethod` object (which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature). ```python -from algokit_utils import ApplicationClient -from algosdk.atomic_transaction_composer import AtomicTransactionComposer +update_call = client.send.update( + params=client.params.update( + method="update_abi", + args=["string_io"], + deploy_time_params=deploy_time_params, + ) +) +delete_call = client.send.delete( + params=client.params.delete( + method="delete_abi", + args=["string_io"], + ) +) +opt_in_call = client.send.opt_in( + params=client.params.opt_in( + method="opt_in" + ) +) +clear_state_call = client.send.bare.clear_state() + +transaction = client.create_transaction.bare.close_out( + params=client.params.bare.close_out( + args=[bytes([1, 2, 3])], + ) +) + +params = client.params.opt_in(method="optin") +``` + +### Nested ABI Method Call Transactions + +The ARC4 ABI specification supports ABI method calls as arguments to other ABI method calls, enabling some interesting use cases. While this conceptually resembles a function call hierarchy, in practice, the transactions are organized as a flat, ordered transaction group. Unfortunately, this logically hierarchical structure cannot always be correctly represented as a flat transaction group, making some scenarios impossible. + +To illustrate this, let's consider an example of two ABI methods with the following signatures: + +- `myMethod(pay,appl)void` +- `myOtherMethod(pay)void` + +These signatures are compatible, so `myOtherMethod` can be passed as an ABI method call argument to `myMethod`, which would look like: + +Hierarchical method call + +``` +myMethod(pay, myOtherMethod(pay)) +``` + +Flat transaction group + +``` +pay (pay) +appl (myOtherMethod) +appl (myMethod) +``` + +An important limitation to note is that the flat transaction group representation does not allow having two different pay transactions. This invariant is represented in the hierarchical call interface of the app client by passing `None` for the value. This acts as a placeholder and tells the app client that another ABI method call argument will supply the value for this argument. For example: + +```python +payment = client.algorand.create_transaction.payment( + sender="SENDER_ADDRESS", + receiver="RECEIVER_ADDRESS", + amount=1_000_000, # 1 Algo +) + +my_other_method_call = client.params.call( + method="myOtherMethod", + args=[payment], +) + +my_method_call = client.send.call( + params=client.params.call( + method="myMethod", + args=[None, my_other_method_call], + ) +) +``` + +`my_other_method_call` supplies the pay transaction to the transaction group and, by association, `my_other_method_call` has access to it as defined in its signature. +To ensure the app client builds the correct transaction group, you must supply a value for every argument in a method call signature. + +## Funding the app account + +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you `fund_app_account(params)`. -client = ApplicationClient(...) -atc = AtomicTransactionComposer() -client.compose_call(atc, "hello", name="world") -... # additional compose calls +The input parameters are: -response = client.execute_atc(atc) +- A `FundAppParams`, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). + +Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you'll want to get the funding call as a transaction, e.g.: + +```python +result = client.send.call( + params=client.params.call( + method="bootstrap", + args=[ + client.create_transaction.fund_app_account( + params=client.params.fund_app_account( + amount=200_000, # microAlgos + ) + ), + ], + box_references=["Box1"], + ) +) ``` +You can also get the funding call as a params object via `client.params.fund_app_account(params)`. ## Reading state +`AppClient` has a number of mechanisms to read state (global, local and box storage) from the app instance. + +### App spec methods + +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the [generic methods](#generic-methods) give you. + +You can access this functionality via: + +- `client.state.global_state.{method}()` - Global state +- `client.state.local_state(address).{method}()` - Local state +- `client.state.box.{method}()` - Box storage + +Where `{method}` is one of: + +- `get_all()` - Returns all single-key state values in a record keyed by the key name and the value a decoded ABI value. +- `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be a `bytes` with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value record. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. + +```python +values = client.state.global_state.get_all() +value = client.state.local_state("ADDRESS").get_value("value1") +map_value = client.state.box.get_map_value("map1", "mapKey") +map_values = client.state.global_state.get_map("myMap") +``` + +### Generic methods + There are various methods defined that let you read state from the smart contract app: -* `get_global_state` - Gets the current global state of the app -* `get_local_state` - Gets the current local state for the given account address +- `get_global_state()` - Gets the current global state +- `get_local_state(address: str)` - Gets the current local state for the given account address +- `get_box_names()` - Gets the current box names +- `get_box_value(name)` - Gets the current value of the given box +- `get_box_value_from_abi_type(name, abi_type)` - Gets the current value of the given box decoded using the specified ABI type +- `get_box_values(filter)` - Gets the current values of the boxes +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes decoded using the specified ABI type + +```python +global_state = client.get_global_state() +local_state = client.get_local_state("ACCOUNT_ADDRESS") + +box_name = "my-box" +box_name2 = "my-box2" + +box_names = client.get_box_names() +box_value = client.get_box_value(box_name) +box_values = client.get_box_values([box_name, box_name2]) +box_abi_value = client.get_box_value_from_abi_type(box_name, algosdk.ABIStringType()) +box_abi_values = client.get_box_values_from_abi_type([box_name, box_name2], algosdk.ABIStringType()) +``` ## Handling logic errors and diagnosing errors -Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, -exhaustion of opcode budget, or any number of other reasons. +Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons. When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](./app-deploy.md#compilation-and-template-substitution) you can expose debugging -information that makes it much easier to understand what's happening. +The information in that error message can be parsed and when combined with the source map from compilation you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. + +The app client and app factory automatically provide this functionality for all smart contract calls. When an error is thrown then the resulting error that is re-thrown will be a `LogicError` object, which has the following fields: -When an error is thrown then the resulting error that is re-thrown will be a `LogicError`, which has the following fields: +- `logic_error_str: str` - The original error message +- `program: str` - The TEAL program +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - The error message +- `pc: int` - The program counter value +- `traces: list[SimulationTrace] | None` - Any traces that were included in the error +- `line_no: int | None` - The line number in the TEAL program that triggered the error +- `lines: list[str]` - The TEAL program split into lines -* `logic_error`: Original exception -* `program`: Program source (if available) -* `source_map`: Source map used (if available) -* `transaction_id`: Transaction ID of failing transaction -* `message`: The error message -* `line_no`: The line number in the TEAL program that -* `traces`: A list of Trace objects providing additional insights on simulation when debug mode is active. +Note: This information will only show if the app client / app factory has a source map. This will occur if: -The function `trace()` will provide a formatted output of the surrounding TEAL where the error occurred. +- You have called `create`, `update` or `deploy` +- You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) +- You had source maps present in an app factory and then used it to create an app client (they are automatically passed through) -```{note} -The extended information will only show if the Application Client has a source map. This will occur if: +If you want to go a step further and automatically issue a simulated transaction and get trace information when there is an error when an ABI method is called you can turn on debug mode: + +```python +from algokit_utils.config import config -1.) The ApplicationClient instance has already called, `create, `update` or `deploy` OR -2.) `template_values` are provided when creating the ApplicationClient, so a SourceMap can be obtained automatically OR -3.) `approval_source_map` on `ApplicationClient` has been set from a previously compiled approval program OR -4.) A source map has been exported/imported using `export_source_map`/`import_source_map`""" +config.configure(debug=True) +``` + +If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. + +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the project root is also configured. + +Example error handling: + +```python +from algokit_utils.config import config + +# Enable debug mode for detailed error information +config.configure(debug=True) + +try: + client.send.call( + params=client.params.call( + method="will_fail", + args=["test"] + ) + ) +except algokit_utils.LogicError as e: + print(f"Error at line {e.value.line_no}") # Access via value property + print(f"Error message: {e.value.message}") + print(f"Transaction ID: {e.value.transaction_id}") + print(e.value.trace()) # Shows TEAL execution trace with source mapping + + if e.value.traces: # Available when debug mode is active + for trace in e.value.traces: + print(f"PC: {trace['pc']}, Stack: {trace['stack']}") +``` + +## Best Practices + +1. Use typed ABI methods when possible for better type safety +2. Always handle potential logic errors with proper error handling +3. Use transaction composition for atomic operations +4. Leverage source maps and debug mode for development +5. Use idempotent deployment patterns with versioning +6. Properly manage box references to avoid transaction failures +7. Use template values for flexible application deployment +8. Implement proper state management with type safety +9. Use the client's parameter builders for type-safe transaction creation +10. Leverage the state accessor patterns for cleaner state management + +## Common Patterns + +### Idempotent Deployment + +```python +# Deploy with idempotency and version tracking +client, response = factory.deploy( + version="1.0.0", + deploy_time_params={"TMPL_VALUE": "value"}, + on_update=algokit_utils.OnUpdate.UpdateApp, + on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, + create_params=factory.params.create( + method="create", + args=["initial_value"], + ), +) + +if response.app.app_id != 0: + print(f"Deployed app ID: {response.app.app_id}") + if response.operation_performed == algokit_utils.OperationPerformed.Create: + print("New application deployed") + else: + print("Existing application found") +``` + +### Application State Migration + +```python +# Deploy with state migration +client, response = factory.deploy( + version="2.0.0", + on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, + on_update=algokit_utils.OnUpdate.UpdateApp, + create_params=factory.params.create( + method="create", + args=["initial_value"], + schema={ + "global_ints": 1, + "global_byte_slices": 1, + "local_ints": 0, + "local_byte_slices": 0, + }, + ), +) + +if response.operation_performed == algokit_utils.OperationPerformed.Replace: + # Migrate state from old to new app + # Note: Migration logic should be implemented in the smart contract + client.send.call( + params=client.params.call( + method="migrate_state", + args=[response.old_app_id], + ) + ) +``` + +### Opt-in Management + +```python +# Create opt-in parameters +opt_in_params = client.params.opt_in( + method="initialize", # Optional: Method to call during opt-in + args=["initial_value"], # Optional: Arguments for initialization + boxes=[("user_data", "ACCOUNT_ADDRESS")], # Optional: Box allocation +) + +# Create and send opt-in transaction +transaction = client.create_transaction.opt_in(opt_in_params) +result = client.send.opt_in(opt_in_params) + +# Check if account is opted in +is_opted_in = client.is_opted_in("ACCOUNT_ADDRESS") + +# Create close-out parameters +close_out_params = client.params.close_out( + method="cleanup", # Optional: Method to call during close-out + args=["cleanup_value"], # Optional: Arguments for cleanup +) + +# Create and send close-out transaction +transaction = client.create_transaction.close_out(close_out_params) +result = client.send.close_out(close_out_params) ``` -### Debug Mode and traces Field -When debug mode is active, the LogicError will contain a field named traces. This field will include raw simulate execution traces, providing a detailed account of the transaction simulation. These traces are crucial for diagnosing complex issues and are automatically included in all application client calls when debug mode is active. +## Default arguments -```{note} -Remember to enable debug mode (`config.debug = True`) to include raw simulate execution traces in the `LogicError`. -``` \ No newline at end of file +If an ABI method call specifies default argument values for any of its arguments you can pass in `None` for the value of that argument for the default value to be automatically populated. diff --git a/docs/source/capabilities/app-manager.md b/docs/source/capabilities/app-manager.md new file mode 100644 index 00000000..3fcfd4fa --- /dev/null +++ b/docs/source/capabilities/app-manager.md @@ -0,0 +1,160 @@ +# App management + +App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes). + +## `AppManager` + +The `AppManager` is a class that is used to manage app information. To get an instance of `AppManager` you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.app` or instantiate it directly (passing in an algod client instance): + +```python +from algokit_utils import AppManager + +app_manager = AppManager(algod_client) +``` + +## Calling apps + +### App Clients + +The recommended way of interacting with apps is via [App clients](./app-client.md) and [App factory](./app-client.md#appfactory). The methods shown on this page are the underlying mechanisms that app clients use and are for advanced use cases when you want more control. + +### Compilation + +The `AppManager` class allows you to compile TEAL code with caching semantics that allows you to avoid duplicate compilation and keep track of source maps from compiled code. + +```python +# Basic compilation +teal_code = "return 1" +compilation_result = app_manager.compile_teal(teal_code) + +# Get cached compilation result +cached_result = app_manager.get_compilation_result(teal_code) + +# Compile with template substitution +template_code = "int TMPL_VALUE" +template_params = {"VALUE": 1} +compilation_result = app_manager.compile_teal_template( + template_code, + template_params=template_params +) + +# Compile with deployment metadata (for updatable/deletable control) +deployment_metadata = {"updatable": True, "deletable": True} +compilation_result = app_manager.compile_teal_template( + template_code, + deployment_metadata=deployment_metadata +) +``` + +The compilation result contains: + +- `teal` - Original TEAL code +- `compiled` - Base64 encoded compiled bytecode +- `compiled_hash` - Hash of compiled bytecode +- `compiled_base64_to_bytes` - Raw bytes of compiled bytecode +- `source_map` - Source map for debugging + +## Accessing state + +### Global state + +To access global state you can use: + +```python +# Get global state for app +global_state = app_manager.get_global_state(app_id) + +# Parse raw state from algod +decoded_state = AppManager.decode_app_state(raw_state) + +# Access state values +key_raw = decoded_state["value1"].key_raw # Raw bytes +key_base64 = decoded_state["value1"].key_base64 # Base64 encoded +value = decoded_state["value1"].value # Parsed value (str or int) +value_raw = decoded_state["value1"].value_raw # Raw bytes if bytes value +value_base64 = decoded_state["value1"].value_base64 # Base64 if bytes value +``` + +### Local state + +To access local state you can use: + +```python +local_state = app_manager.get_local_state(app_id, "ACCOUNT_ADDRESS") +``` + +### Boxes + +To access box storage: + +```python +# Get box names +box_names = app_manager.get_box_names(app_id) + +# Get box values +box_value = app_manager.get_box_value(app_id, box_name) +box_values = app_manager.get_box_values(app_id, [box_name1, box_name2]) + +# Get decoded ABI values +abi_value = app_manager.get_box_value_from_abi_type( + app_id, box_name, algosdk.abi.StringType() +) +abi_values = app_manager.get_box_values_from_abi_type( + app_id, [box_name1, box_name2], algosdk.abi.StringType() +) + +# Get box reference for transaction +box_ref = AppManager.get_box_reference(box_id) +``` + +## Getting app information + +To get app information: + +```python +# Get app info by ID +app_info = app_manager.get_by_id(app_id) + +# Get ABI return value from transaction +abi_return = AppManager.get_abi_return(confirmation, abi_method) +``` + +## Box references + +Box references can be specified in several ways: + +```python +# String name (encoded to bytes) +box_ref = "my_box" + +# Raw bytes +box_ref = b"my_box" + +# Account signer (uses address as name) +box_ref = account_signer + +# Box reference with app ID +box_ref = BoxReference(app_id=123, name="my_box") +``` + +## Common app parameters + +When interacting with apps (creating, updating, deleting, calling), there are common parameters that can be passed: + +- `app_id` - ID of the application +- `sender` - Address of transaction sender +- `signer` - Transaction signer (optional) +- `args` - Arguments to pass to the smart contract +- `account_references` - Account addresses to reference +- `app_references` - App IDs to reference +- `asset_references` - Asset IDs to reference +- `box_references` - Box references to load +- `on_complete` - On complete action +- Other common transaction parameters like `note`, `lease`, etc. + +For ABI method calls, additional parameters: + +- `method` - The ABI method to call +- `args` - ABI typed arguments to pass + +See [App client](./app-client.md) for more details on constructing app calls. diff --git a/docs/source/capabilities/client.md b/docs/source/capabilities/client.md index 851c66ff..08614682 100644 --- a/docs/source/capabilities/client.md +++ b/docs/source/capabilities/client.md @@ -1,29 +1,107 @@ # Client management -Client management is one of the core capabilities provided by AlgoKit Utils. -It allows you to create [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) -and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. +Client management is one of the core capabilities provided by AlgoKit Utils. It allows you to create (auto-retry) [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. -Any AlgoKit Utils function that needs one of these clients will take the underlying `algosdk` classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, -`algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the -Client management capability if you prefer. +Any AlgoKit Utils function that needs one of these clients will take the underlying algosdk classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, `algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the Client management capability if you prefer. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_network_clients.py). +## `ClientManager` + +The `ClientManager` is a class that is used to manage client instances. + +To get an instance of `ClientManager` you can instantiate it directly: + +```python +from algokit_utils import ClientManager + +# Algod client only +client_manager = ClientManager(algod=algod_client) +# All clients +client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) +# Algod config only +client_manager = ClientManager(algod_config=algod_config) +# All client configs +client_manager = ClientManager(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) +``` + ## Network configuration -The network configuration is specified using the `AlgoClientConfig` class. This same interface is used to specify the config for algod, indexer and kmd clients. +The network configuration is specified using the `AlgoClientConfig` type. This same type is used to specify the config for [algod](https://developer.algorand.org/docs/sdks/python/), [indexer](https://developer.algorand.org/docs/sdks/python/) and [kmd](https://developer.algorand.org/docs/sdks/python/) SDK clients. There are a number of ways to produce one of these configuration objects: -- Manually creating the object, e.g. `AlgoClientConfig(server="https://myalgodnode.com", token="SECRET_TOKEN")` -- `algokit_utils.get_algonode_config(network, config, token)`: Loads an Algod or indexer config against [Nodely](https://nodely.io/docs/free/start) to either MainNet or TestNet -- `algokit_utils.get_default_localnet_config(configOrPort)`: Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration +- Manually specifying a dictionary that conforms with the type, e.g. + ```python + { + "server": "https://myalgodnode.com" + } + # Or with the optional values: + { + "server": "https://myalgodnode.com", + "port": 443, + "token": "SECRET_TOKEN" + } + ``` +- `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables +- `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algonode_config(network)` - Loads an Algod or indexer config against [AlgoNode free tier](https://nodely.io/docs/free/start) to either MainNet or TestNet +- `ClientManager.get_default_localnet_config()` - Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration ## Clients -Once you have the configuration for a client, to get the client you can use the following functions: +### Creating an SDK client instance + +Once you have the configuration for a client, to get a new client you can use the following functions: + +- `ClientManager.get_algod_client(config)` - Returns an Algod client for the given configuration; the client automatically retries on transient HTTP errors +- `ClientManager.get_indexer_client(config)` - Returns an Indexer client for given configuration +- `ClientManager.get_kmd_client(config)` - Returns a Kmd client for the given configuration + +You can also shortcut needing to write the likes of `ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())` with environment shortcut methods: + +- `ClientManager.get_algod_client_from_environment()` - Returns an Algod client by loading the config from environment variables +- `ClientManager.get_indexer_client_from_environment()` - Returns an indexer client by loading the config from environment variables +- `ClientManager.get_kmd_client_from_environment()` - Returns a kmd client by loading the config from environment variables + +### Accessing SDK clients via ClientManager instance + +Once you have a `ClientManager` instance, you can access the SDK clients: + +```python +client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) + +algod_client = client_manager.algod +indexer_client = client_manager.indexer +kmd_client = client_manager.kmd +``` + +If the method to create the `ClientManager` doesn't configure indexer or kmd (both of which are optional), then accessing those clients will trigger an error. + +### Creating a TestNet dispenser API client instance + +You can also create a [TestNet dispenser API client instance](./dispenser-client.md) from `ClientManager` too. + +## Automatic retry + +When receiving an Algod or Indexer client from AlgoKit Utils, it will be a special wrapper client that handles retrying transient failures. + +## Network information + +You can get information about the current network you are connected to: + +```python +# Get network information +network = client_manager.network() +print(f"Connected to: {network.name}") # e.g., "mainnet", "testnet", "localnet" +print(f"Genesis ID: {network.genesis_id}") +print(f"Genesis hash: {network.genesis_hash}") + +# Check specific network types +is_mainnet = client_manager.is_mainnet() +is_testnet = client_manager.is_testnet() +is_localnet = client_manager.is_localnet() +``` -- `algokit_utils.get_algod_client(config)`: Returns an Algod client for the given configuration or if none is provided retrieves a configuration from the environment using `ALGOD_SERVER`, `ALGOD_TOKEN` and optionally `ALGOD_PORT`. -- `algokit_utils.get_indexer_client(config)`: Returns an Indexer client for given configuration or if none is provided retrieves a configuration from the environment using `INDEXER_SERVER`, `INDEXER_TOKEN` and optionally `INDEXER_PORT` -- `algokit_utils.get_kmd_client_from_algod_client(config)`: - Returns a Kmd client based on the provided algod client configuration, with the assumption the KMD services is running on the same host but a different port (either `KMD_PORT` environment variable or `4002` by default) +The first time `network()` is called it will make a HTTP call to algod to get the network parameters, but from then on it will be cached within that `ClientManager` instance for subsequent calls. diff --git a/docs/source/capabilities/debugger.md b/docs/source/capabilities/debugger.md index 96ff7d22..85b87c22 100644 --- a/docs/source/capabilities/debugger.md +++ b/docs/source/capabilities/debugger.md @@ -4,43 +4,75 @@ The AlgoKit Python Utilities package provides a set of debugging tools that can ## Configuration -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: - -- `debug`: Indicates whether debug mode is enabled. -- `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. -- `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. -- `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. -- `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. - -The `configure` method can be used to set these attributes. +The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. To enable debug mode in your project you can configure it as follows: -```py +```python from algokit_utils.config import config config.configure(debug=True) ``` -## Debugging Utilities +## Configuration Options + +The `UpdatableConfig` class provides several configuration options that affect debugging behavior: + +- `debug` (bool): Indicates whether debug mode is enabled. +- `project_root` (Path | None): The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with AlgoKit AVM Debugger. Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. +- `trace_all` (bool): Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. +- `trace_buffer_size_mb` (float): The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. +- `max_search_depth` (int): The maximum depth to search for an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: +You can configure these options as follows: -- `persist_sourcemaps`: This method persists the sourcemaps for the given sources as AVM Debugger compliant artifacts. It takes a list of `PersistSourceMapInput` objects, a `Path` object representing the root directory of the project, an `AlgodClient` object for interacting with the Algorand blockchain, and a boolean indicating whether to dump teal source files along with sourcemaps. -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. +```python +config.configure( + debug=True, + project_root=Path("./my-project"), + trace_all=True, + trace_buffer_size_mb=512, + max_search_depth=15 +) +``` + +## Debugging Utilities + +When debug mode is enabled, AlgoKit Utils will automatically: + +1. Store simulation traces for transactions that fail (by default) or all transactions (if `trace_all=True`) +2. Save these traces in the `debug_traces` folder within your project root +3. Manage the trace buffer size to prevent excessive disk usage + +The following methods are provided for scenarios where you want to manually persist sourcemaps and traces: + +- `persist_sourcemaps`: This method persists the sourcemaps for the + given sources as AVM Debugger compliant artifacts. It takes a list of + `PersistSourceMapInput` objects, a `Path` object representing the root + directory of the project, an `AlgodClient` object for interacting with the + Algorand blockchain, and a boolean indicating whether to dump teal source + files along with sourcemaps. +- `simulate_and_persist_response`: This method simulates the atomic + transactions using the provided `AtomicTransactionComposer` object and + `AlgodClient` object, and persists the simulation response to an AVM + Debugger compliant JSON file. It takes an `AtomicTransactionComposer` + object representing the atomic transactions to be simulated and persisted, + a `Path` object representing the root directory of the project, an + `AlgodClient` object representing the Algorand client, and a float + representing the size of the trace buffer in megabytes. ### Trace filename format The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; +``` +${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json ``` Where: -- `timestamp`: The time when the trace file was created, in ISO 8601 format, with colons and periods removed. -- `last_round`: The last round when the simulation was performed. -- `transaction_types`: A string representing the types and counts of transactions in the atomic group. Each transaction type is represented as `${count}#${type}`, and different transaction types are separated by underscores. +- `timestamp`: The time when the trace file was created, in ISO 8601 format, with colons and periods removed +- `last_round`: The last round when the simulation was performed +- `transaction_types`: A string representing the types and counts of transactions in the atomic group. Each transaction type is represented as `${count}#${type}`, and different transaction types are separated by underscores For example, a trace file might be named `20220301T123456Z_lr1000_2#pay_1#axfer.trace.avm.json`, indicating that the trace file was created at `2022-03-01T12:34:56Z`, the last round was `1000`, and the atomic group contained 2 payment transactions and 1 asset transfer transaction. diff --git a/docs/source/capabilities/dispenser-client.md b/docs/source/capabilities/dispenser-client.md index 315b52f4..d9370f0a 100644 --- a/docs/source/capabilities/dispenser-client.md +++ b/docs/source/capabilities/dispenser-client.md @@ -7,54 +7,85 @@ The TestNet Dispenser Client is a utility for interacting with the AlgoKit TestN To create a Dispenser Client, you need to provide an authorization token. This can be done in two ways: 1. Pass the token directly to the client constructor as `auth_token`. -2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) on how to obtain the token). +2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) on how to obtain the token). If both methods are used, the constructor argument takes precedence. -```py +```python +import algokit_utils + +# With auth token +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", +) + +# With auth token and timeout +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", + request_timeout=2, # seconds +) + +# From environment variables +# i.e. os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' +dispenser = algorand.client.get_testnet_dispenser_from_environment() + +# Alternatively, you can construct it directly from algokit_utils import TestNetDispenserApiClient # Using constructor argument - client = TestNetDispenserApiClient(auth_token="your_auth_token") # Using environment variable - import os -os.environ["ALGOKIT_DISPENSER_ACCESS_TOKEN"] = "your_auth_token" +os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' client = TestNetDispenserApiClient() ``` ## Funding an Account -To fund an account with Algos from the dispenser API, use the `fund` method. This method requires the receiver's address, the amount to be funded, and the asset ID. +To fund an account with Algo from the dispenser API, use the `fund` method. This method requires the receiver's address and the amount to be funded. -```py -response = client.fund(address="receiver_address", amount=1000, asset_id=0) +```python +response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, # Amount in microAlgos +) ``` -The `fund` method returns a `FundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. +The `fund` method returns a `DispenserFundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. ## Registering a Refund To register a refund for a transaction with the dispenser API, use the `refund` method. This method requires the transaction ID of the refund transaction. -```py -client.refund(refund_txn_id="transaction_id") +```python +dispenser.refund("transaction_id") ``` -> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by send funds back to TestNet Dispenser, then you can invoke this `refund` endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the `sender` field of any issued `fund` transaction initiated via [`fund`](#funding-an-account). +> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by sending funds back to TestNet Dispenser, then you can invoke this refund endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the sender field of any issued fund transaction initiated via [fund](#funding-an-account). ## Getting Current Limit -To get the current limit for an account with Algos from the dispenser API, use the `get_limit` method. This method requires the account address. +To get the current limit for an account with Algo from the dispenser API, use the `get_limit` method. -```py -response = client.get_limit(address="account_address") +```python +response = dispenser.get_limit() ``` -The `get_limit` method returns a `LimitResponse` object, which contains the current limit amount. +The `get_limit` method returns a `DispenserLimitResponse` object, which contains the current limit amount. ## Error Handling If an error occurs while making a request to the dispenser API, an exception will be raised with a message indicating the type of error. Refer to [Error Handling docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) for details on how you can handle each individual error `code`. + +Here's an example of handling errors: + +```python +try: + response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, + ) +except Exception as e: + print(f"Error occurred: {str(e)}") +``` diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md new file mode 100644 index 00000000..0e6a3331 --- /dev/null +++ b/docs/source/capabilities/transaction-composer.md @@ -0,0 +1,227 @@ +# Transaction composer + +The `TransactionComposer` class allows you to easily compose one or more compliant Algorand transactions and execute and/or simulate them. + +It's the core of how the `AlgorandClient` class composes and sends transactions. + +```python +from algokit_utils import TransactionComposer, AppManager +from algokit_utils.transactions import ( + PaymentParams, + AppCallMethodCallParams, + AssetCreateParams, + AppCreateParams, + # ... other transaction parameter types +) +``` + +To get an instance of `TransactionComposer` you can either get it from an app client, from an `AlgorandClient`, or by instantiating via the constructor. + +```python +# From AlgorandClient +composer_from_algorand = algorand.new_group() + +# From AppClient +composer_from_app_client = app_client.algorand.new_group() + +# From constructor +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer +) + +# From constructor with optional params +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer, + # Custom function to get suggested params + get_suggested_params=lambda: algod.suggested_params(), + # Number of rounds the transaction should be valid for + default_validity_window=1000, + # Optional AppManager instance for TEAL compilation + app_manager=AppManager(algod) +) +``` + +## Constructing a transaction + +To construct a transaction you need to add it to the composer, passing in the relevant params object for that transaction. Params are Python dataclasses aavailable for import from `algokit_utils.transactions`. + +Parameter types include: + +- `PaymentParams` - For ALGO transfers +- `AssetCreateParams` - For creating ASAs +- `AssetConfigParams` - For reconfiguring ASAs +- `AssetTransferParams` - For ASA transfers +- `AssetOptInParams` - For opting in to ASAs +- `AssetOptOutParams` - For opting out of ASAs +- `AssetDestroyParams` - For destroying ASAs +- `AssetFreezeParams` - For freezing ASA balances +- `AppCreateParams` - For creating applications +- `AppCreateMethodCallParams` - For creating applications with ABI method calls +- `AppCallParams` - For calling applications +- `AppCallMethodCallParams` - For calling ABI methods on applications +- `AppUpdateParams` - For updating applications +- `AppUpdateMethodCallParams` - For updating applications with ABI method calls +- `AppDeleteParams` - For deleting applications +- `AppDeleteMethodCallParams` - For deleting applications with ABI method calls +- `OnlineKeyRegistrationParams` - For online key registration transactions + +The methods to construct a transaction are all named `add_{transaction_type}` and return an instance of the composer so they can be chained together fluently to construct a transaction group. + +For example: + +```python +from algokit_utils import AlgoAmount +from algokit_utils.transactions import AppCallMethodCallParams, PaymentParams + +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100), + note=b"Payment note" + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + boxes=[box_reference] # Optional box references + )) +) +``` + +## Simulating a transaction + +Transactions can be simulated using the simulate endpoint in algod, which enables evaluating the transaction on the network without it actually being committed to a block. +This is a powerful feature, which has a number of options which are detailed in the [simulate API docs](https://developer.algorand.org/docs/rest-apis/algod/#post-v2transactionssimulate). + +The `simulate()` method accepts several optional parameters that are passed through to the algod simulate endpoint: + +- `allow_more_logs: bool | None` - Allow more logs than standard +- `allow_empty_signatures: bool | None` - Allow transactions without signatures +- `allow_unnamed_resources: bool | None` - Allow unnamed resources in app calls +- `extra_opcode_budget: int | None` - Additional opcode budget +- `exec_trace_config: SimulateTraceConfig | None` - Execution trace configuration +- `simulation_round: int | None` - Round to simulate at +- `skip_signatures: int | None` - Skip signature verification + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate() +) + +# Access simulation results +simulate_response = result.simulate_response +confirmations = result.confirmations +transactions = result.transactions +returns = result.returns # ABI returns if any +``` + +### Simulate without signing + +There are situations where you may not be able to (or want to) sign the transactions when executing simulate. +In these instances you should set `skip_signatures=True` which automatically builds empty transaction signers and sets both `fix-signers` and `allow-empty-signatures` to `True` when sending the algod API call. + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate( + skip_signatures=True, + allow_more_logs=True, # Optional: allow more logs + extra_opcode_budget=700 # Optional: increase opcode budget + ) +) +``` + +### Resource Population + +The `TransactionComposer` includes automatic resource population capabilities for application calls. When sending or simulating transactions, it can automatically detect and populate required references for: + +- Account references +- Application references +- Asset references +- Box references + +This happens automatically when either: + +1. The global `algokit_utils.config` instance is set to `populate_app_call_resources=True` (default is `False`) +2. The `populate_app_call_resources` parameter is explicitly passed as `True` when sending transactions + +```python +# Automatic resource population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + # Resources will be automatically populated! + )) + .send(populate_app_call_resources=True) +) + +# Or disable automatic population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + # Explicitly specify required resources + account_references=["ACCOUNT"], + app_references=[456], + asset_references=[789], + box_references=[box_reference] + )) + .send(populate_app_call_resources=False) +) +``` + +The resource population: + +- Respects the maximum limits (4 for accounts, 8 for foreign references) +- Handles cross-reference resources efficiently (e.g., asset holdings and local state) +- Automatically distributes resources across multiple transactions in a group when needed +- Raises descriptive errors if resource limits are exceeded + +This feature is particularly useful when: + +- Working with complex smart contracts that access various resources +- Building transaction groups where resources need to be coordinated +- Developing applications where resource requirements may change dynamically + +Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. diff --git a/docs/source/capabilities/transaction.md b/docs/source/capabilities/transaction.md new file mode 100644 index 00000000..34f3c45d --- /dev/null +++ b/docs/source/capabilities/transaction.md @@ -0,0 +1,147 @@ +# Transaction management + +Transaction management is one of the core capabilities provided by AlgoKit Utils. It allows you to construct, simulate and send single or grouped transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, multiple sender account types, and sending behavior. + +## Transaction Results + +All AlgoKit Utils functions that send transactions return either a `SendSingleTransactionResult` or `SendAtomicTransactionComposerResults`, providing consistent mechanisms to interpret transaction outcomes. + +### SendSingleTransactionResult + +The base `SendSingleTransactionResult` class is used for single transactions: + +```python +@dataclass(frozen=True, kw_only=True) +class SendSingleTransactionResult: + transaction: TransactionWrapper # Last transaction + confirmation: AlgodResponseType # Last confirmation + group_id: str + tx_id: str | None = None + tx_ids: list[str] # Full array of transaction IDs + transactions: list[TransactionWrapper] + confirmations: list[AlgodResponseType] + returns: list[ABIReturn] | None = None +``` + +Common variations include: + +- `SendSingleAssetCreateTransactionResult` - Adds `asset_id` +- `SendAppTransactionResult` - Adds `abi_return` +- `SendAppUpdateTransactionResult` - Adds compilation results +- `SendAppCreateTransactionResult` - Adds `app_id` and `app_address` + +### SendAtomicTransactionComposerResults + +When using the atomic transaction composer directly via `TransactionComposer.send()` or `TransactionComposer.simulate()`, you'll receive a `SendAtomicTransactionComposerResults`: + +```python +@dataclass +class SendAtomicTransactionComposerResults: + group_id: str # The group ID if this was a transaction group + confirmations: list[AlgodResponseType] # The confirmation info for each transaction + tx_ids: list[str] # The transaction IDs that were sent + transactions: list[TransactionWrapper] # The transactions that were sent + returns: list[ABIReturn] # The ABI return values from any ABI method calls + simulate_response: dict[str, Any] | None = None # Simulation response if simulated +``` + +### Application-specific Result Types + +When working with applications via `AppClient` or `AppFactory`, you'll get enhanced result types that provide direct access to parsed ABI values: + +- `SendAppFactoryTransactionResult` +- `SendAppUpdateFactoryTransactionResult` +- `SendAppCreateFactoryTransactionResult` + +These types extend the base transaction results to add an `abi_value` field that contains the parsed ABI return value according to the ARC-56 specification. The `Arc56ReturnValueType` can be: + +- A primitive ABI value (bool, int, str, bytes) +- An ABI struct (as a Python dict) +- None (for void returns) + +### Where You'll Encounter Each Result Type + +Different interfaces return different result types: + +1. **Direct Transaction Composer** + + - `TransactionComposer.send()` → `SendAtomicTransactionComposerResults` + - `TransactionComposer.simulate()` → `SendAtomicTransactionComposerResults` + +2. **AlgorandClient Methods** + + - `.send.payment()` → `SendSingleTransactionResult` + - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` + - `.send.app_create()` → `SendAppCreateTransactionResult` + - `.send.app_update()` → `SendAppUpdateTransactionResult` + +3. **AppClient Methods** + + - `.call()` → `SendAppTransactionResult` + - `.create()` → `SendAppCreateTransactionResult` + - `.update()` → `SendAppUpdateTransactionResult` + +4. **AppFactory Methods** + - `.create()` → `SendAppCreateFactoryTransactionResult` + - `.call()` → `SendAppFactoryTransactionResult` + - `.update()` → `SendAppUpdateFactoryTransactionResult` + +Example usage with AppFactory for easy access to ABI returns: + +```python +# Using AppFactory +result = app_factory.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Access the parsed ABI return value directly +parsed_value = result.abi_value # Already decoded per ARC-56 spec + +# Compared to base AppClient where you need to parse manually +base_result = app_client.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Need to manually handle ABI return parsing +if base_result.abi_return: + parsed_value = base_result.abi_return.value +``` + +Key differences between result types: + +1. **Base Transaction Results** (`SendSingleTransactionResult`) + + - Focus on transaction confirmation details + - Include group support but optimized for single transactions + - No direct ABI value parsing + +2. **Atomic Transaction Results** (`SendAtomicTransactionComposerResults`) + + - Built for transaction groups + - Include simulation support + - Raw ABI returns via `.returns` + - No single transaction convenience fields + +3. **Application Results** (`SendAppTransactionResult` family) + + - Add application-specific fields (`app_id`, compilation results) + - Include raw ABI returns via `.abi_return` + - Base application transaction support + +4. **Factory Results** (`SendAppFactoryTransactionResult` family) + - Highest level of abstraction + - Direct access to parsed ABI values via `.abi_value` + - Automatic ARC-56 compliant value parsing + - Combines app-specific fields with parsed ABI returns + +## Further reading + +To understand how to create, simulate and send transactions consult: + +- The [`TransactionComposer`](./transaction-composer.md) documentation for composing transaction groups +- The [`AlgorandClient`](./algorand-client.md) documentation for a high-level interface to send transactions + +The transaction composer documentation covers the details of constructing transactions and transaction groups, while the Algorand client documentation covers the high-level interface for sending transactions. diff --git a/docs/source/capabilities/transfer.md b/docs/source/capabilities/transfer.md index af28b6d1..0e965981 100644 --- a/docs/source/capabilities/transfer.md +++ b/docs/source/capabilities/transfer.md @@ -1,58 +1,145 @@ -# Algo transfers - -Algo transfers is a higher-order use case capability provided by AlgoKit Utils allows you to easily initiate algo transfers between accounts, including dispenser management and -idempotent account funding. - -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_transfer.py). - -## Transferring Algos - -The key function to facilitate Algo transfers is `algokit.transfer(algod_client, transfer_parameters)`, which returns the underlying `EnsureFundedResponse` and takes a `TransferParameters` - -The following fields on `TransferParameters` are required to transfer ALGOs: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `micro_algos`: The amount of micro ALGOs to send - -## Ensuring minimum Algos - -The ability to automatically fund an account to have a minimum amount of disposable ALGOs to spend is incredibly useful for automation and deployment scripts. -The function to facilitate this is `ensure_funded(client, parameters)`, which takes an `EnsureBalanceParameters` instance and returns the underlying `EnsureFundedResponse` if a payment was made, a string if the dispenser API was used, or None otherwise. - -The following fields on `EnsureBalanceParameters` are required to ensure minimum ALGOs: - -- `account_to_fund`: The account address that will receive the ALGOs. This can be an `Account` instance, an `AccountTransactionSigner` instance, or a string. -- `min_spending_balance_micro_algos`: The minimum balance of micro ALGOs that the account should have available to spend (i.e. on top of minimum balance requirement). -- `min_funding_increment_micro_algos`: When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets called often on an active account). Default is 0. -- `funding_source`: The account (with private key) or signer that will send the ALGOs. If not set, it will use `get_dispenser_account`. This can be an `Account` instance, an `AccountTransactionSigner` instance, [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) instance, or None. -- `suggested_params`: (optional) Transaction parameters, an instance of `SuggestedParams`. -- `note`: (optional) The transaction note, default is "Funding account to meet minimum requirement". -- `fee_micro_algos`: (optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call. -- `max_fee_micro_algos`: (optional) The maximum fee that you are happy to pay (default: unbounded). If this is set it's possible the transaction could get rejected during network congestion. - -The function calls Algod to find the current balance and minimum balance requirement, gets the difference between those two numbers and checks to see if it's more than the `min_spending_balance_micro_algos`. If so, it will send the difference, or the `min_funding_increment_micro_algos` if that is specified. If the account is on TestNet and `use_dispenser_api` is True, the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md) will be used to fund the account. - -> Please note, if you are attempting to fund via Dispenser API, make sure to set `ALGOKIT_DISPENSER_ACCESS_TOKEN` environment variable prior to invoking `ensure_funded`. To generate the token refer to [AlgoKit CLI documentation](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) - -## Transfering Assets - -The key function to facilitate asset transfers is `transfer_asset(algod_client, transfer_parameters)`, which returns a `AssetTransferTxn` and takes a `TransferAssetParameters`: - -The following fields on `TransferAssetParameters` are required to transfer assets: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `asset_id`: The asset id that will be transfered -- `amount`: The amount to send as the smallest divisible unit value +# Algo transfers (payments) + +Algo transfers, or [payments](https://developer.algorand.org/docs/get-details/transactions/#payment-transaction), is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [Algo amount handling](./amount.md) and [Transaction management](./transaction.md). It allows you to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding. + +To see some usage examples check out the automated tests in the repository. + +## `payment` + +The key function to facilitate Algo transfers is `algorand.send.payment(params)` (immediately send a single payment transaction), `algorand.create_transaction.payment(params)` (construct a payment transaction), or `algorand.new_group().add_payment(params)` (add payment to a group of transactions) per [`AlgorandClient`](./algorand-client.md) [transaction semantics](./algorand-client.md#creating-and-issuing-transactions). + +The base type for specifying a payment transaction is `PaymentParams`, which has the following parameters in addition to the [common transaction parameters](./algorand-client.md#transaction-parameters): + +- `receiver: str` - The address of the account that will receive the Algo +- `amount: AlgoAmount` - The amount of Algo to send +- `close_remainder_to: Optional[str]` - If given, close the sender account and send the remaining balance to this address (**warning:** use this carefully as it can result in loss of funds if used incorrectly) + +```python +# Minimal example +result = algod.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo") + ) +) + +# Advanced example +result2 = algod.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo"), + close_remainder_to="CLOSEREMAINDERTOADDRESS", + lease="lease", + note=b"note", + # Use this with caution, it's generally better to use algod.account.rekey_account + rekey_to="REKEYTOADDRESS", + # You wouldn't normally set this field + first_valid_round=1000, + validity_window=10, + extra_fee=AlgoAmount(1000, "microalgo"), + static_fee=AlgoAmount(1000, "microalgo"), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee=AlgoAmount(3000, "microalgo"), + # Signer only needed if you want to provide one, + # generally you'd register it with AlgorandClient + # against the sender and not need to pass it in + signer=transaction_signer, + max_rounds_to_wait=5, + suppress_log=True, + ) +) +``` + +## `ensure_funded` + +The `ensure_funded` function automatically funds an account to maintain a minimum amount of [disposable Algo](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance). This is particularly useful for automation and deployment scripts that get run multiple times and consume Algo when run. + +There are 3 variants of this function: + +- `algod.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algod.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + - **Note:** requires environment variables to be set. + - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` + if it's a rekeyed account, or against default LocalNet if no environment variables present. +- `algod.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +The general structure of these calls is similar, they all take: + +- `account_to_fund: str | Account` - Address or signing account of the account to fund +- The source (dispenser): + - In `ensure_funded`: `dispenser_account: str | Account` - the address or signing account of the account to use as a dispenser + - In `ensure_funded_from_environment`: Not specified, loaded automatically from the ephemeral environment + - In `ensure_funded_from_testnet_dispenser_api`: `dispenser_client: TestNetDispenserApiClient` - a client instance of the TestNet dispenser API +- `min_spending_balance: AlgoAmount` - The minimum balance of Algo that the account should have available to spend (i.e., on top of the minimum balance requirement) +- An `options` object, which has: + - [Common transaction parameters](./algorand-client.md#transaction-parameters) (not for TestNet Dispenser API) + - [Execution parameters](./algorand-client.md#sending-a-single-transaction) (not for TestNet Dispenser API) + - `min_funding_increment: Optional[AlgoAmount]` - When issuing a funding amount, the minimum amount to transfer; this avoids many small transfers if this function gets called often on an active account + +### Examples + +```python +# From account + +# Basic example +algod.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +# With configuration +algod.account.ensure_funded( + "ACCOUNTADDRESS", + "DISPENSERADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + suppress_log=True, +) + +# From environment + +# Basic example +algod.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +# With configuration +algod.account.ensure_funded_from_environment( + "ACCOUNTADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + suppress_log=True, +) + +# TestNet Dispenser API + +# Basic example +algod.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algod.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo") +) +# With configuration +algod.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algod.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), +) +``` + +All 3 variants return an `EnsureFundedResponse` (and the first two also return a [single transaction result](./algorand-client.md#sending-a-single-transaction)) if a funding transaction was needed, or `None` if no transaction was required: + +- `amount_funded: AlgoAmount` - The number of Algo that was paid +- `transaction_id: str` - The ID of the transaction that funded the account + +If you are using the TestNet Dispenser API then the `transaction_id` is useful if you want to use the [refund functionality](./dispenser-client.md#registering-a-refund). ## Dispenser -If you want to programmatically send funds then you will often need a "dispenser" account that has a store of ALGOs that can be sent and a private key available for that dispenser account. +If you want to programmatically send funds to an account so it can transact then you will often need a "dispenser" account that has a store of Algo that can be sent and a private key available for that dispenser account. -There is a standard AlgoKit Utils function to get access to a [dispenser account](./account.md#account): `get_dispenser_account`. When running against -[LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md), the dispenser account can be automatically determined using the -[Kmd API](https://developer.algorand.org/docs/rest-apis/kmd). When running against other networks like TestNet or MainNet the mnemonic of the dispenser account can be provided via environment -variable `DISPENSER_MNEMONIC` +There's a number of ways to get a dispensing account in AlgoKit Utils: -Please note that this does not refer to the [AlgoKit TestNet Dispenser API](./dispenser-client.md) which is a separate abstraction that can be used to fund accounts on TestNet via dedicated API service. +- Get a dispenser via [account manager](./account.md#dispenser) - either automatically from [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) or from the environment +- By programmatically creating one of the many account types via [account manager](./account.md#accounts) +- By programmatically interacting with [KMD](./account.md#kmd-account-management) if running against LocalNet +- By using the [AlgoKit TestNet Dispenser API client](./dispenser-client.md) which can be used to fund accounts on TestNet via a dedicated API service diff --git a/docs/source/index.md b/docs/source/index.md index ede55d1b..50bb0ea8 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,16 +1,14 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. -This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. -Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. ```{note} If you prefer TypeScript there's an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). ``` -[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) +[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Config and logging](#config-and-logging) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) ```{toctree} --- @@ -25,6 +23,12 @@ capabilities/app-deploy capabilities/transfer capabilities/dispenser-client capabilities/debugger +capabilities/asset +capabilities/testing +capabilities/indexer +capabilities/transaction +capabilities/amount +capabilities/app apidocs/algokit_utils/algokit_utils ``` @@ -32,21 +36,21 @@ apidocs/algokit_utils/algokit_utils # Core principles -This library is designed with the following principles: +This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles) and is designed with the following principles: -- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are - exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. -- **Type-safety** - This library provides strong TypeScript support with effort put into creating types that provide good type safety and intellisense. -- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write +- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. +- **Type-safety** - This library provides strong type hints with effort put into creating types that provide good type safety and intellisense when used with tools like MyPy. +- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write. (installation)= # Installation -This library can be installed from PyPi using pip or poetry, e.g.: +This library can be installed from PyPi using pip or poetry: -``` +```bash pip install algokit-utils +# or poetry add algokit-utils ``` @@ -54,49 +58,117 @@ poetry add algokit-utils # Usage -To use this library simply include the following at the top of your file: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: ```python -import algokit_utils +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() ``` -Then you can use intellisense to auto-complete the various functions and types that are available by typing `algokit_utils.` in your favourite Integrated Development Environment (IDE), -or you can refer to the [reference documentation](apidocs/algokit_utils/algokit_utils.md). +# Config and logging -## Types +The library provides configuration and logging capabilities through the `config` module: -The library contains extensive type hinting combined with a tool like MyPy this can help identify issues where incorrect types have been used, or used incorrectly. +```python +from algokit_utils.config import config + +# Enable debug mode +config.configure(debug=True) +# Configure project root for debug traces +config.configure(project_root=Path("./my-project")) +# Enable tracing of all operations +config.configure(trace_all=True) +``` (capabilities)= # Capabilities -The library helps you with the following capabilities: +The library provides a comprehensive set of capabilities to interact with Algorand: + +## Core capabilities + +### Client Management + +- Create and manage algod, indexer and kmd clients +- Auto-retry functionality for transient errors +- Environment-based configuration +- Network detection and information + +### Account Management + +- Create and manage various account types (mnemonic, multisig, rekeyed) +- Transaction signing and management +- KMD integration for LocalNet +- Environment variable injection + +### Transaction Management + +- Atomic transaction composition +- Transaction simulation +- Automatic resource population +- Fee management +- ABI method call support + +### Amount Handling -- Core capabilities - - [**Client management**](capabilities/client.md) - Creation of algod, indexer and kmd clients against various networks resolved from environment or specified configuration - - [**Account management**](capabilities/account.md) - Creation and use of accounts including mnemonic, multisig, transaction signer, idempotent KMD accounts and environment variable injected -- Higher-order use cases - - [**ARC-0032 Application Spec client**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities to provide a high productivity application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker) - - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution - - [**Algo transfers**](capabilities/transfer.md) - Ability to easily initiate algo transfers between accounts, including dispenser management and idempotent account funding - - [**Debugger**](capabilities/debugger.md) - Provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AVM Debugger extension](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). +- Safe Algo amount manipulation +- Explicit microAlgo/Algo conversion +- Arithmetic operations +- Comparison operations + +## Higher-order Use Cases + +### Application Management + +- Smart contract deployment +- ARC-32/56 application clients +- State management +- Box storage +- Application calls + +### Asset Management + +- ASA creation and configuration +- Asset transfers +- Opt-in/out management +- Asset destruction + +### Testing and Debugging + +- Transaction simulation +- AVM Debugger support +- Trace management + +### Utility Functions + +- Algo transfers +- Account funding +- TestNet dispenser integration +- Indexer pagination (reference-documentation)= # Reference documentation -We have [auto-generated reference documentation for the code](apidocs/algokit_utils/algokit_utils.md). +For detailed API documentation, see the [auto-generated reference documentation](apidocs/algokit_utils/algokit_utils.md). -# Roadmap +# Contributing -This library will naturally evolve with any logical developer experience improvements needed to facilitate the [AlgoKit](https://github.com/algorandfoundation/algokit-cli) roadmap as it evolves. +This is an open source project managed by the Algorand Foundation. See the [AlgoKit contributing page](https://github.com/algorandfoundation/algokit-cli/blob/main/CONTRIBUTING.MD) to learn about making improvements. -Likely future capability additions include: +To successfully run the tests in this repository you need to be running LocalNet via [AlgoKit](https://github.com/algorandfoundation/algokit-cli): -- Typed application client -- Asset management -- Expanded indexer API wrapper support +```bash +algokit localnet start +``` # Indices and tables diff --git a/docs/source/migration-guide.md b/docs/source/migration-guide.md new file mode 100644 index 00000000..a766ff33 --- /dev/null +++ b/docs/source/migration-guide.md @@ -0,0 +1,252 @@ +# v3 Migration Guide + +Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: + +- Easier and simpler consumption experience guided by IDE autocompletion +- Less redundant parameter passing (e.g., `algod` client) +- Better performance through caching of commonly retrieved values like transaction parameters +- More consistent and intuitive API design +- Stronger type safety and better error messages +- Improved ARC-56 compatibility +- Feature parity with `algokit-utils-ts` >= `v7` interfaces + +The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. + +The old version (in `algokit_utils._legacy_v2`) will still work until at least v4 (we have maintained backwards compatibility), but it exposes an older, function-based interface that is deprecated. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. + +## Migration Steps + +### Prerequisites + +If you have previously relied on `beta` versions of ` + +### Step 1 - Replace SDK Clients with AlgorandClient + +First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: + +```python +"""Before""" +import algokit_utils +algod = algokit_utils.get_algod_client() +indexer = algokit_utils.get_indexer_client() + +"""After""" +from algokit_utils import AlgorandClient +algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. +``` + +During migration, you can still access SDK clients if needed: + +```python +algod = algorand.client.algod +indexer = algorand.client.indexer +kmd = algorand.client.kmd +``` + +### Step 2 - Update Account Management + +Account management has moved to `algorand.account`: + +```python +"""Before""" +account = algokit_utils.get_account_from_mnemonic( + mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), +) +dispenser = algokit_utils.get_dispenser_account(algod) + +"""After""" +account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) +dispenser = algorand.account.dispenser_from_environment() +``` + +Key changes: + +- `get_account` → `account.from_environment` +- `get_account_from_mnemonic` → `account.from_mnemonic` +- `get_dispenser_account` → `account.dispenser_from_environment` +- `get_localnet_default_account` → `account.localnet_dispenser` + +### Step 3 - Update Transaction Management + +Transaction creation and sending is now more structured: + +```python +"""Before""" +result = algokit_utils.transfer_algos( + from_account=account, + to_addr="RECEIVER", + amount=algokit_utils.algos(1), + algod_client=algod, +) + +"""After""" +result = algorand.send.payment( + sender=account.address, + receiver="RECEIVER", + amount=(1).algo(), +) + +# For transaction groups +"""Before""" +atc = AtomicTransactionComposer() +# ... add transactions ... +result = algokit_utils.execute_atc_with_logic_error(atc, algod) + +"""After""" +composer = algorand.new_group() +# ... add transactions ... +result = composer.send() +``` + +Key changes: + +- `transfer_algos` → `send.payment` +- `transfer_asset` → `send.asset_transfer` +- `execute_atc_with_logic_error` → `composer.send()` +- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) +- Amount handling uses extension methods (e.g., `(1).algo()` instead of `algos(1)`) + +### Step 4 - Update Application Client Usage + +The application client has been split into `AppClient` and `AppFactory`: + +```python +"""Before""" +app_client = ApplicationClient( + algod_client=algod, + app_spec=app_spec, + app_id=existing_app_id, +) + +"""After""" +# For existing apps +app_client = AppClient( + app_id=existing_app_id, + app_spec=app_spec, + algorand=algorand, +) + +# For creating/deploying apps +app_factory = algorand.get_app_factory( + app_spec=app_spec, + app_name="MyApp", +) +``` + +Key changes in method calls: + +```python +"""Before""" +result = app_client.call( + method="hello", + method_args=["World"], + boxes=[("name", "box1")], +) + +"""After""" +result = app_client.send.call( + app_client.params.call( + method="hello", + args=["World"], + box_references=[("name", "box1")], + ) +) +``` + +Notable changes: + +- Split between `AppClient` (for existing apps) and `AppFactory` (for creation/deployment) +- More structured transaction building with `.params`, `.create_transaction`, and `.send` +- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) +- Better ARC-56 support for state management +- Improved error handling and debugging support + +### Step 5 - Update State Management + +State management is now more structured and type-safe: + +```python +"""Before""" +global_state = app_client.get_global_state() +local_state = app_client.get_local_state(account_address) +box_value = app_client.get_box_value("box_name") + +"""After""" +# Global state +global_state = app_client.state.global_state.get_all() +value = app_client.state.global_state.get_value("key_name") +map_value = app_client.state.global_state.get_map_value("map_name", "key") + +# Local state +local_state = app_client.state.local_state(account_address).get_all() +value = app_client.state.local_state(account_address).get_value("key_name") +map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") + +# Box storage +box_value = app_client.state.box.get_value("box_name") +boxes = app_client.state.box.get_all() +map_value = app_client.state.box.get_map_value("map_name", "key") +``` + +### Step 6 - Update Asset Management + +Asset management is now more consistent: + +```python +"""Before""" +result = algokit_utils.opt_in(algod, account, [asset_id]) + +"""After""" +result = algorand.send.asset_opt_in( + sender=account.address, + asset_id=asset_id, +) +``` + +## Breaking Changes + +1. **Client Management** + + - Removal of standalone client creation functions + - All clients now accessed through `AlgorandClient` + +2. **Account Management** + + - Account creation functions moved to `AccountManager` + - Changed parameter names for consistency + - Improved typing for account operations + +3. **Transaction Management** + + - Restructured transaction creation and sending + - Removed `skip_sending` parameter (use `create_transaction` instead) + - Changed parameter names for consistency + - New transaction composition interface + +4. **Application Client** + + - Split into `AppClient` and `AppFactory` + - New structured interface for transactions + - Changed parameter names for consistency + - Improved ARC-56 support + +5. **State Management** + + - New hierarchical state access + - Improved typing for state values + - Better support for ARC-56 state schemas + +6. **Asset Management** + - Moved to consistent transaction interface + - Changed parameter names for consistency + +## Best Practices + +1. Use the new `AlgorandClient` as the main entry point +2. Leverage IDE autocompletion to discover available functionality +3. Use the new parameter builders for type-safe transaction creation +4. Use the state accessor patterns for cleaner state management +5. Use transaction composition for atomic operations +6. Use source maps and debug mode for development +7. Use idempotent deployment patterns with versioning +8. Properly manage box references to avoid transaction failures diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index 28de779f..5ef9d203 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -6,6 +6,7 @@ from algosdk.account import address_from_private_key from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams +from typing_extensions import deprecated from algokit_utils.models.account import Account @@ -80,6 +81,7 @@ def _check_fee(transaction: PaymentTxn | AssetTransferTxn, max_fee: int | None) ) +@deprecated("Use the `TransactionComposer` abstraction instead to construct appropriate transfer transactions") def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTxn: """Transfer µALGOs between accounts""" @@ -100,6 +102,7 @@ def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTx return result +@deprecated("Use the `TransactionComposer` abstraction instead to construct appropriate transfer transactions") def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) -> AssetTransferTxn: """Transfer assets between accounts""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index 4d1341b9..f3de3a11 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -48,7 +48,7 @@ def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> A return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) -@deprecated("Use AlgorandClient.client.test_net() or AlgorandClient.main_net() instead") +@deprecated("Use AlgorandClient.client.testnet() or AlgorandClient.mainnet() instead") def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str ) -> AlgoClientConfig: @@ -69,7 +69,7 @@ def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: return AlgodClient(config.token, config.server, headers) -@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") +@deprecated("Use AlgorandClient.client.default_localnet().kmd instead") def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment @@ -88,28 +88,28 @@ def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: return IndexerClient(config.token, config.server, headers) -@deprecated("Use AlgorandClient.client.is_local_net() instead") +@deprecated("Use AlgorandClient.client.is_localnet() instead") def is_localnet(client: AlgodClient) -> bool: """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" params = client.suggested_params() return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] -@deprecated("Use AlgorandClient.client.is_main_net() instead") +@deprecated("Use AlgorandClient.client.is_mainnet() instead") def is_mainnet(client: AlgodClient) -> bool: """Returns True if client genesis is `mainnet-v1`""" params = client.suggested_params() return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] -@deprecated("Use AlgorandClient.client.is_test_net() instead") +@deprecated("Use AlgorandClient.client.is_testnet() instead") def is_testnet(client: AlgodClient) -> bool: """Returns True if client genesis is `testnet-v1`""" params = client.suggested_params() return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] -@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") +@deprecated("Use AlgorandClient.client.default_localnet().kmd instead") def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index baaddfcf..37fc3ef1 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -34,22 +34,67 @@ @dataclass(frozen=True, kw_only=True) class _CommonEnsureFundedParams: + """ + Common parameters for ensure funded responses. + """ + transaction_id: str amount_funded: AlgoAmount @dataclass(frozen=True, kw_only=True) class EnsureFundedResponse(SendSingleTransactionResult, _CommonEnsureFundedParams): - pass + """ + Response from performing an ensure funded call. + """ @dataclass(frozen=True, kw_only=True) class EnsureFundedFromTestnetDispenserApiResponse(_CommonEnsureFundedParams): - pass + """ + Response from performing an ensure funded call using TestNet dispenser API. + """ @dataclass(frozen=True, kw_only=True) class AccountInformation: + """ + Information about an Algorand account's current status, balance and other properties. + + See `https://developer.algorand.org/docs/rest-apis/algod/#account` for detailed field descriptions. + + :param str address: The account's address + :param int amount: The account's current balance in microAlgos + :param int amount_without_pending_rewards: The account's balance in microAlgos without the pending rewards + :param int min_balance: The account's minimum required balance in microAlgos + :param int pending_rewards: The amount of pending rewards in microAlgos + :param int rewards: The amount of rewards earned in microAlgos + :param int round: The round for which this information is relevant + :param str status: The account's status (e.g., 'Offline', 'Online') + :param int|None total_apps_opted_in: Number of applications this account has opted into + :param int|None total_assets_opted_in: Number of assets this account has opted into + :param int|None total_box_bytes: Total number of box bytes used by this account + :param int|None total_boxes: Total number of boxes used by this account + :param int|None total_created_apps: Number of applications created by this account + :param int|None total_created_assets: Number of assets created by this account + :param list[dict]|None apps_local_state: Local state of applications this account has opted into + :param int|None apps_total_extra_pages: Number of extra pages allocated to applications + :param dict|None apps_total_schema: Total schema for all applications + :param list[dict]|None assets: Assets held by this account + :param str|None auth_addr: If rekeyed, the authorized address + :param int|None closed_at_round: Round when this account was closed + :param list[dict]|None created_apps: Applications created by this account + :param list[dict]|None created_assets: Assets created by this account + :param int|None created_at_round: Round when this account was created + :param bool|None deleted: Whether this account is deleted + :param bool|None incentive_eligible: Whether this account is eligible for incentives + :param int|None last_heartbeat: Last heartbeat round for this account + :param int|None last_proposed: Last round this account proposed a block + :param dict|None participation: Participation information for this account + :param int|None reward_base: Base reward for this account + :param str|None sig_type: Signature type for this account + """ + address: str amount: int amount_without_pending_rewards: int @@ -83,13 +128,21 @@ class AccountInformation: class AccountManager: - """Creates and keeps track of addresses and signers""" + """ + Creates and keeps track of signing accounts that can sign transactions for a sending address. + + This class provides functionality to create, track, and manage various types of accounts including + mnemonic-based, rekeyed, multisig, and logic signature accounts. + """ def __init__(self, client_manager: ClientManager): """ Create a new account manager. - :param client_manager: The ClientManager client to use for algod and kmd clients + :param ClientManager client_manager: The ClientManager client to use for algod and kmd clients + + :example: + >>> account_manager = AccountManager(client_manager) """ self._client_manager = client_manager self._kmd_account_manager = KmdAccountManager(client_manager) @@ -100,24 +153,92 @@ def set_default_signer(self, signer: TransactionSigner) -> Self: """ Sets the default signer to use if no other signer is specified. - :param signer: The signer to use - :return: The `AccountManager` so method calls can be chained + If this isn't set and a transaction needs signing for a given sender + then an error will be thrown from `get_signer` / `get_account`. + + :param TransactionSigner signer: A `TransactionSigner` signer to use. + :returns: The `AccountManager` so method calls can be chained + + :example: + >>> signer_account = account_manager.random() + >>> account_manager.set_default_signer(signer_account.signer) + >>> # When signing a transaction, if there is no signer registered for the sender + >>> # then the default signer will be used + >>> signer = account_manager.get_signer("{SENDERADDRESS}") """ self._default_signer = signer return self def set_signer(self, sender: str, signer: TransactionSigner) -> Self: """ - Tracks the given account for later signing. + Tracks the given `TransactionSigner` against the given sender address for later signing. + + :param str sender: The sender address to use this signer for + :param TransactionSigner signer: The `TransactionSigner` to sign transactions with for the given sender + :returns: The `AccountManager` instance for method chaining - :param sender: The sender address to use this signer for - :param signer: The signer to sign transactions with for the given sender - :return: The AccountCreator instance for method chaining + :example: + >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) """ self._signers[sender] = signer return self + def set_signer_from_account(self, account: Account | LogicSigAccount | MultiSigAccount) -> Self: + """ + Tracks the given account for later signing. + + Note: If you are generating accounts via the various methods on `AccountManager` + (like `random`, `from_mnemonic`, `logic_sig`, etc.) then they automatically get tracked. + + :param Account|LogicSigAccount|MultiSigAccount account: The account to register + :returns: The `AccountManager` instance for method chaining + + :example: + >>> account_manager = AccountManager(client_manager) + >>> account_manager.set_signer_from_account(Account.new_account()) + >>> account_manager.set_signer_from_account(LogicSigAccount(program, args)) + >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) + """ + if isinstance(account, LogicSigAccount): + addr = account.address() + self._signers[addr] = LogicSigTransactionSigner(account) + else: + addr = account.address + self._signers[addr] = account.signer + return self + + def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSigner: + """ + Returns the `TransactionSigner` for the given sender address. + + If no signer has been registered for that address then the default signer is used if registered. + + :param str|Account|LogicSigAccount sender: The sender address or account + :returns: The `TransactionSigner` + :raises ValueError: If no signer is found and no default signer is set + + :example: + >>> signer = account_manager.get_signer("SENDERADDRESS") + """ + signer = self._signers.get(self._get_address(sender)) or self._default_signer + if not signer: + raise ValueError(f"No signer found for address {sender}") + return signer + def get_account(self, sender: str) -> Account: + """ + Returns the `Account` for the given sender address. + + :param str sender: The sender address + :returns: The `Account` + :raises ValueError: If no account is found or if the account is not a regular account + + :example: + >>> sender = account_manager.random() + >>> # ... + >>> # Returns the `Account` for `sender` that has previously been registered + >>> account = account_manager.get_account(sender) + """ account = self._signers.get(sender) if not account: raise ValueError(f"No account found for address {sender}") @@ -126,6 +247,13 @@ def get_account(self, sender: str) -> Account: return account def get_logic_sig_account(self, sender: str) -> LogicSigAccount: + """ + Returns the `LogicSigAccount` for the given sender address. + + :param str sender: The sender address + :returns: The `LogicSigAccount` + :raises ValueError: If no account is found or if the account is not a logic signature account + """ account = self._signers.get(sender) if not account: raise ValueError(f"No account found for address {sender}") @@ -133,26 +261,19 @@ def get_logic_sig_account(self, sender: str) -> LogicSigAccount: raise ValueError(f"Account {sender} is not a logic sig account") return account - def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSigner: - """ - Returns the `TransactionSigner` for the given sender address. - - If no signer has been registered for that address then the default signer is used if registered. - - :param sender: The sender address - :return: The `TransactionSigner` or throws an error if not found - """ - signer = self._signers.get(self._get_address(sender)) or self._default_signer - if not signer: - raise ValueError(f"No signer found for address {sender}") - return signer - def get_information(self, sender: str | Account) -> AccountInformation: """ Returns the given sender account's current status, balance and spendable amounts. - :param sender: The address of the sender/account to look up - :return: The account information + See ``_ + for response data schema details. + + :param str|Account sender: The address of the sender/account to look up + :returns: The account information + + :example: + >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" + >>> account_info = account_manager.get_information(address) """ info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) @@ -160,19 +281,24 @@ def get_information(self, sender: str | Account) -> AccountInformation: return AccountInformation(**info) def _register_account(self, private_key: str) -> Account: - """Helper method to create and register an account with its signer. - - Args: - private_key: The private key for the account + """ + Helper method to create and register an account with its signer. - Returns: - The registered Account instance + :param str private_key: The private key for the account + :returns: The registered Account instance """ account = Account(private_key=private_key) self._signers[account.address] = account.signer return account def _register_logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + """ + Helper method to create and register a logic signature account. + + :param bytes program: The bytes that make up the compiled logic signature + :param list[bytes]|None args: The (binary) arguments to pass into the logic signature + :returns: The registered LogicSigAccount instance + """ logic_sig = LogicSigAccount(program, args) self._signers[logic_sig.address()] = LogicSigTransactionSigner(logic_sig) return logic_sig @@ -180,6 +306,15 @@ def _register_logic_sig(self, program: bytes, args: list[bytes] | None = None) - def _register_multi_sig( self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] ) -> MultiSigAccount: + """ + Helper method to create and register a multisig account. + + :param int version: The version of the multisig account + :param int threshold: The threshold number of signatures required + :param list[str] addrs: The list of addresses that can sign + :param list[Account] signing_accounts: The list of accounts that are present to sign + :returns: The registered MultisigAccount instance + """ msig_account = MultiSigAccount( MultisigMetadata(version=version, threshold=threshold, addresses=addrs), signing_accounts, @@ -188,17 +323,56 @@ def _register_multi_sig( return msig_account def from_mnemonic(self, mnemonic: str) -> Account: + """ + Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. + + :param str mnemonic: The mnemonic secret representing the private key of an account + :returns: The account + + .. warning:: + Be careful how the mnemonic is handled. Never commit it into source control and ideally load it + from the environment (ideally via a secret storage service) rather than the file system. + + :example: + >>> account = account_manager.from_mnemonic("mnemonic secret ...") + """ private_key = to_private_key(mnemonic) return self._register_account(private_key) def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Account: + """ + Tracks and returns an Algorand account with private key loaded by convention from environment variables. + + This allows you to write code that will work seamlessly in production and local development (LocalNet) + without manual config locally (including when you reset the LocalNet). + + :param str name: The name identifier of the account + :param AlgoAmount|None fund_with: Optional amount to fund the account with when it gets created + (when targeting LocalNet) + :returns: The account + :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} + + .. note:: + Convention: + * **Non-LocalNet:** will load `{NAME}_MNEMONIC` as a mnemonic secret. + If `{NAME}_SENDER` is defined then it will use that for the sender address + (i.e. to support rekeyed accounts) + * **LocalNet:** will load the account from a KMD wallet called {NAME} and if that wallet doesn't exist + it will create it and fund the account for you + + :example: + >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: + >>> account = account_manager.from_environment('MY_ACCOUNT') + >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created + >>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser + """ account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") if account_mnemonic: private_key = mnemonic.to_private_key(account_mnemonic) return self._register_account(private_key) - if self._client_manager.is_local_net(): + if self._client_manager.is_localnet(): kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with) return self._register_account(kmd_account.private_key) @@ -207,6 +381,21 @@ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Ac def from_kmd( self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None ) -> Account: + """ + Tracks and returns an Algorand account with private key loaded from the given KMD wallet. + + :param str name: The name of the wallet to retrieve an account from + :param Callable[[dict[str, Any]], bool]|None predicate: Optional filter to use to find the account + :param str|None sender: Optional sender address to use this signer for (aka a rekeyed account) + :returns: The account + :raises ValueError: If unable to find KMD account with given name and predicate + + :example: + >>> # Get default funded account in a LocalNet: + >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', + ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 + ... ) + """ kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender) if not kmd_account: raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}") @@ -214,33 +403,94 @@ def from_kmd( return self._register_account(kmd_account.private_key) def logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + """ + Tracks and returns an account that represents a logic signature. + + :param bytes program: The bytes that make up the compiled logic signature + :param list[bytes]|None args: Optional (binary) arguments to pass into the logic signature + :returns: A logic signature account wrapper + + :example: + >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) + """ return self._register_logic_sig(program, args) def multi_sig( self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] ) -> MultiSigAccount: + """ + Tracks and returns an account that supports partial or full multisig signing. + + :param int version: The version of the multisig account + :param int threshold: The threshold number of signatures required + :param list[str] addrs: The list of addresses that can sign + :param list[Account] signing_accounts: The signers that are currently present + :returns: A multisig account wrapper + + :example: + >>> account = account_manager.multi_sig( + ... version=1, + ... threshold=1, + ... addrs=["ADDRESS1...", "ADDRESS2..."], + ... signing_accounts=[account1, account2] + ... ) + """ return self._register_multi_sig(version, threshold, addrs, signing_accounts) def random(self) -> Account: """ Tracks and returns a new, random Algorand account. - :return: The account + :returns: The account + + :example: + >>> account = account_manager.random() """ account = Account.new_account() return self._register_account(account.private_key) def localnet_dispenser(self) -> Account: + """ + Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + + This account can be used to fund other accounts. + + :returns: The account + + :example: + >>> account = account_manager.localnet_dispenser() + """ kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() return self._register_account(kmd_account.private_key) def dispenser_from_environment(self) -> Account: + """ + Returns an account (with private key loaded) that can act as a dispenser from environment variables. + + If environment variables are not present, returns the default LocalNet dispenser account. + + :returns: The account + + :example: + >>> account = account_manager.dispenser_from_environment() + """ name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") if name: return self.from_environment(DISPENSER_ACCOUNT_NAME) return self.localnet_dispenser() def rekeyed(self, sender: Account | str, account: Account) -> Account: + """ + Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. + + :param Account|str sender: The account or address to use as the sender + :param Account account: The account to use as the signer for this new rekeyed account + :returns: The rekeyed account + + :example: + >>> account = account.from_mnemonic("mnemonic secret ...") + >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") + """ sender_address = sender.address if isinstance(sender, Account) else sender self._signers[sender_address] = account.signer return Account(address=sender_address, private_key=account.private_key) @@ -249,8 +499,7 @@ def rekey_account( # noqa: PLR0913 self, account: str | Account, rekey_to: str | Account, - *, - # Common transaction parameters + *, # Common transaction parameters signer: TransactionSigner | None = None, note: bytes | None = None, lease: bytes | None = None, @@ -262,24 +511,45 @@ def rekey_account( # noqa: PLR0913 last_valid_round: int | None = None, suppress_log: bool | None = None, ) -> SendAtomicTransactionComposerResults: - """Rekey an account to a new address. - - Args: - account: The account to rekey - rekey_to: The address or account to rekey to - signer: Optional transaction signer - note: Optional transaction note - lease: Optional transaction lease - static_fee: Optional static fee - extra_fee: Optional extra fee - max_fee: Optional max fee - validity_window: Optional validity window - first_valid_round: Optional first valid round - last_valid_round: Optional last valid round - suppress_log: Optional flag to suppress logging - - Returns: - The transaction result + """ + Rekey an account to a new address. + + :param str|Account account: The account to rekey + :param str|Account rekey_to: The address or account to rekey to + :param TransactionSigner|None signer: Optional transaction signer + :param bytes|None note: Optional transaction note + :param bytes|None lease: Optional transaction lease + :param AlgoAmount|None static_fee: Optional static fee + :param AlgoAmount|None extra_fee: Optional extra fee + :param AlgoAmount|None max_fee: Optional max fee + :param int|None validity_window: Optional validity window + :param int|None first_valid_round: Optional first valid round + :param int|None last_valid_round: Optional last valid round + :param bool|None suppress_log: Optional flag to suppress logging + :returns: The result of the transaction and the transaction that was sent + + .. warning:: + Please be careful with this function and be sure to read the + `official rekey guidance `_. + + :example: + >>> # Basic example (with string addresses): + >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) + >>> # Basic example (with signer accounts): + >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) + >>> # Advanced example: + >>> algorand.account.rekey_account({ + ... account: "ACCOUNTADDRESS", + ... rekey_to: "NEWADDRESS", + ... lease: 'lease', + ... note: 'note', + ... first_valid_round: 1000, + ... validity_window: 10, + ... extra_fee: AlgoAmount.from_micro_algo(1000), + ... static_fee: AlgoAmount.from_micro_algo(1000), + ... max_fee: AlgoAmount.from_micro_algo(3000), + ... suppress_log: True, + ... }) """ sender_address = self._get_address(account) rekey_address = self._get_address(rekey_to) @@ -338,6 +608,48 @@ def ensure_funded( # noqa: PLR0913 first_valid_round: int | None = None, last_valid_round: int | None = None, ) -> EnsureFundedResponse | None: + """ + Funds a given account using a dispenser account as a funding source. + + Ensures the given account has a certain amount of Algo free to spend (accounting for + Algo locked in minimum balance requirement). + + See ``_ for details. + + :param str|Account account_to_fund: The account to fund + :param str|Account dispenser_account: The account to use as a dispenser funding source + :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account + should have available to spend + :param AlgoAmount|None min_funding_increment: Optional minimum funding increment + :param int|None max_rounds_to_wait: Optional maximum rounds to wait for transaction + :param bool|None suppress_log: Optional flag to suppress logging + :param bool|None populate_app_call_resources: Optional flag to populate app call resources + :param TransactionSigner|None signer: Optional transaction signer + :param str|None rekey_to: Optional rekey address + :param bytes|None note: Optional transaction note + :param bytes|None lease: Optional transaction lease + :param AlgoAmount|None static_fee: Optional static fee + :param AlgoAmount|None extra_fee: Optional extra fee + :param AlgoAmount|None max_fee: Optional maximum fee + :param int|None validity_window: Optional validity window + :param int|None first_valid_round: Optional first valid round + :param int|None last_valid_round: Optional last valid round + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, + or None if no funds were needed + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded( + ... "ACCOUNTADDRESS", + ... "DISPENSERADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) + """ account_to_fund = self._get_address(account_to_fund) dispenser_account = self._get_address(dispenser_account) amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) @@ -405,28 +717,51 @@ def ensure_funded_from_environment( # noqa: PLR0913 first_valid_round: int | None = None, last_valid_round: int | None = None, ) -> EnsureFundedResponse | None: - """Ensure an account is funded from a dispenser account configured in environment. - - Args: - account_to_fund: Address of account to fund - min_spending_balance: Minimum spending balance to ensure - min_funding_increment: Optional minimum funding increment - max_rounds_to_wait: Optional maximum rounds to wait for transaction - suppress_log: Optional flag to suppress logging - populate_app_call_resources: Optional flag to populate app call resources - signer: Optional transaction signer - rekey_to: Optional rekey address - note: Optional transaction note - lease: Optional transaction lease - static_fee: Optional static fee - extra_fee: Optional extra fee - max_fee: Optional maximum fee - validity_window: Optional validity window - first_valid_round: Optional first valid round - last_valid_round: Optional last valid round - - Returns: - EnsureFundedResponse if funding was needed, None otherwise + """ + Ensure an account is funded from a dispenser account configured in environment. + + Uses a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, + as a funding source such that the given account has a certain amount of Algo free to spend + (accounting for Algo locked in minimum balance requirement). + + See ``_ for details. + + :param str|Account account_to_fund: The account to fund + :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account should have available to + spend + :param AlgoAmount|None min_funding_increment: Optional minimum funding increment + :param int|None max_rounds_to_wait: Optional maximum rounds to wait for transaction + :param bool|None suppress_log: Optional flag to suppress logging + :param bool|None populate_app_call_resources: Optional flag to populate app call resources + :param TransactionSigner|None signer: Optional transaction signer + :param str|None rekey_to: Optional rekey address + :param bytes|None note: Optional transaction note + :param bytes|None lease: Optional transaction lease + :param AlgoAmount|None static_fee: Optional static fee + :param AlgoAmount|None extra_fee: Optional extra fee + :param AlgoAmount|None max_fee: Optional maximum fee + :param int|None validity_window: Optional validity window + :param int|None first_valid_round: Optional first valid round + :param int|None last_valid_round: Optional last valid round + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or + None if no funds were needed + + .. note:: + The dispenser account is retrieved from the account mnemonic stored in + process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER + if it's a rekeyed account, or against default LocalNet if no environment variables present. + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_environment( + ... "ACCOUNTADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) """ account_to_fund = self._get_address(account_to_fund) dispenser_account = self.dispenser_from_environment() @@ -482,23 +817,41 @@ def ensure_funded_from_testnet_dispenser_api( *, # Force remaining params to be keyword-only min_funding_increment: AlgoAmount | None = None, ) -> EnsureFundedFromTestnetDispenserApiResponse | None: - """Ensure an account is funded using the TestNet Dispenser API. - - Args: - account_to_fund: Address of account to fund - dispenser_client: Instance of TestNetDispenserApiClient to use for funding - min_spending_balance: Minimum spending balance to ensure - min_funding_increment: Optional minimum funding increment - - Returns: - EnsureFundedResponse if funding was needed, None otherwise - - Raises: - ValueError: If attempting to fund on non-TestNet network + """ + Ensure an account is funded using the TestNet Dispenser API. + + Uses the TestNet Dispenser API as a funding source such that the account has a certain amount + of Algo free to spend (accounting for Algo locked in minimum balance requirement). + + See ``_ for details. + + :param str|Account account_to_fund: The account to fund + :param TestNetDispenserApiClient dispenser_client: The TestNet dispenser funding client + :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account should have + available to spend + :param AlgoAmount|None min_funding_increment: Optional minimum funding increment + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or + None if no funds were needed + :raises ValueError: If attempting to fund on non-TestNet network + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1) + ... ) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2) + ... ) """ account_to_fund = self._get_address(account_to_fund) - if not self._client_manager.is_test_net(): + if not self._client_manager.is_testnet(): raise ValueError("Attempt to fund using TestNet dispenser API on non TestNet network.") amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index 611f59d0..701ac0a1 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -54,7 +54,7 @@ def kmd(self) -> KMDClient: Exception: If KMD is not configured """ if self._kmd is None: - if self._client_manager.is_local_net(): + if self._client_manager.is_localnet(): kmd_config = ClientManager.get_config_from_environment_or_localnet() self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config) return self._kmd @@ -179,7 +179,7 @@ def get_localnet_dispenser_account(self) -> KmdAccount: dispenser = kmd_manager.get_localnet_dispenser_account() ``` """ - if not self._client_manager.is_local_net(): + if not self._client_manager.is_localnet(): raise Exception("Can't get LocalNet dispenser account from non LocalNet network") dispenser = self.get_wallet_account( diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 85ef23e9..e6a7eba7 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -909,6 +909,10 @@ def __init__(self, params: AppClientParams) -> None: self._send_accessor = _AppClientSendAccessor(self) self._create_transaction_accessor = _AppClientMethodCallTransactionCreator(self) + @property + def algorand(self) -> AlgorandClientProtocol: + return self._algorand + @property def app_id(self) -> int: return self._app_id @@ -973,11 +977,11 @@ def from_network( app_spec = AppClient.normalise_app_spec(app_spec) network_names = [network.genesis_hash] - if network.is_local_net: + if network.is_localnet: network_names.append("localnet") - if network.is_main_net: + if network.is_mainnet: network_names.append("mainnet") - if network.is_test_net: + if network.is_testnet: network_names.append("testnet") available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else [] diff --git a/src/algokit_utils/beta/__init__.py b/src/algokit_utils/beta/__init__.py new file mode 100644 index 00000000..7e7b6ceb --- /dev/null +++ b/src/algokit_utils/beta/__init__.py @@ -0,0 +1,72 @@ +from typing import Any, NoReturn + + +def _deprecated_import_error(old_path: str, new_path: str) -> NoReturn: + """Helper to create consistent deprecation error messages""" + raise ImportError( + f"The module '{old_path}' has been moved in v3. " + f"Please update your imports to use '{new_path}' instead. " + "See the migration guide for more details: " + "https://github.com/algorandfoundation/algokit-utils-py/blob/prerelease/ts-feature-parity/docs/migration-guide.md" + ) + + +class AlgorandClient: + """@deprecated Use algokit_utils.clients.AlgorandClient instead""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + _deprecated_import_error("algokit_utils.beta.AlgorandClient", "algokit_utils.AlgorandClient") + + +class AlgokitComposer: + """@deprecated Use algokit_utils.transactions.TransactionComposer instead""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + _deprecated_import_error("algokit_utils.beta.AlgokitComposer", "algokit_utils.TransactionComposer") + + +class AccountManager: + """@deprecated Use algokit_utils.accounts.AccountManager instead""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + _deprecated_import_error("algokit_utils.beta.AccountManager", "algokit_utils.AccountManager") + + +class ClientManager: + """@deprecated Use algokit_utils.clients.ClientManager instead""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + _deprecated_import_error("algokit_utils.beta.ClientManager", "algokit_utils.ClientManager") + + +# Re-export all the parameter classes with deprecation warnings +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" + + param_mappings = { + # Transaction params + "PayParams": "algokit_utils.transactions.PaymentParams", + "AssetCreateParams": "algokit_utils.transactions.AssetCreateParams", + "AssetConfigParams": "algokit_utils.transactions.AssetConfigParams", + "AssetFreezeParams": "algokit_utils.transactions.AssetFreezeParams", + "AssetDestroyParams": "algokit_utils.transactions.AssetDestroyParams", + "AssetTransferParams": "algokit_utils.transactions.AssetTransferParams", + "AssetOptInParams": "algokit_utils.transactions.AssetOptInParams", + "AppCallParams": "algokit_utils.transactions.AppCallParams", + "MethodCallParams": "algokit_utils.transactions.MethodCallParams", + "OnlineKeyRegParams": "algokit_utils.transactions.OnlineKeyRegistrationParams", + } + + if name in param_mappings: + _deprecated_import_error(f"algokit_utils.beta.{name}", param_mappings[name]) + + raise AttributeError(f"module 'algokit_utils.beta' has no attribute '{name}'") + + +# Clean up namespace to only show intended exports +__all__ = [ + "AccountManager", + "AlgokitComposer", + "AlgorandClient", + "ClientManager", +] diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index 24ca220e..19f0d6fc 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -3,14 +3,17 @@ import typing_extensions from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.kmd import KMDClient from algosdk.transaction import SuggestedParams +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient from algokit_utils.accounts.account_manager import AccountManager from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.models.network import AlgoClientConfigs +from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs from algokit_utils.transactions.transaction_composer import ( TransactionComposer, ) @@ -160,7 +163,7 @@ def create_transaction(self) -> AlgorandClientTransactionCreator: return self._transaction_creator @staticmethod - def default_local_net() -> "AlgorandClient": + def default_localnet() -> "AlgorandClient": """ Returns an `AlgorandClient` pointing at default LocalNet ports and API token. @@ -168,14 +171,14 @@ def default_local_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=ClientManager.get_default_local_net_config("algod"), - indexer_config=ClientManager.get_default_local_net_config("indexer"), - kmd_config=ClientManager.get_default_local_net_config("kmd"), + algod_config=ClientManager.get_default_localnet_config("algod"), + indexer_config=ClientManager.get_default_localnet_config("indexer"), + kmd_config=ClientManager.get_default_localnet_config("kmd"), ) ) @staticmethod - def test_net() -> "AlgorandClient": + def testnet() -> "AlgorandClient": """ Returns an `AlgorandClient` pointing at TestNet using AlgoNode. @@ -190,7 +193,7 @@ def test_net() -> "AlgorandClient": ) @staticmethod - def main_net() -> "AlgorandClient": + def mainnet() -> "AlgorandClient": """ Returns an `AlgorandClient` pointing at MainNet using AlgoNode. @@ -205,14 +208,18 @@ def main_net() -> "AlgorandClient": ) @staticmethod - def from_clients(clients: AlgoSdkClients) -> "AlgorandClient": + def from_clients( + algod: AlgodClient, indexer: IndexerClient | None = None, kmd: KMDClient | None = None + ) -> "AlgorandClient": """ Returns an `AlgorandClient` pointing to the given client(s). - :param clients: The clients to use + :param algod: The algod client to use + :param indexer: The indexer client to use + :param kmd: The kmd client to use :return: The `AlgorandClient` """ - return AlgorandClient(clients) + return AlgorandClient(AlgoSdkClients(algod=algod, indexer=indexer, kmd=kmd)) @staticmethod def from_environment() -> "AlgorandClient": @@ -228,11 +235,19 @@ def from_environment() -> "AlgorandClient": return AlgorandClient(ClientManager.get_config_from_environment_or_localnet()) @staticmethod - def from_config(config: AlgoClientConfigs) -> "AlgorandClient": + def from_config( + algod_config: AlgoClientConfig, + indexer_config: AlgoClientConfig | None = None, + kmd_config: AlgoClientConfig | None = None, + ) -> "AlgorandClient": """ Returns an `AlgorandClient` from the given config. - :param config: The config to use + :param algod_config: The config to use for the algod client + :param indexer_config: The config to use for the indexer client + :param kmd_config: The config to use for the kmd client :return: The `AlgorandClient` """ - return AlgorandClient(config) + return AlgorandClient( + AlgoClientConfigs(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) + ) diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 978cb0c8..74c2c576 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -42,9 +42,9 @@ def __init__( @dataclass(kw_only=True, frozen=True) class NetworkDetail: - is_test_net: bool - is_main_net: bool - is_local_net: bool + is_testnet: bool + is_mainnet: bool + is_localnet: bool genesis_id: str genesis_hash: str @@ -105,21 +105,21 @@ def kmd(self) -> KMDClient: def network(self) -> NetworkDetail: sp = self._algod.suggested_params() # TODO: cache it return NetworkDetail( - is_test_net=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], - is_main_net=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], - is_local_net=ClientManager.genesis_id_is_local_net(str(sp.gen)), + is_testnet=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], + is_mainnet=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], + is_localnet=ClientManager.genesis_id_is_localnet(str(sp.gen)), genesis_id=str(sp.gen), genesis_hash=sp.gh, ) - def is_local_net(self) -> bool: - return self.network().is_local_net + def is_localnet(self) -> bool: + return self.network().is_localnet - def is_test_net(self) -> bool: - return self.network().is_test_net + def is_testnet(self) -> bool: + return self.network().is_testnet - def is_main_net(self) -> bool: - return self.network().is_main_net + def is_mainnet(self) -> bool: + return self.network().is_mainnet def get_testnet_dispenser( self, auth_token: str | None = None, request_timeout: int | None = None @@ -270,7 +270,7 @@ def get_indexer_client_from_environment() -> IndexerClient: return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) @staticmethod - def genesis_id_is_local_net(genesis_id: str) -> bool: + def genesis_id_is_localnet(genesis_id: str) -> bool: return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] @staticmethod @@ -304,9 +304,9 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: ) else: # Use localnet defaults - algod_config = ClientManager.get_default_local_net_config("algod") - indexer_config = ClientManager.get_default_local_net_config("indexer") - kmd_config = ClientManager.get_default_local_net_config("kmd") + algod_config = ClientManager.get_default_localnet_config("algod") + indexer_config = ClientManager.get_default_localnet_config("indexer") + kmd_config = ClientManager.get_default_localnet_config("kmd") return AlgoClientConfigs( algod_config=algod_config, @@ -315,7 +315,7 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: ) @staticmethod - def get_default_local_net_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: + def get_default_localnet_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: port = ( config_or_port if isinstance(config_or_port, int) diff --git a/src/algokit_utils/models/__init__.py b/src/algokit_utils/models/__init__.py index 52d4ce8f..d4790dc4 100644 --- a/src/algokit_utils/models/__init__.py +++ b/src/algokit_utils/models/__init__.py @@ -2,6 +2,7 @@ from algokit_utils.models.account import * # noqa: F403 from algokit_utils.models.amount import * # noqa: F403 from algokit_utils.models.application import * # noqa: F403 +from algokit_utils.models.network import * # noqa: F403 from algokit_utils.models.simulate import * # noqa: F403 from algokit_utils.models.state import * # noqa: F403 from algokit_utils.models.transaction import * # noqa: F403 diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index 1d063060..f364a25a 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -9,7 +9,19 @@ class AlgoAmount: + """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers.""" + def __init__(self, amount: dict[str, int | Decimal]): + """Create a new AlgoAmount instance. + + :param amount: A dictionary containing either algos, algo, microAlgos, or microAlgo as key + and their corresponding value as an integer or Decimal. + :raises ValueError: If an invalid amount format is provided. + + :example: + >>> amount = AlgoAmount({"algos": 1}) + >>> amount = AlgoAmount({"microAlgos": 1_000_000}) + """ if "microAlgos" in amount: self.amount_in_micro_algo = int(amount["microAlgos"]) elif "microAlgo" in amount: @@ -23,42 +35,88 @@ def __init__(self, amount: dict[str, int | Decimal]): @property def micro_algos(self) -> int: + """Return the amount as a number in µAlgo. + + :returns: The amount in µAlgo. + """ return self.amount_in_micro_algo @property def micro_algo(self) -> int: + """Return the amount as a number in µAlgo. + + :returns: The amount in µAlgo. + """ return self.amount_in_micro_algo @property def algos(self) -> int | Decimal: + """Return the amount as a number in Algo. + + :returns: The amount in Algo. + """ return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @property def algo(self) -> int | Decimal: + """Return the amount as a number in Algo. + + :returns: The amount in Algo. + """ return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @staticmethod def from_algos(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of Algo. + + :param amount: The amount in Algo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_algos(1) + """ return AlgoAmount({"algos": amount}) @staticmethod def from_algo(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of Algo. + + :param amount: The amount in Algo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_algo(1) + """ return AlgoAmount({"algo": amount}) @staticmethod def from_micro_algos(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of µAlgo. + + :param amount: The amount in µAlgo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_micro_algos(1_000_000) + """ return AlgoAmount({"microAlgos": amount}) @staticmethod def from_micro_algo(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of µAlgo. + + :param amount: The amount in µAlgo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_micro_algo(1_000_000) + """ return AlgoAmount({"microAlgo": amount}) def __str__(self) -> str: - """Return a string representation of the amount.""" return f"{self.micro_algo:,} µALGO" def __int__(self) -> int: - """Return the amount as an integer number of microAlgos.""" return self.micro_algos def __add__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 7f7597cb..aee899dc 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -38,9 +38,13 @@ __all__ = [ + "AppCallMethodCallParams", "AppCallParams", + "AppCreateMethodCallParams", "AppCreateParams", + "AppDeleteMethodCallParams", "AppDeleteParams", + "AppUpdateMethodCallParams", "AppUpdateParams", "AssetConfigParams", "AssetCreateParams", @@ -49,6 +53,7 @@ "AssetOptInParams", "AssetOptOutParams", "AssetTransferParams", + "MethodCallParams", "OfflineKeyRegistrationParams", "OnlineKeyRegistrationParams", "PaymentParams", diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py index ad78ac43..716b7690 100644 --- a/tests/accounts/test_account_manager.py +++ b/tests/accounts/test_account_manager.py @@ -9,7 +9,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index 008b2558..457898a6 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -28,7 +28,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index d186e321..9a17bb0f 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -27,7 +27,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index 57313d31..83a8ed2e 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -9,7 +9,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index 2d5ea4e4..fbb80c37 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -17,7 +17,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py index 77c56b29..91a337f5 100644 --- a/tests/clients/algorand_client/test_transfer.py +++ b/tests/clients/algorand_client/test_transfer.py @@ -16,7 +16,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture @@ -352,7 +352,7 @@ def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClie def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: - algorand = AlgorandClient.test_net() + algorand = AlgorandClient.testnet() account_to_fund = algorand.account.random() monkeypatch.setenv( "ALGOKIT_DISPENSER_ACCESS_TOKEN", @@ -375,7 +375,7 @@ def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, http def test_ensure_funded_testnet_api_bad_response(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: - algorand = AlgorandClient.test_net() + algorand = AlgorandClient.testnet() account_to_fund = algorand.account.random() monkeypatch.setenv( "ALGOKIT_DISPENSER_ACCESS_TOKEN", diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index 8bf4b085..8b7c1a4a 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -31,7 +31,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index 58c4df24..fc52c4f0 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -21,7 +21,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 8452be08..f0ed1023 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -34,7 +34,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture(autouse=True) diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index 9bf2bbab..e034f2ce 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -35,7 +35,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 47000dde..7f44f641 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -32,7 +32,7 @@ @pytest.fixture def algorand() -> AlgorandClient: - return AlgorandClient.default_local_net() + return AlgorandClient.default_localnet() @pytest.fixture From 8024d375ca925f0806e82b595a260889f53ea6cc Mon Sep 17 00:00:00 2001 From: Al Date: Wed, 22 Jan 2025 12:54:15 +0100 Subject: [PATCH 11/31] refactor: refinements to support client generator v3 (#130) * chore: initial methods for typed client generator * fix: fixing arc2 note generation in AppDeployer * fix: changing CompiledTeal compiled to str from bytes; patching _build_source_map * refactor: adding usage of collections.abc.Sequence for args params * refactor: expose algorand client protocol; expand to cover latest interface * refactor: patch compile_teal_template behaviour around deployment_metadata * refactor: using generics for abi_return type in AppFactoryCreateMethodCallResult * refactor: making deploy arg types in app factory rely on generic (for redefinition in client generator) * refactor: further leveraging generics to match ts behaviour, the abi_return type turns into ABIValue * fix: proper use of type vars for client manager typed class getters * refactor: make all bare properties on app client send/createTxn/params optional * refactor: add error handler for simulate responses * fix: proper handling of default args from hints * fix: proper handling of getting abi args with default values (when no args given) * fix: typo in generics inheritance * refactor: ensure send calls from AppClient for methods parse output to arc56 values * fix: skip snake case conversion for struct field in arc56 * fix: group id generation for log during send atc * fix: exposing AppMethodCallTransactionArgument for typed client * fix: fixing params creation in factory * fix: safer global state decoding * refactor: parametrize indent in to_json methods of arc classes * fix: typo in get_map decoding * fix: box key decoding * refactor: contravariant typevars for typed factory protocol plus a fix for box decoding --- poetry.lock | 8 +- src/algokit_utils/__init__.py | 2 + src/algokit_utils/_debugging.py | 2 +- src/algokit_utils/applications/abi.py | 5 +- src/algokit_utils/applications/app_client.py | 346 ++++++++++++------ .../applications/app_deployer.py | 121 +++--- src/algokit_utils/applications/app_factory.py | 316 ++++++++++------ src/algokit_utils/applications/app_manager.py | 30 +- .../applications/app_spec/arc32.py | 4 +- .../applications/app_spec/arc56.py | 6 +- src/algokit_utils/clients/client_manager.py | 118 +++++- src/algokit_utils/models/application.py | 2 +- src/algokit_utils/protocols/client.py | 31 +- .../transactions/transaction_composer.py | 64 +++- .../transactions/transaction_sender.py | 47 +-- tests/applications/test_app_client.py | 35 +- tests/applications/test_app_factory.py | 28 +- tests/applications/test_arc56.py | 8 +- .../transactions/test_transaction_composer.py | 37 +- .../transactions/test_transaction_creator.py | 4 +- tests/transactions/test_transaction_sender.py | 6 +- 21 files changed, 839 insertions(+), 381 deletions(-) diff --git a/poetry.lock b/poetry.lock index cafb6951..4191a80e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -467,7 +467,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -478,7 +477,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -907,13 +905,13 @@ trio = ["async_generator", "trio"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 5aacda4a..9eeebbee 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -14,6 +14,7 @@ from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.errors.logic_error import LogicError from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.protocols import AlgorandClientProtocol # Common managers/clients that are frequently used entry points from algokit_utils.accounts.account_manager import AccountManager @@ -138,6 +139,7 @@ "AlgorandClient", "DELETABLE_TEMPLATE_NAME", "UPDATABLE_TEMPLATE_NAME", + "AlgorandClientProtocol", # Common managers/clients "AccountManager", "AppClient", diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index fa5dcd99..c59cc139 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -169,7 +169,7 @@ def _build_avm_sourcemap( source_map = compiled_teal.source_map.__dict__ teal_content = compiled_teal.teal elif isinstance(compiled_teal, CompiledTeal): - program_hash = base64.b64encode(checksum(compiled_teal.compiled)).decode() + program_hash = base64.b64encode(checksum(compiled_teal.compiled_base64_to_bytes)).decode() source_map = compiled_teal.source_map.__dict__ if compiled_teal.source_map else {} teal_content = compiled_teal.teal else: diff --git a/src/algokit_utils/applications/abi.py b/src/algokit_utils/applications/abi.py index 36f77c04..7ce82a20 100644 --- a/src/algokit_utils/applications/abi.py +++ b/src/algokit_utils/applications/abi.py @@ -15,6 +15,7 @@ ABIStruct: TypeAlias = dict[str, list[dict[str, "ABIValue"]]] Arc56ReturnValueType: TypeAlias = ABIValue | ABIStruct | None + ABIType: TypeAlias = algosdk.abi.ABIType ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType @@ -56,13 +57,13 @@ def is_success(self) -> bool: def get_arc56_value( self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] - ) -> ABIValue | ABIStruct | None: + ) -> Arc56ReturnValueType: return get_arc56_value(self, method, structs) def get_arc56_value( abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] -) -> ABIValue | ABIStruct | None: +) -> Arc56ReturnValueType: if isinstance(method, AlgorandABIMethod): type_str = method.returns.type struct = None # AlgorandABIMethod doesn't have struct info diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index e6a7eba7..de0904fb 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -4,15 +4,22 @@ import copy import json import os +from collections.abc import Sequence from dataclasses import dataclass, fields -from typing import TYPE_CHECKING, Any, Protocol, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar import algosdk from algosdk.source_map import SourceMap from algosdk.transaction import OnComplete, Transaction +from typing_extensions import Self from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps from algokit_utils.applications.abi import ( + ABIReturn, + ABIStruct, + ABIType, + ABIValue, + Arc56ReturnValueType, BoxABIValue, get_abi_decoded_value, get_abi_encoded_value, @@ -21,6 +28,7 @@ from algokit_utils.applications.app_spec.arc32 import Arc32Contract from algokit_utils.applications.app_spec.arc56 import ( Arc56Contract, + Method, PcOffsetMethod, ProgramSourceInfo, SourceInfo, @@ -39,6 +47,7 @@ from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, + AppCreateSchema, AppDeleteMethodCallParams, AppMethodCallTransactionArgument, AppUpdateMethodCallParams, @@ -57,7 +66,6 @@ from algosdk.atomic_transaction_composer import TransactionSigner - from algokit_utils.applications.abi import ABIStruct, ABIType, ABIValue from algokit_utils.applications.app_deployer import AppLookup from algokit_utils.applications.app_manager import AppManager from algokit_utils.models.amount import AlgoAmount @@ -66,6 +74,7 @@ __all__ = [ "AppClient", + "AppClientBareCallCreateParams", "AppClientBareCallParams", "AppClientBareCallWithCallOnCompleteParams", "AppClientBareCallWithCompilationAndSendParams", @@ -74,13 +83,18 @@ "AppClientCallParams", "AppClientCompilationParams", "AppClientCompilationResult", + "AppClientCreateSchema", + "AppClientMethodCallCreateParams", "AppClientMethodCallParams", "AppClientMethodCallWithCompilationAndSendParams", "AppClientMethodCallWithCompilationParams", "AppClientMethodCallWithSendParams", "AppClientParams", "AppSourceMaps", + "BaseAppClientMethodCallParams", + "BaseOnCompleteParams", "FundAppAccountParams", + "TypedAppClientProtocol", ] # TEAL opcodes for constant blocks @@ -152,6 +166,45 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 return max(bytecblock_offset or 0, intcblock_offset or 0) +class TypedAppClientProtocol(Protocol): + @classmethod + def from_creator_and_name( + cls, + *, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + algorand: AlgorandClientProtocol, + ) -> Self: ... + + @classmethod + def from_network( + cls, + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + algorand: AlgorandClientProtocol, + ) -> Self: ... + + def __init__( + self, + *, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + algorand: AlgorandClientProtocol, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> None: ... + + @dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: approval_program: bytes @@ -202,14 +255,19 @@ class AppClientCallParams: send_params: dict | None = None # Parameters to control transaction sending +ArgsT = TypeVar("ArgsT") +MethodT = TypeVar("MethodT") +OnCompleteT = TypeVar("OnCompleteT") + + @dataclass(kw_only=True, frozen=True) -class AppClientMethodCallParams: - method: str - args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None = None +class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT, OnCompleteT]): + method: MethodT + args: ArgsT | None = None account_references: list[str] | None = None app_references: list[int] | None = None asset_references: list[int] | None = None - box_references: list[BoxReference | BoxIdentifier] | None = None + box_references: Sequence[BoxReference | BoxIdentifier] | None = None extra_fee: AlgoAmount | None = None first_valid_round: int | None = None lease: bytes | None = None @@ -221,7 +279,16 @@ class AppClientMethodCallParams: static_fee: AlgoAmount | None = None validity_window: int | None = None last_valid_round: int | None = None - on_complete: algosdk.transaction.OnComplete | None = None + on_complete: OnCompleteT | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallParams( + BaseAppClientMethodCallParams[ + Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None], str, algosdk.transaction.OnComplete + ] +): + pass @dataclass(kw_only=True, frozen=True) @@ -261,9 +328,10 @@ class AppClientBareCallParams: box_references: list[BoxReference | BoxIdentifier] | None = None -@dataclass(kw_only=True, frozen=True) -class _CallOnComplete: - on_complete: algosdk.transaction.OnComplete +@dataclass(frozen=True) +class AppClientCreateSchema: + extra_program_pages: int | None = None + schema: AppCreateSchema | None = None @dataclass(kw_only=True, frozen=True) @@ -282,31 +350,30 @@ class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, App @dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, _CallOnComplete): +class BaseOnCompleteParams(Generic[OnCompleteT]): """Combined parameters for bare calls with an OnComplete value""" + on_complete: OnCompleteT | None = None -class _AppClientStateMethodsProtocol(Protocol): - def get_all(self) -> dict[str, Any]: ... - - def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: ... - - def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: ... # noqa: ANN401 - - def get_map(self, map_name: str) -> dict[str, ABIValue]: ... +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCallOnCompleteParams( + AppClientBareCallParams, BaseOnCompleteParams[algosdk.transaction.OnComplete] +): + """Combined parameters for bare calls with an OnComplete value""" -class _AppClientBoxMethodsProtocol(Protocol): - def get_all(self) -> dict[str, Any]: ... - def get_value(self, name: str) -> ABIValue | None: ... +@dataclass(frozen=True) +class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallWithCallOnCompleteParams): + pass - def get_map_value(self, map_name: str, key: bytes | Any) -> Any: ... # noqa: ANN401 - def get_map(self, map_name: str) -> dict[str, ABIValue]: ... +@dataclass(frozen=True) +class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams): + pass -class _AppClientStateMethods(_AppClientStateMethodsProtocol): +class _AppClientStateMethods: def __init__( self, *, @@ -333,7 +400,7 @@ def get_map(self, map_name: str) -> dict[str, ABIValue]: return self._get_map(map_name) -class _AppClientBoxMethods(_AppClientBoxMethodsProtocol): +class _AppClientBoxMethods: def __init__( self, *, @@ -367,7 +434,7 @@ def __init__(self, client: AppClient) -> None: self._app_id = client._app_id self._app_spec = client._app_spec - def local_state(self, address: str) -> _AppClientStateMethodsProtocol: + def local_state(self, address: str) -> _AppClientStateMethods: """Methods to access local state for the current app for a given address""" return self._get_state_methods( state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address), @@ -376,7 +443,7 @@ def local_state(self, address: str) -> _AppClientStateMethodsProtocol: ) @property - def global_state(self) -> _AppClientStateMethodsProtocol: + def global_state(self) -> _AppClientStateMethods: """Methods to access global state for the current app""" return self._get_state_methods( state_getter=lambda: self._algorand.app.get_global_state(self._app_id), @@ -385,11 +452,11 @@ def global_state(self) -> _AppClientStateMethodsProtocol: ) @property - def box(self) -> _AppClientBoxMethodsProtocol: + def box(self) -> _AppClientBoxMethods: """Methods to access box storage for the current app""" return self._get_box_methods() - def _get_box_methods(self) -> _AppClientBoxMethodsProtocol: + def _get_box_methods(self) -> _AppClientBoxMethods: """Get methods to access box storage for the current app.""" def get_all() -> dict[str, Any]: @@ -466,7 +533,7 @@ def _get_state_methods( # noqa: C901 state_getter: Callable[[], dict[str, AppState]], key_getter: Callable[[], dict[str, StorageKey]], map_getter: Callable[[], dict[str, StorageMap]], - ) -> _AppClientStateMethodsProtocol: + ) -> _AppClientStateMethods: def get_all() -> dict[str, Any]: state = state_getter() keys = key_getter() @@ -486,7 +553,7 @@ def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState state = app_state or state_getter() metadata = map_getter()[map_name] - prefix = bytes(metadata.prefix or "", "base64") + prefix = base64.b64decode(metadata.prefix or "") encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") value = next((s for s in state.values() if s.key_base64 == full_key), None) @@ -498,7 +565,7 @@ def get_map(map_name: str) -> dict[str, ABIValue]: state = state_getter() metadata = map_getter()[map_name] - prefix = metadata.prefix or "" + prefix = base64.b64decode(metadata.prefix or "").decode("utf-8") prefixed_state = {k: v for k, v in state.items() if k.startswith(prefix)} @@ -575,25 +642,33 @@ def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = return call_params def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: - call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.OptInOC)) + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.OptInOC) + ) return call_params - def delete(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + def delete(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: call_params: AppCallParams = AppCallParams( - **self._get_bare_params(params.__dict__, OnComplete.DeleteApplicationOC) + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.DeleteApplicationOC) ) return call_params - def clear_state(self, params: AppClientBareCallWithSendParams) -> AppCallParams: - call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.ClearStateOC)) + def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.ClearStateOC) + ) return call_params - def close_out(self, params: AppClientBareCallWithSendParams) -> AppCallParams: - call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.CloseOutOC)) + def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.CloseOutOC) + ) return call_params - def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> AppCallParams: - call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.NoOpOC)) + def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> AppCallParams: + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.NoOpOC) + ) return call_params @@ -683,12 +758,11 @@ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transacti if params.get("method"): input_params["method"] = self._app_spec.get_arc56_method(params["method"]).to_abi_method() - if params.get("args"): - input_params["args"] = self._client._get_abi_args_with_default_values( - method_name_or_signature=params["method"], - args=params["args"], - sender=self._client._get_sender(input_params["sender"]), - ) + input_params["args"] = self._client._get_abi_args_with_default_values( + method_name_or_signature=params["method"], + args=params.get("args"), + sender=self._client._get_sender(input_params["sender"]), + ) return input_params @@ -698,23 +772,35 @@ def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand - def update(self, params: AppClientBareCallWithCompilationAndSendParams) -> Transaction: - return self._algorand.create_transaction.app_update(self._client.params.bare.update(params)) + def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_update( + self._client.params.bare.update(params or AppClientBareCallWithCompilationAndSendParams()) + ) - def opt_in(self, params: AppClientBareCallWithSendParams) -> Transaction: - return self._algorand.create_transaction.app_call(self._client.params.bare.opt_in(params)) + def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_call( + self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) + ) - def delete(self, params: AppClientBareCallWithSendParams) -> Transaction: - return self._algorand.create_transaction.app_call(self._client.params.bare.delete(params)) + def delete(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_call( + self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) + ) - def clear_state(self, params: AppClientBareCallWithSendParams) -> Transaction: - return self._algorand.create_transaction.app_call(self._client.params.bare.clear_state(params)) + def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_call( + self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) + ) - def close_out(self, params: AppClientBareCallWithSendParams) -> Transaction: - return self._algorand.create_transaction.app_call(self._client.params.bare.close_out(params)) + def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_call( + self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) + ) - def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> Transaction: - return self._algorand.create_transaction.app_call(self._client.params.bare.call(params)) + def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> Transaction: + return self._algorand.create_transaction.app_call( + self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) + ) class _AppClientMethodCallTransactionCreator: @@ -757,8 +843,8 @@ def __init__(self, client: AppClient) -> None: def update( self, - params: AppClientBareCallWithCompilationAndSendParams, - ) -> SendAppTransactionResult: + params: AppClientBareCallWithCompilationAndSendParams | None = None, + ) -> SendAppTransactionResult[ABIReturn]: """Send an application update transaction. Args: @@ -771,36 +857,52 @@ def update( Returns: The result of sending the transaction """ + params = params or AppClientBareCallWithCompilationAndSendParams() compiled = self._client.compile_sourcemaps(params.deploy_time_params, params.updatable, params.deletable) bare_params = self._client.params.bare.update(params) bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params)) - return SendAppTransactionResult(**{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}) + return SendAppTransactionResult[ABIReturn]( + **{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}, + abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)), + ) - def opt_in(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call(self._client.params.bare.opt_in(params)) + def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) + ) ) - def delete(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call(self._client.params.bare.delete(params)) + def delete(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) + ) ) - def clear_state(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call(self._client.params.bare.clear_state(params)) + def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) + ) ) - def close_out(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call(self._client.params.bare.close_out(params)) + def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) + ) ) - def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call(self._client.params.bare.call(params)) + def call( + self, params: AppClientBareCallWithCallOnCompleteParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) + ) ) @@ -821,27 +923,43 @@ def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactio lambda: self._algorand.send.payment(self._client.params.fund_app_account(params)) ) - def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)) + def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)), + self._app_spec.get_arc56_method(params.method), + ) ) - def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)) + def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)), + self._app_spec.get_arc56_method(params.method), + ) ) - def update(self, params: AppClientMethodCallWithCompilationAndSendParams) -> SendAppUpdateTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)) + def update( + self, params: AppClientMethodCallWithCompilationAndSendParams + ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]: + result = self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)), + self._app_spec.get_arc56_method(params.method), + ) ) + assert isinstance(result, SendAppUpdateTransactionResult) + return result - def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: - return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)) + def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)), + self._app_spec.get_arc56_method(params.method), + ) ) - def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: is_read_only_call = ( params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None ) and self._app_spec.get_arc56_method(params.method).readonly @@ -863,7 +981,7 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR ) ) - return SendAppTransactionResult( + return SendAppTransactionResult[Arc56ReturnValueType]( tx_ids=simulate_response.tx_ids, transactions=simulate_response.transactions, transaction=simulate_response.transactions[-1], @@ -871,11 +989,16 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR confirmations=simulate_response.confirmations, group_id=simulate_response.group_id or "", returns=simulate_response.returns, - abi_return=simulate_response.returns[-1], + abi_return=simulate_response.returns[-1].get_arc56_value( + self._app_spec.get_arc56_method(params.method), self._app_spec.structs + ), ) return self._client._handle_call_errors( - lambda: self._algorand.send.app_call_method_call(self._client.params.call(params)) + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.call(params)), + self._app_spec.get_arc56_method(params.method), + ) ) @@ -1066,7 +1189,7 @@ def is_base64(s: str) -> bool: app_spec.source.get_decoded_approval(), template_params=deploy_time_params, deployment_metadata=( - {"updatable": updatable or False, "deletable": deletable or False} + {"updatable": updatable, "deletable": deletable} if updatable is not None or deletable is not None else None ), @@ -1383,7 +1506,7 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 self, *, method_name_or_signature: str, - args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, + args: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, sender: str, ) -> list[Any]: """Get ABI args with default values filled in. @@ -1428,10 +1551,9 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 # Get method return value default_method = self._app_spec.get_arc56_method(default_value.data) empty_args = [None] * len(default_method.args) - call_result = self._algorand.send.app_call_method_call( - AppCallMethodCallParams( - app_id=self._app_id, - method=algosdk.abi.Method.from_signature(default_value.data), + call_result = self.send.call( + AppClientMethodCallWithSendParams( + method=default_value.data, args=empty_args, sender=sender, ) @@ -1445,12 +1567,12 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 result.append( get_abi_tuple_from_abi_struct( call_result.abi_return, - self._app_spec.structs[str(default_method.returns.type)], + self._app_spec.structs[str(default_method.returns.struct)], self._app_spec.structs, ) ) - elif call_result.abi_return.value: - result.append(call_result.abi_return.value) + elif call_result.abi_return: + result.append(call_result.abi_return) case "local" | "global": # Get state value @@ -1503,3 +1625,21 @@ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transacti "onComplete": on_complete, "args": args, } + + def _process_method_call_return( + self, + result: Callable[[], SendAppUpdateTransactionResult[ABIReturn] | SendAppTransactionResult[ABIReturn]], + method: Method, + ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType] | SendAppTransactionResult[Arc56ReturnValueType]: + result_value = result() + abi_return = ( + result_value.abi_return.get_arc56_value(method, self._app_spec.structs) + if isinstance(result_value.abi_return, ABIReturn) + else None + ) + + if isinstance(result_value, SendAppUpdateTransactionResult): + return SendAppUpdateTransactionResult[Arc56ReturnValueType]( + **{**result_value.__dict__, "abi_return": abi_return} + ) + return SendAppTransactionResult[Arc56ReturnValueType](**{**result_value.__dict__, "abi_return": abi_return}) diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 34f84b3e..7357e556 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -8,6 +8,7 @@ from algosdk.logic import get_application_address from algosdk.v2client.indexer import IndexerClient +from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager from algokit_utils.config import config from algokit_utils.models.state import TealTemplateParams @@ -18,6 +19,7 @@ AppDeleteParams, AppUpdateMethodCallParams, AppUpdateParams, + TransactionComposer, ) from algokit_utils.transactions.transaction_sender import ( AlgorandClientTransactionSender, @@ -46,7 +48,20 @@ logger = config.logger -@dataclasses.dataclass +@dataclasses.dataclass(frozen=True) +class AppDeployMetaData: + """Metadata about an application stored in a transaction note during creation.""" + + name: str + version: str + deletable: bool | None + updatable: bool | None + + def dictify(self) -> dict[str, str | bool]: + return {k: v for k, v in asdict(self).items() if v is not None} + + +@dataclasses.dataclass(frozen=True) class AppReference: """Information about an Algorand app""" @@ -54,28 +69,39 @@ class AppReference: app_address: str -@dataclasses.dataclass -class AppDeployMetaData: - """Metadata about an application stored in a transaction note during creation. +@dataclasses.dataclass(frozen=True) +class AppMetaData: + """Complete metadata about a deployed app""" - The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field - as part of {py:meth}`ApplicationClient.deploy` - """ + reference: AppReference + deploy_metadata: AppDeployMetaData + created_round: int + updated_round: int + deleted: bool = False - name: str - version: str - deletable: bool | None - updatable: bool | None + @property + def app_id(self) -> int: + return self.reference.app_id + @property + def app_address(self) -> str: + return self.reference.app_address -@dataclasses.dataclass -class AppMetaData(AppReference, AppDeployMetaData): - """Metadata about a deployed app""" + @property + def name(self) -> str: + return self.deploy_metadata.name - created_round: int - updated_round: int - created_metadata: AppDeployMetaData - deleted: bool + @property + def version(self) -> str: + return self.deploy_metadata.version + + @property + def deletable(self) -> bool | None: + return self.deploy_metadata.deletable + + @property + def updatable(self) -> bool | None: + return self.deploy_metadata.updatable @dataclasses.dataclass @@ -151,9 +177,9 @@ class AppDeployParams: class AppDeployResponse: app: AppMetaData operation_performed: OperationPerformed - create_response: SendAppCreateTransactionResult | None = None - update_response: SendAppUpdateTransactionResult | None = None - delete_response: SendAppTransactionResult | None = None + create_response: SendAppCreateTransactionResult[ABIReturn] | None = None + update_response: SendAppUpdateTransactionResult[ABIReturn] | None = None + delete_response: SendAppTransactionResult[ABIReturn] | None = None class AppDeployer: @@ -170,14 +196,6 @@ def __init__( self._indexer = indexer self._app_lookups: dict[str, AppLookup] = {} - def _create_deploy_note(self, metadata: AppDeployMetaData) -> bytes: - note = { - "dapp_name": APP_DEPLOY_NOTE_DAPP, - "format": "j", - "data": metadata.__dict__, - } - return json.dumps(note).encode() - def deploy(self, deployment: AppDeployParams) -> AppDeployResponse: # Create new instances with updated notes logger.info( @@ -188,7 +206,13 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResponse: f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}", suppress_log=deployment.suppress_log, ) - note = self._create_deploy_note(deployment.metadata) + note = TransactionComposer.arc2_note( + { + "dapp_name": APP_DEPLOY_NOTE_DAPP, + "format": "j", + "data": deployment.metadata.dictify(), + } + ) create_params = dataclasses.replace(deployment.create_params, note=note) update_params = dataclasses.replace(deployment.update_params, note=note) @@ -335,10 +359,10 @@ def _create_app( ) app_metadata = AppMetaData( - app_id=create_response.app_id, - app_address=get_application_address(create_response.app_id), - **asdict(deployment.metadata), - created_metadata=deployment.metadata, + reference=AppReference( + app_id=create_response.app_id, app_address=get_application_address(create_response.app_id) + ), + deploy_metadata=deployment.metadata, created_round=create_response.confirmation.get("confirmed-round", 0) if isinstance(create_response.confirmation, dict) else 0, @@ -409,15 +433,13 @@ def _replace_app( result = composer.send() - create_response = SendAppCreateTransactionResult.from_composer_result(result, create_txn_index) - delete_response = SendAppTransactionResult.from_composer_result(result, delete_txn_index) + create_response = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index) + delete_response = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index) app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] app_metadata = AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - **deployment.metadata.__dict__, - created_metadata=deployment.metadata, + reference=AppReference(app_id=app_id, app_address=get_application_address(app_id)), + deploy_metadata=deployment.metadata, created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] deleted=False, @@ -465,12 +487,10 @@ def _update_app( ) app_metadata = AppMetaData( - app_id=existing_app.app_id, - app_address=existing_app.app_address, - created_metadata=existing_app.created_metadata, + reference=AppReference(app_id=existing_app.app_id, app_address=existing_app.app_address), + deploy_metadata=deployment.metadata, created_round=existing_app.created_round, updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, - **deployment.metadata.__dict__, deleted=False, ) @@ -571,7 +591,7 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = min_round=app["created-at-round"], address=creator_address, address_role="sender", - note_prefix=base64.b64encode(APP_DEPLOY_NOTE_DAPP.encode()), + note_prefix=APP_DEPLOY_NOTE_DAPP.encode(), limit=1, ) @@ -589,11 +609,14 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = if metadata.get("name"): app_lookup[metadata["name"]] = AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - created_metadata=metadata, + reference=AppReference(app_id=app_id, app_address=get_application_address(app_id)), + deploy_metadata=AppDeployMetaData( + name=metadata["name"], + version=metadata.get("version", "1.0"), + deletable=metadata.get("deletable"), + updatable=metadata.get("updatable"), + ), created_round=creation_txn["confirmed-round"], - **metadata, updated_round=creation_txn["confirmed-round"], deleted=app.get("deleted", False), ) diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 2316a57b..6e9498be 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -1,10 +1,9 @@ import base64 -from collections.abc import Callable +import dataclasses +from collections.abc import Callable, Sequence from dataclasses import asdict, dataclass, replace -from typing import Any, TypeVar +from typing import Any, Generic, Literal, Protocol, TypeVar -from algosdk import transaction -from algosdk.abi import Method from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.source_map import SourceMap from algosdk.transaction import OnComplete, Transaction @@ -13,19 +12,21 @@ from algokit_utils._legacy_v2.application_specification import ApplicationSpecification from algokit_utils.applications.abi import ( ABIReturn, - ABIStruct, - ABIValue, Arc56ReturnValueType, get_abi_decoded_value, get_abi_tuple_from_abi_struct, ) from algokit_utils.applications.app_client import ( AppClient, + AppClientBareCallCreateParams, AppClientBareCallParams, AppClientCompilationParams, AppClientCompilationResult, + AppClientCreateSchema, + AppClientMethodCallCreateParams, AppClientMethodCallParams, AppClientParams, + TypedAppClientProtocol, ) from algokit_utils.applications.app_deployer import ( AppDeployMetaData, @@ -38,7 +39,7 @@ OperationPerformed, ) from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME -from algokit_utils.applications.app_spec.arc56 import Arc56Contract +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Method from algokit_utils.models.application import ( AppSourceMaps, ) @@ -75,6 +76,7 @@ "SendAppCreateFactoryTransactionResult", "SendAppFactoryTransactionResult", "SendAppUpdateFactoryTransactionResult", + "TypedAppFactoryProtocol", ] @@ -92,10 +94,22 @@ class AppFactoryParams: @dataclass(kw_only=True, frozen=True) -class AppFactoryCreateParams(AppClientBareCallParams, AppClientCompilationParams): - on_complete: transaction.OnComplete | None = None - schema: dict[str, int] | None = None - extra_program_pages: int | None = None +class _AppFactoryCreateBaseParams(AppClientCreateSchema, AppClientCompilationParams): + on_complete: ( + Literal[ + OnComplete.NoOpOC, + OnComplete.UpdateApplicationOC, + OnComplete.DeleteApplicationOC, + OnComplete.OptInOC, + OnComplete.CloseOutOC, + ] + | None + ) = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateParams(_AppFactoryCreateBaseParams, AppClientBareCallParams): + pass @dataclass(kw_only=True, frozen=True) @@ -104,19 +118,23 @@ class AppFactoryCreateWithSendParams(AppFactoryCreateParams, SendParams): @dataclass(kw_only=True, frozen=True) -class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompilationParams): - on_complete: transaction.OnComplete | None = None - schema: dict[str, int] | None = None - extra_program_pages: int | None = None +class AppFactoryCreateMethodCallParams(_AppFactoryCreateBaseParams, AppClientMethodCallParams): + pass + + +ABIReturnT = TypeVar( + "ABIReturnT", + bound=Arc56ReturnValueType, +) @dataclass(frozen=True, kw_only=True) -class AppFactoryCreateMethodCallResult(SendSingleTransactionResult): +class AppFactoryCreateMethodCallResult(SendSingleTransactionResult, Generic[ABIReturnT]): app_id: int app_address: str compiled_approval: Any | None = None compiled_clear: Any | None = None - abi_return: ABIValue | ABIStruct | None = None + abi_return: ABIReturnT | None = None @dataclass(kw_only=True, frozen=True) @@ -125,18 +143,18 @@ class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, @dataclass(frozen=True) -class SendAppFactoryTransactionResult(SendAppTransactionResult): - abi_value: Arc56ReturnValueType | None = None +class SendAppFactoryTransactionResult(SendAppTransactionResult[Arc56ReturnValueType]): + pass @dataclass(frozen=True) -class SendAppUpdateFactoryTransactionResult(SendAppUpdateTransactionResult): - abi_value: Arc56ReturnValueType | None = None +class SendAppUpdateFactoryTransactionResult(SendAppUpdateTransactionResult[Arc56ReturnValueType]): + pass @dataclass(frozen=True, kw_only=True) -class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult): - abi_value: Arc56ReturnValueType | None = None +class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult[Arc56ReturnValueType]): + pass @dataclass(frozen=True) @@ -158,7 +176,7 @@ def from_deploy_response( app_compilation_data: AppClientCompilationResult | None = None, ) -> Self: def to_factory_response( - response_data: SendAppTransactionResult + response_data: SendAppTransactionResult[ABIReturn] | SendAppCreateTransactionResult | SendAppUpdateTransactionResult | None, @@ -167,25 +185,24 @@ def to_factory_response( if not response_data: return None - abi_value = None + response_data_dict = asdict(response_data) abi_return = response_data.abi_return if abi_return and abi_return.method: - abi_value = abi_return.get_arc56_value(params.method, app_spec.structs) + response_data_dict["abi_return"] = abi_return.get_arc56_value(params.method, app_spec.structs) match response_data: case SendAppCreateTransactionResult(): - return SendAppCreateFactoryTransactionResult(**asdict(response_data), abi_value=abi_value) + return SendAppCreateFactoryTransactionResult(**response_data_dict) case SendAppUpdateTransactionResult(): - raw_response = asdict(response_data) - raw_response["compiled_approval"] = ( + response_data_dict["compiled_approval"] = ( app_compilation_data.compiled_approval if app_compilation_data else None ) - raw_response["compiled_clear"] = ( + response_data_dict["compiled_clear"] = ( app_compilation_data.compiled_clear if app_compilation_data else None ) - return SendAppUpdateFactoryTransactionResult(**raw_response, abi_value=abi_value) + return SendAppUpdateFactoryTransactionResult(**response_data_dict) case SendAppTransactionResult(): - return SendAppFactoryTransactionResult(**asdict(response_data), abi_value=abi_value) + return SendAppFactoryTransactionResult(**response_data_dict) return cls( app=response.app, @@ -216,51 +233,61 @@ def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParam compiled = self._factory.compile(base_params) return AppCreateParams( - approval_program=compiled.approval_program, - clear_state_program=compiled.clear_state_program, - schema=base_params.schema - or { - "global_bytes": self._factory._app_spec.state.schema.global_state.bytes, - "global_ints": self._factory._app_spec.state.schema.global_state.ints, - "local_bytes": self._factory._app_spec.state.schema.local_state.bytes, - "local_ints": self._factory._app_spec.state.schema.local_state.ints, - }, - sender=self._factory._get_sender(base_params.sender), - signer=self._factory._get_signer(base_params.sender, base_params.signer), - on_complete=base_params.on_complete or OnComplete.NoOpOC, - extra_program_pages=base_params.extra_program_pages, + **{ + **{ + param: value + for param, value in asdict(base_params).items() + if param in {f.name for f in dataclasses.fields(AppCreateParams)} + }, + "approval_program": compiled.approval_program, + "clear_state_program": compiled.clear_state_program, + "schema": base_params.schema + or { + "global_byte_slices": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_byte_slices": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, + }, + "sender": self._factory._get_sender(base_params.sender), + "signer": self._factory._get_signer(base_params.sender, base_params.signer), + "on_complete": base_params.on_complete or OnComplete.NoOpOC, + } ) def deploy_update(self, params: AppClientBareCallParams | None = None) -> AppUpdateParams: return AppUpdateParams( - app_id=0, - approval_program="", - clear_state_program="", - sender=self._factory._get_sender(params.sender if params else None), - on_complete=OnComplete.UpdateApplicationOC, - signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), - note=params.note if params else None, - lease=params.lease if params else None, - rekey_to=params.rekey_to if params else None, - account_references=params.account_references if params else None, - app_references=params.app_references if params else None, - asset_references=params.asset_references if params else None, - box_references=params.box_references if params else None, + **{ + **{ + param: value + for param, value in asdict(params or AppClientBareCallParams()).items() + if param in {f.name for f in dataclasses.fields(AppUpdateParams)} + }, + "app_id": 0, + "approval_program": "", + "clear_state_program": "", + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.UpdateApplicationOC, + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + } ) def deploy_delete(self, params: AppClientBareCallParams | None = None) -> AppDeleteParams: return AppDeleteParams( - app_id=0, - sender=self._factory._get_sender(params.sender if params else None), - signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), - on_complete=OnComplete.DeleteApplicationOC, - note=params.note if params else None, - lease=params.lease if params else None, - rekey_to=params.rekey_to if params else None, - account_references=params.account_references if params else None, - app_references=params.app_references if params else None, - asset_references=params.asset_references if params else None, - box_references=params.box_references if params else None, + **{ + **{ + param: value + for param, value in asdict(params or AppClientBareCallParams()).items() + if param in {f.name for f in dataclasses.fields(AppDeleteParams)} + }, + "app_id": 0, + "sender": self._factory._get_sender(params.sender if params else None), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "on_complete": OnComplete.DeleteApplicationOC, + } ) @@ -277,52 +304,70 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCal compiled = self._factory.compile(params) return AppCreateMethodCallParams( - app_id=0, - approval_program=compiled.approval_program, - clear_state_program=compiled.clear_state_program, - schema=params.schema - or { - "global_bytes": self._factory._app_spec.state.schema.global_state.bytes, - "global_ints": self._factory._app_spec.state.schema.global_state.ints, - "local_bytes": self._factory._app_spec.state.schema.local_state.bytes, - "local_ints": self._factory._app_spec.state.schema.local_state.ints, - }, - sender=self._factory._get_sender(params.sender), - signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), - method=self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), - args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), - on_complete=params.on_complete or OnComplete.NoOpOC, - note=params.note, - lease=params.lease, - rekey_to=params.rekey_to, + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppCreateMethodCallParams)} + }, + "app_id": 0, + "approval_program": compiled.approval_program, + "clear_state_program": compiled.clear_state_program, + "schema": params.schema + or { + "global_byte_slices": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_byte_slices": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, + }, + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": params.on_complete or OnComplete.NoOpOC, + } ) def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCallParams: return AppUpdateMethodCallParams( - app_id=0, - approval_program="", - clear_state_program="", - sender=self._factory._get_sender(params.sender), - signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), - method=self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), - args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), - on_complete=OnComplete.UpdateApplicationOC, - note=params.note, - lease=params.lease, - rekey_to=params.rekey_to, + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppUpdateMethodCallParams)} + }, + "app_id": 0, + "approval_program": "", + "clear_state_program": "", + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": OnComplete.UpdateApplicationOC, + } ) def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: return AppDeleteMethodCallParams( - app_id=0, - sender=self._factory._get_sender(params.sender), - signer=self._factory._get_signer(params.sender if params else None, params.signer if params else None), - method=self._factory.app_spec.get_arc56_method(params.method).to_abi_method(), - args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), - on_complete=OnComplete.DeleteApplicationOC, - note=params.note, - lease=params.lease, - rekey_to=params.rekey_to, + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppDeleteMethodCallParams)} + }, + "app_id": 0, + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory.app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": OnComplete.DeleteApplicationOC, + } ) @@ -385,7 +430,7 @@ def create( self._factory.get_app_client_by_id( app_id=result.app_id, ), - SendAppCreateTransactionResult( + SendAppCreateTransactionResult[ABIReturn]( transaction=result.transaction, confirmation=result.confirmation, app_id=result.app_id, @@ -410,7 +455,9 @@ def __init__(self, factory: "AppFactory") -> None: def bare(self) -> _AppFactoryBareSendAccessor: return self._bare - def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, AppFactoryCreateMethodCallResult]: + def create( + self, params: AppFactoryCreateMethodCallParams + ) -> tuple[AppClient, AppFactoryCreateMethodCallResult[Arc56ReturnValueType]]: create_params = replace( params, updatable=params.updatable if params.updatable is not None else self._factory._updatable, @@ -433,7 +480,7 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, A result = self._factory._handle_call_errors( lambda: self._factory._parse_method_call_return( lambda: self._algorand.send.app_create_method_call(self._factory.params.create(create_params)), - self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + self._factory._app_spec.get_arc56_method(params.method), ) ) @@ -441,7 +488,7 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, A self._factory.get_app_client_by_id( app_id=result.app_id, ), - AppFactoryCreateMethodCallResult( + AppFactoryCreateMethodCallResult[Arc56ReturnValueType]( transaction=result.transaction, confirmation=result.confirmation, tx_id=result.tx_id, @@ -459,6 +506,40 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, A ) +CreateParamsT = TypeVar( # noqa: PLC0105 + "CreateParamsT", bound=AppClientMethodCallCreateParams | AppClientBareCallCreateParams, contravariant=True +) +UpdateParamsT = TypeVar("UpdateParamsT", bound=AppClientMethodCallParams | AppClientBareCallParams, contravariant=True) # noqa: PLC0105 +DeleteParamsT = TypeVar("DeleteParamsT", bound=AppClientMethodCallParams | AppClientBareCallParams, contravariant=True) # noqa: PLC0105 + + +class TypedAppFactoryProtocol(Protocol, Generic[CreateParamsT, UpdateParamsT, DeleteParamsT]): + def __init__( + self, + algorand: AlgorandClientProtocol, + **kwargs: Any, + ) -> None: ... + + def deploy( # noqa: PLR0913 + self, + *, + deploy_time_params: TealTemplateParams | None = None, + on_update: OnUpdate = OnUpdate.Fail, + on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + create_params: CreateParamsT | None = None, + update_params: UpdateParamsT | None = None, + delete_params: DeleteParamsT | None = None, + existing_deployments: AppLookup | None = None, + ignore_cache: bool = False, + updatable: bool | None = None, + deletable: bool | None = None, + app_name: str | None = None, + max_rounds_to_wait: int | None = None, + suppress_log: bool = False, + populate_app_call_resources: bool = False, + ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... + + class AppFactory: def __init__(self, params: AppFactoryParams) -> None: self._app_spec = AppClient.normalise_app_spec(params.app_spec) @@ -506,7 +587,7 @@ def deploy( # noqa: PLR0913 deploy_time_params: TealTemplateParams | None = None, on_update: OnUpdate = OnUpdate.Fail, on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, - create_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + create_params: AppClientMethodCallCreateParams | AppClientBareCallCreateParams | None = None, update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, existing_deployments: AppLookup | None = None, @@ -519,7 +600,6 @@ def deploy( # noqa: PLR0913 populate_app_call_resources: bool = False, ) -> tuple[AppClient, AppFactoryDeployResponse]: """Deploy the application with the specified parameters.""" - # Resolve control parameters with factory defaults resolved_updatable = ( updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") @@ -531,7 +611,7 @@ def deploy( # noqa: PLR0913 def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: """Prepare create arguments based on parameter type.""" - if create_params and isinstance(create_params, AppClientMethodCallParams): + if create_params and isinstance(create_params, AppClientMethodCallCreateParams): return self.params.create( AppFactoryCreateMethodCallParams( **asdict(create_params), @@ -541,7 +621,7 @@ def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: ) ) - base_params = create_params or AppClientBareCallParams() + base_params = create_params or AppClientBareCallCreateParams() return self.params.bare.create( AppFactoryCreateParams( **asdict(base_params) if base_params else {}, @@ -734,9 +814,9 @@ def _parse_method_call_return( [], SendAppTransactionResult | SendAppCreateTransactionResult | SendAppUpdateTransactionResult ], method: Method, - ) -> AppFactoryCreateMethodCallResult: + ) -> AppFactoryCreateMethodCallResult[Arc56ReturnValueType]: result_value = result() - return AppFactoryCreateMethodCallResult( + return AppFactoryCreateMethodCallResult[Arc56ReturnValueType]( **{ **result_value.__dict__, "abi_return": result_value.abi_return.get_arc56_value(method, self._app_spec.structs) @@ -748,7 +828,7 @@ def _parse_method_call_return( def _get_create_abi_args_with_default_values( self, method_name_or_signature: str, - user_args: list[Any] | None, + user_args: Sequence[Any] | None, ) -> list[Any]: """ Builds a list of ABI argument values for creation calls, applying default diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index ad198a81..fcbdfe77 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -149,7 +149,7 @@ def compile_teal_template( self, teal_template_code: str, template_params: TealTemplateParams | None = None, - deployment_metadata: Mapping[str, bool] | None = None, + deployment_metadata: Mapping[str, bool | None] | None = None, ) -> CompiledTeal: teal_code = AppManager.strip_teal_comments(teal_template_code) teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) @@ -207,7 +207,7 @@ def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: name = AppManager.get_box_reference(box_name)[1] box_result = self._algod.application_box_by_name(app_id, name) assert isinstance(box_result, dict) - return bytes(box_result["value"], "utf-8") + return base64.b64decode(box_result["value"]) def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: return [self.get_box_value(app_id, box_name) for box_name in box_names] @@ -216,7 +216,7 @@ def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_ value = self.get_box_value(app_id, box_name) try: parse_to_tuple = isinstance(abi_type, algosdk.abi.TupleType) - decoded_value = abi_type.decode(base64.b64decode(value)) + decoded_value = abi_type.decode(value) return tuple(decoded_value) if parse_to_tuple else decoded_value except Exception as e: raise ValueError(f"Failed to decode box value {value.decode('utf-8')} with ABI type {abi_type}") from e @@ -270,10 +270,16 @@ def get_abi_return( def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: state_values: dict[str, AppState] = {} + def decode_bytes_to_str(value: bytes) -> str: + try: + return value.decode("utf-8") + except UnicodeDecodeError: + return value.hex() + for state_val in state: key_base64 = state_val["key"] key_raw = base64.b64decode(key_base64) - key = key_raw.decode("utf-8") + key = decode_bytes_to_str(key_raw) teal_value = state_val["value"] data_type_flag = teal_value.get("action", teal_value.get("type")) @@ -286,7 +292,7 @@ def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: key_base64=key_base64, value_raw=value_raw, value_base64=value_base64, - value=value_raw.decode("utf-8"), + value=decode_bytes_to_str(value_raw), ) elif data_type_flag == DataTypeFlag.UINT: value = teal_value.get("uint", 0) @@ -323,22 +329,26 @@ def replace_template_variables(program: str, template_values: TealTemplateParams return "\n".join(program_lines) @staticmethod - def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: Mapping[str, bool]) -> str: - if params.get("updatable") is not None: + def replace_teal_template_deploy_time_control_params( + teal_template_code: str, params: Mapping[str, bool | None] + ) -> str: + updatable = params.get("updatable") + if updatable is not None: if UPDATABLE_TEMPLATE_NAME not in teal_template_code: raise ValueError( f"Deploy-time updatability control requested for app deployment, but {UPDATABLE_TEMPLATE_NAME} " "not present in TEAL code" ) - teal_template_code = teal_template_code.replace(UPDATABLE_TEMPLATE_NAME, str(int(params["updatable"]))) + teal_template_code = teal_template_code.replace(UPDATABLE_TEMPLATE_NAME, str(int(updatable))) - if params.get("deletable") is not None: + deletable = params.get("deletable") + if deletable is not None: if DELETABLE_TEMPLATE_NAME not in teal_template_code: raise ValueError( f"Deploy-time deletability control requested for app deployment, but {DELETABLE_TEMPLATE_NAME} " "not present in TEAL code" ) - teal_template_code = teal_template_code.replace(DELETABLE_TEMPLATE_NAME, str(int(params["deletable"]))) + teal_template_code = teal_template_code.replace(DELETABLE_TEMPLATE_NAME, str(int(deletable))) return teal_template_code diff --git a/src/algokit_utils/applications/app_spec/arc32.py b/src/algokit_utils/applications/app_spec/arc32.py index 3be8a42c..505e2b27 100644 --- a/src/algokit_utils/applications/app_spec/arc32.py +++ b/src/algokit_utils/applications/app_spec/arc32.py @@ -169,8 +169,8 @@ def dictify(self) -> dict: "bare_call_config": _encode_method_config(self.bare_call_config), } - def to_json(self) -> str: - return json.dumps(self.dictify(), indent=4) + def to_json(self, indent: int | None = None) -> str: + return json.dumps(self.dictify(), indent=indent) @staticmethod def from_json(application_spec: str) -> "Arc32Contract": diff --git a/src/algokit_utils/applications/app_spec/arc56.py b/src/algokit_utils/applications/app_spec/arc56.py index 80d13d54..ffb61534 100644 --- a/src/algokit_utils/applications/app_spec/arc56.py +++ b/src/algokit_utils/applications/app_spec/arc56.py @@ -687,7 +687,7 @@ def from_dict(application_spec: dict) -> Arc56Contract: data["methods"] = [Method.from_dict(item) for item in data["methods"]] data["state"] = State.from_dict(data["state"]) data["structs"] = { - key: [StructField.from_dict(item) for item in value] for key, value in data["structs"].items() + key: [StructField.from_dict(item) for item in value] for key, value in application_spec["structs"].items() } if data.get("byte_code"): data["byte_code"] = ByteCode.from_dict(data["byte_code"]) @@ -742,8 +742,8 @@ def get_abi_struct_from_abi_tuple( result[key] = value return result - def to_json(self) -> str: - return json.dumps(self.dictify(), indent=4) + def to_json(self, indent: int | None = None) -> str: + return json.dumps(self.dictify(), indent=indent) def dictify(self) -> dict: return asdict(self, dict_factory=_arc56_dict_factory()) diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 74c2c576..aec3d27e 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,6 +1,6 @@ import os from dataclasses import dataclass -from typing import Literal +from typing import Literal, TypeVar from urllib import parse import algosdk @@ -12,9 +12,9 @@ # from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils.applications.app_client import AppClient, AppClientParams +from algokit_utils.applications.app_client import AppClient, AppClientParams, TypedAppClientProtocol from algokit_utils.applications.app_deployer import AppLookup -from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams, TypedAppFactoryProtocol from algokit_utils.applications.app_spec.arc56 import Arc56Contract from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs @@ -27,6 +27,9 @@ "NetworkDetail", ] +TypedFactoryT = TypeVar("TypedFactoryT", bound=TypedAppFactoryProtocol) +TypedAppClientT = TypeVar("TypedAppClientT", bound=TypedAppClientProtocol) + class AlgoSdkClients: def __init__( @@ -273,6 +276,115 @@ def get_indexer_client_from_environment() -> IndexerClient: def genesis_id_is_localnet(genesis_id: str) -> bool: return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + def get_typed_app_client_by_creator_and_name( + self, + typed_client: type[TypedAppClientT], + *, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + ) -> TypedAppClientT: + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client.from_creator_and_name( + creator_address=creator_address, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + algorand=self._algorand, + ) + + def get_typed_app_client_by_id( + self, + typed_client: type[TypedAppClientT], + *, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> TypedAppClientT: + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client( + app_id=app_id, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + def get_typed_app_client_by_network( + self, + typed_client: type[TypedAppClientT], + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> TypedAppClientT: + """Returns a new typed client, resolves the app ID for the current network. + + Uses pre-determined network-specific app IDs specified in the ARC-56 app spec. + If no IDs are in the app spec or the network isn't recognised, an error is thrown. + + Args: + typed_client: The typed client class to instantiate + default_sender: Optional default sender address + default_signer: Optional default transaction signer + + Returns: + The typed client instance + """ + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client.from_network( + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + def get_typed_app_factory( + self, + typed_factory: type[TypedFactoryT], + *, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + version: str | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + deploy_time_params: TealTemplateParams | None = None, + ) -> TypedFactoryT: + if not self._algorand: + raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") + + return typed_factory( + algorand=self._algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + version=version, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + @staticmethod def get_config_from_environment_or_localnet() -> AlgoClientConfigs: """Retrieve client configuration from environment variables or fallback to localnet defaults. diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py index b2950f12..46d2ddba 100644 --- a/src/algokit_utils/models/application.py +++ b/src/algokit_utils/models/application.py @@ -43,7 +43,7 @@ class AppInformation: @dataclass(kw_only=True, frozen=True) class CompiledTeal: teal: str - compiled: bytes + compiled: str compiled_hash: str compiled_base64_to_bytes: bytes source_map: algosdk.source_map.SourceMap | None diff --git a/src/algokit_utils/protocols/client.py b/src/algokit_utils/protocols/client.py index 4ae98b4b..e7199427 100644 --- a/src/algokit_utils/protocols/client.py +++ b/src/algokit_utils/protocols/client.py @@ -2,9 +2,16 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable +import typing_extensions + if TYPE_CHECKING: + from algosdk.atomic_transaction_composer import TransactionSigner + from algosdk.transaction import SuggestedParams + + from algokit_utils.accounts import AccountManager from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager + from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import ClientManager from algokit_utils.transactions.transaction_composer import TransactionComposer from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator @@ -29,7 +36,27 @@ def send(self) -> AlgorandClientTransactionSender: ... @property def create_transaction(self) -> AlgorandClientTransactionCreator: ... - def new_group(self) -> TransactionComposer: ... - @property def client(self) -> ClientManager: ... + + @property + def account(self) -> AccountManager: ... + + @property + def asset(self) -> AssetManager: ... + + def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self: ... + + def set_default_signer(self, signer: TransactionSigner) -> typing_extensions.Self: ... + + def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extensions.Self: ... + + def set_suggested_params( + self, suggested_params: SuggestedParams, until: float | None = None + ) -> typing_extensions.Self: ... + + def set_suggested_params_timeout(self, timeout: int) -> typing_extensions.Self: ... + + def get_suggested_params(self) -> SuggestedParams: ... + + def new_group(self) -> TransactionComposer: ... diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index aee899dc..5d3663b3 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,14 +1,18 @@ from __future__ import annotations +import base64 +import json import math +import re from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, TypedDict, Union import algosdk import algosdk.atomic_transaction_composer import algosdk.v2client.models from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, + SimulateAtomicTransactionResponse, TransactionSigner, TransactionWithSigner, ) @@ -42,8 +46,10 @@ "AppCallParams", "AppCreateMethodCallParams", "AppCreateParams", + "AppCreateSchema", "AppDeleteMethodCallParams", "AppDeleteParams", + "AppMethodCallTransactionArgument", "AppUpdateMethodCallParams", "AppUpdateParams", "AssetConfigParams", @@ -53,6 +59,7 @@ "AssetOptInParams", "AssetOptOutParams", "AssetTransferParams", + "BuiltTransactions", "MethodCallParams", "OfflineKeyRegistrationParams", "OnlineKeyRegistrationParams", @@ -324,6 +331,13 @@ class AppCallParams(_CommonTxnWithSendParams): box_references: list[BoxReference | BoxIdentifier] | None = None +class AppCreateSchema(TypedDict): + global_ints: int + global_byte_slices: int + local_ints: int + local_byte_slices: int + + @dataclass(kw_only=True, frozen=True) class AppCreateParams(_CommonTxnWithSendParams): """ @@ -345,7 +359,7 @@ class AppCreateParams(_CommonTxnWithSendParams): approval_program: str | bytes clear_state_program: str | bytes - schema: dict[str, int] | None = None + schema: AppCreateSchema | None = None on_complete: OnComplete | None = None args: list[bytes] | None = None account_references: list[str] | None = None @@ -410,7 +424,7 @@ class _BaseAppMethodCall(_CommonTxnWithSendParams): app_references: list[int] | None = None asset_references: list[int] | None = None box_references: list[BoxReference | BoxIdentifier] | None = None - schema: dict[str, int] | None = None + schema: AppCreateSchema | None = None @dataclass(kw_only=True, frozen=True) @@ -466,7 +480,7 @@ class AppCreateMethodCallParams(_BaseAppMethodCall): approval_program: str | bytes clear_state_program: str | bytes - schema: dict[str, int] | None = None + schema: AppCreateSchema | None = None on_complete: OnComplete | None = None extra_program_pages: int | None = None @@ -613,7 +627,9 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 # Get group ID if multiple transactions group_id = None if len(transactions_to_send) > 1: - group_id = transactions_to_send[0].group.hex() if transactions_to_send[0].group else None + group_id = ( + base64.b64encode(transactions_to_send[0].group).decode("utf-8") if transactions_to_send[0].group else "" + ) if not suppress_log: logger.info(f"Sending group of {len(transactions_to_send)} transactions ({group_id})") @@ -917,6 +933,18 @@ def send( except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e + def _handle_simulate_error(self, simulate_response: SimulateAtomicTransactionResponse) -> None: + # const failedGroup = simulateResponse?.txnGroups[0] + failed_group = simulate_response.simulate_response.get("txn-groups", [{}])[0] + failure_message = failed_group.get("failure-message") + failed_at = [str(x) for x in failed_group.get("failed-at", [])] + if failure_message: + error_message = ( + f"Transaction failed at transaction(s) {', '.join(failed_at) if failed_at else 'N/A'} in the group. " + f"{failure_message}" + ) + raise Exception(error_message) + def simulate( self, allow_more_logs: bool | None = None, @@ -952,7 +980,7 @@ def simulate( simulation_round, skip_signatures, ) - + self._handle_simulate_error(response) return SendAtomicTransactionComposerResults( confirmations=response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ "txn-results" @@ -975,7 +1003,7 @@ def simulate( simulation_round, skip_signatures, ) - + self._handle_simulate_error(response) confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ "txn-results" ] @@ -999,7 +1027,19 @@ def arc2_note(note: Arc2TransactionNote) -> bytes: :param note: The ARC-2 note to encode. """ - arc2_payload = f"{note['dapp_name']}:{note['format']}{note['data']}" + pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$" + if not re.match(pattern, note["dapp_name"]): + raise ValueError( + "dapp_name must be 5-32 chars, start with alphanumeric, " + "and contain only alphanumeric, _, /, @, ., or -" + ) + + data = note["data"] + if note["format"] == "j" and isinstance(data, (dict | list)): + # Ensure JSON data uses double quotes + data = json.dumps(data) + + arc2_payload = f"{note['dapp_name']}:{note['format']}{data}" return arc2_payload.encode("utf-8") def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSigner]: @@ -1131,13 +1171,13 @@ def _build_method_call( # noqa: C901, PLR0912 "accounts": params.account_references, "global_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("global_ints", 0), - num_byte_slices=params.schema.get("global_bytes", 0), + num_byte_slices=params.schema.get("global_byte_slices", 0), ) if params.schema else None, "local_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("local_ints", 0), - num_byte_slices=params.schema.get("local_bytes", 0), + num_byte_slices=params.schema.get("local_byte_slices", 0), ) if params.schema else None, @@ -1238,11 +1278,11 @@ def _build_app_call( **txn_params, "global_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("global_ints", 0), - num_byte_slices=params.schema.get("global_bytes", 0), + num_byte_slices=params.schema.get("global_byte_slices", 0), ), "local_schema": algosdk.transaction.StateSchema( num_uints=params.schema.get("local_ints", 0), - num_byte_slices=params.schema.get("local_bytes", 0), + num_byte_slices=params.schema.get("local_byte_slices", 0), ), "extra_pages": params.extra_program_pages or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index f0ffdf83..34d486c1 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,6 +1,6 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any, TypeVar +from typing import Any, Generic, TypeVar import algosdk import algosdk.atomic_transaction_composer @@ -103,19 +103,22 @@ class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): asset_id: int +ABIReturnT = TypeVar("ABIReturnT") + + @dataclass(frozen=True) -class SendAppTransactionResult(SendSingleTransactionResult): - abi_return: ABIReturn | None = None +class SendAppTransactionResult(SendSingleTransactionResult, Generic[ABIReturnT]): + abi_return: ABIReturnT | None = None @dataclass(frozen=True) -class SendAppUpdateTransactionResult(SendAppTransactionResult): +class SendAppUpdateTransactionResult(SendAppTransactionResult[ABIReturnT]): compiled_approval: Any | None = None compiled_clear: Any | None = None @dataclass(frozen=True, kw_only=True) -class SendAppCreateTransactionResult(SendAppUpdateTransactionResult): +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult[ABIReturnT]): app_id: int app_address: str @@ -180,10 +183,10 @@ def _send_app_call( c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], pre_log: Callable[[T, Transaction], str] | None = None, post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppTransactionResult]: - def send_app_call(params: T) -> SendAppTransactionResult: + ) -> Callable[[T], SendAppTransactionResult[ABIReturn]]: + def send_app_call(params: T) -> SendAppTransactionResult[ABIReturn]: result = self._send(c, pre_log, post_log)(params) - return SendAppTransactionResult( + return SendAppTransactionResult[ABIReturn]( **result.__dict__, abi_return=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), ) @@ -195,8 +198,8 @@ def _send_app_update_call( c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], pre_log: Callable[[T, Transaction], str] | None = None, post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppUpdateTransactionResult]: - def send_app_update_call(params: T) -> SendAppUpdateTransactionResult: + ) -> Callable[[T], SendAppUpdateTransactionResult[ABIReturn]]: + def send_app_update_call(params: T) -> SendAppUpdateTransactionResult[ABIReturn]: result = self._send_app_call(c, pre_log, post_log)(params) if not isinstance( @@ -215,7 +218,7 @@ def send_app_update_call(params: T) -> SendAppUpdateTransactionResult: else None ) - return SendAppUpdateTransactionResult( + return SendAppUpdateTransactionResult[ABIReturn]( **result.__dict__, compiled_approval=compiled_approval, compiled_clear=compiled_clear, @@ -228,12 +231,12 @@ def _send_app_create_call( c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], pre_log: Callable[[T, Transaction], str] | None = None, post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppCreateTransactionResult]: - def send_app_create_call(params: T) -> SendAppCreateTransactionResult: + ) -> Callable[[T], SendAppCreateTransactionResult[ABIReturn]]: + def send_app_create_call(params: T) -> SendAppCreateTransactionResult[ABIReturn]: result = self._send_app_update_call(c, pre_log, post_log)(params) app_id = int(result.confirmation["application-index"]) # type: ignore[call-overload] - return SendAppCreateTransactionResult( + return SendAppCreateTransactionResult[ABIReturn]( **result.__dict__, app_id=app_id, app_address=algosdk.logic.get_application_address(app_id), @@ -358,35 +361,35 @@ def asset_opt_out( ), )(params) - def app_create(self, params: AppCreateParams) -> SendAppCreateTransactionResult: + def app_create(self, params: AppCreateParams) -> SendAppCreateTransactionResult[ABIReturn]: """Create a new application.""" return self._send_app_create_call(lambda c: c.add_app_create)(params) - def app_update(self, params: AppUpdateParams) -> SendAppUpdateTransactionResult: + def app_update(self, params: AppUpdateParams) -> SendAppUpdateTransactionResult[ABIReturn]: """Update an application.""" return self._send_app_update_call(lambda c: c.add_app_update)(params) - def app_delete(self, params: AppDeleteParams) -> SendAppTransactionResult: + def app_delete(self, params: AppDeleteParams) -> SendAppTransactionResult[ABIReturn]: """Delete an application.""" return self._send_app_call(lambda c: c.add_app_delete)(params) - def app_call(self, params: AppCallParams) -> SendAppTransactionResult: + def app_call(self, params: AppCallParams) -> SendAppTransactionResult[ABIReturn]: """Call an application.""" return self._send_app_call(lambda c: c.add_app_call)(params) - def app_create_method_call(self, params: AppCreateMethodCallParams) -> SendAppCreateTransactionResult: + def app_create_method_call(self, params: AppCreateMethodCallParams) -> SendAppCreateTransactionResult[ABIReturn]: """Call an application's create method.""" return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params) - def app_update_method_call(self, params: AppUpdateMethodCallParams) -> SendAppUpdateTransactionResult: + def app_update_method_call(self, params: AppUpdateMethodCallParams) -> SendAppUpdateTransactionResult[ABIReturn]: """Call an application's update method.""" return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params) - def app_delete_method_call(self, params: AppDeleteMethodCallParams) -> SendAppTransactionResult: + def app_delete_method_call(self, params: AppDeleteMethodCallParams) -> SendAppTransactionResult[ABIReturn]: """Call an application's delete method.""" return self._send_app_call(lambda c: c.add_app_delete_method_call)(params) - def app_call_method_call(self, params: AppCallMethodCallParams) -> SendAppTransactionResult: + def app_call_method_call(self, params: AppCallMethodCallParams) -> SendAppTransactionResult[ABIReturn]: """Call an application's call method.""" return self._send_app_call(lambda c: c.add_app_call_method_call)(params) diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index 457898a6..c5d150b8 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -67,9 +67,9 @@ def hello_world_arc32_app_id( clear_state_program=hello_world_arc32_app_spec.clear_program, schema={ "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, - "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, - "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, }, ) ) @@ -108,9 +108,9 @@ def testing_app_arc32_app_id( approval_program=approval, clear_state_program=testing_app_arc32_app_spec.clear_program, schema={ - "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, - "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, }, ) @@ -178,9 +178,9 @@ def testing_app_puya_arc32_app_id( approval_program=testing_app_puya_arc32_app_spec.approval_program, clear_state_program=testing_app_puya_arc32_app_spec.clear_program, schema={ - "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, - "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, }, ) @@ -353,10 +353,9 @@ def test_construct_transaction_with_abi_encoding_including_transaction( result.confirmation, test_app_client.app_spec.get_arc56_method("call_abi_txn").to_abi_method() ) expected_return = f"Sent {amount.micro_algos}. test" - assert result.abi_return - assert result.abi_return.value == expected_return + assert result.abi_return == expected_return assert response - assert response.value == result.abi_return.value + assert response.value == result.abi_return def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( @@ -450,10 +449,9 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no test_app_client.app_spec.get_arc56_method("call_abi_foreign_refs").to_abi_method(), ) assert result.abi_return - assert result.abi_return.value - assert str(result.abi_return.value).startswith("App: 345, Asset: 567, Account: ") + assert str(result.abi_return).startswith("App: 345, Asset: 567, Account: ") assert expected_return - assert expected_return.value == result.abi_return.value + assert expected_return.value == result.abi_return def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: @@ -519,11 +517,11 @@ def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> assert sorted(b.name.name_base64 for b in box_values) == sorted([box_name1_base64, box_name2_base64]) box1 = next(b for b in box_values if b.name.name_base64 == box_name1_base64) - assert box1.value == base64.b64encode(bytes("value1", "utf-8")) + assert box1.value == b"value1" assert box1_value == box1.value box2 = next(b for b in box_values if b.name.name_base64 == box_name2_base64) - assert box2.value == base64.b64encode(bytes("value2", "utf-8")) + assert box2.value == b"value2" # Legacy contract strips ABI prefix; manually encoded ABI string after # passing algosdk's atc results in \x00\n\x00\n1234524352. @@ -661,8 +659,7 @@ def test_box_methods_with_arc4_returns_parametrized( ) # Encode the expected value using the specified ABI type - value_encoded = ABIType.from_string(value_type).encode(arg_value) - expected_value = base64.b64encode(value_encoded) + expected_value = ABIType.from_string(value_type).encode(arg_value) # Retrieve the actual box value actual_box_value = test_app_client_puya.get_box_value(box_reference) @@ -710,14 +707,12 @@ def test_abi_with_default_arg_method( AppClientMethodCallWithSendParams(method=method_signature, args=[defined_value]) ) - assert defined_value_result.abi_return - assert defined_value_result.abi_return.value == "Local state, defined value" + assert defined_value_result.abi_return == "Local state, defined value" # Test with default value default_value_result = app_client.send.call(AppClientMethodCallWithSendParams(method=method_signature, args=[None])) assert default_value_result - assert default_value_result.abi_return - assert default_value_result.abi_return.value == "Local state, banana" + assert default_value_result.abi_return == "Local state, banana" def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 9a17bb0f..80785c3d 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -7,6 +7,7 @@ from algokit_utils.applications.app_client import ( AppClient, + AppClientMethodCallCreateParams, AppClientMethodCallParams, AppClientMethodCallWithCompilationAndSendParams, AppClientMethodCallWithSendParams, @@ -164,7 +165,7 @@ def test_deploy_app_create_abi(factory: AppFactory) -> None: deploy_time_params={ "VALUE": 1, }, - create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), ) assert deploy_result.operation_performed == OperationPerformed.Create @@ -240,8 +241,7 @@ def test_deploy_app_update_abi(factory: AppFactory) -> None: assert ( update_deploy_result.update_response.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC ) - assert update_deploy_result.update_response.abi_return - assert update_deploy_result.update_response.abi_value == "args_io" + assert update_deploy_result.update_response.abi_return == "args_io" def test_deploy_app_replace(factory: AppFactory) -> None: @@ -295,7 +295,7 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: "VALUE": 2, }, on_update=OnUpdate.ReplaceApp, - create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), delete_params=AppClientMethodCallParams(method="delete_abi", args=["arg2_io"]), ) @@ -315,10 +315,8 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: assert ( replace_deploy_result.delete_response.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC ) - assert replace_deploy_result.create_response.abi_return - assert replace_deploy_result.create_response.abi_value == "arg_io" - assert replace_deploy_result.delete_response.abi_return - assert replace_deploy_result.delete_response.abi_value == "arg2_io" + assert replace_deploy_result.create_response.abi_return == "arg_io" + assert replace_deploy_result.delete_response.abi_return == "arg2_io" def test_create_then_call_app(factory: AppFactory) -> None: @@ -333,9 +331,7 @@ def test_create_then_call_app(factory: AppFactory) -> None: ) call = app_client.send.call(AppClientMethodCallWithSendParams(method="call_abi", args=["test"])) - - assert call.abi_return - assert call.abi_return.value == "Hello, test" + assert call.abi_return == "Hello, test" def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, factory: AppFactory) -> None: @@ -397,8 +393,7 @@ def test_update_app_with_abi(factory: AppFactory) -> None: ) ) - assert call_return.abi_return - assert call_return.abi_return.value == "string_io" + assert call_return.abi_return == "string_io" # assert call_return.compiled_approval is not None # TODO: centralize approval/clear compilation @@ -420,8 +415,7 @@ def test_delete_app_with_abi(factory: AppFactory) -> None: ) ) - assert call_return.abi_return - assert call_return.abi_return.value == "string_io" + assert call_return.abi_return == "string_io" def test_export_import_sourcemaps( @@ -471,7 +465,7 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( arc56_factory: AppFactory, ) -> None: app_client, _ = arc56_factory.deploy( - create_params=AppClientMethodCallParams(method="createApplication"), + create_params=AppClientMethodCallCreateParams(method="createApplication"), deploy_time_params={ "bytes64TmplVar": "0" * 64, "uint64TmplVar": 123, @@ -491,7 +485,7 @@ def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( ) -> None: # Deploy app with template parameters app_client, _ = arc56_factory.deploy( - create_params=AppClientMethodCallParams(method="createApplication"), + create_params=AppClientMethodCallCreateParams(method="createApplication"), deploy_time_params={ "bytes64TmplVar": "0" * 64, "uint64TmplVar": 0, diff --git a/tests/applications/test_arc56.py b/tests/applications/test_arc56.py index f5b6c2dc..e81f51f3 100644 --- a/tests/applications/test_arc56.py +++ b/tests/applications/test_arc56.py @@ -14,7 +14,7 @@ def test_arc56_from_arc32_json() -> None: assert arc56_app_spec - check_output_stability(arc56_app_spec.to_json()) + check_output_stability(arc56_app_spec.to_json(indent=4)) def test_arc56_from_arc32_instance() -> None: @@ -26,7 +26,7 @@ def test_arc56_from_arc32_instance() -> None: assert arc56_app_spec - check_output_stability(arc56_app_spec.to_json()) + check_output_stability(arc56_app_spec.to_json(indent=4)) def test_arc56_from_json() -> None: @@ -34,7 +34,7 @@ def test_arc56_from_json() -> None: assert arc56_app_spec - check_output_stability(arc56_app_spec.to_json()) + check_output_stability(arc56_app_spec.to_json(indent=4)) def test_arc56_from_dict() -> None: @@ -42,4 +42,4 @@ def test_arc56_from_dict() -> None: assert arc56_app_spec - check_output_stability(arc56_app_spec.to_json()) + check_output_stability(arc56_app_spec.to_json(indent=4)) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index f0ed1023..66e39e7d 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -173,7 +173,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No sender=funded_account.address, approval_program=approval_program, clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, ) composer.add_app_create(params) built = composer.build_transactions() @@ -199,7 +199,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco sender=funded_account.address, approval_program=approval_program, clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, ) ) response = composer.send() @@ -273,6 +273,39 @@ def test_arc2_note() -> None: assert encoded_note == expected_note +def test_arc2_note_dapp_name_validation() -> None: + invalid_names = [ + "_TestDApp", # starts with underscore + "Test", # too short + "a" * 33, # too long + "Test@App!", # invalid character ! + "Test App", # contains space + ] + + for invalid_name in invalid_names: + note_data: Arc2TransactionNote = {"dapp_name": invalid_name, "format": "j", "data": {"key": "value"}} + with pytest.raises(ValueError, match="dapp_name must be"): + TransactionComposer.arc2_note(note_data) + + +def test_arc2_note_valid_dapp_names() -> None: + valid_names = [ + "TestDApp", # simple case + "test-dapp", # with hyphen + "test_dapp", # with underscore + "test.dapp", # with dot + "test@dapp", # with @ + "test/dapp", # with / + "a" * 32, # maximum length + "12345", # minimum length, numeric + ] + + for valid_name in valid_names: + note_data: Arc2TransactionNote = {"dapp_name": valid_name, "format": "j", "data": {"key": "value"}} + encoded_note = TransactionComposer.arc2_note(note_data) + assert encoded_note.startswith(valid_name.encode()) + + def _get_test_transaction( default_account: Account, amount: AlgoAmount | None = None, sender: Account | None = None ) -> dict[str, Any]: diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index e034f2ce..a64a62d2 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -207,7 +207,7 @@ def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: sender=funded_account.address, approval_program=approval_program, clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, ) ) @@ -227,7 +227,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde sender=funded_account.address, approval_program=approval_program, clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, ) ) app_id = algorand.client.algod.pending_transaction_info(create_result.tx_ids[0])["application-index"] # type: ignore[call-overload] diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 7f44f641..6c533f03 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -86,9 +86,9 @@ def test_hello_world_arc32_app_id( clear_state_program=test_hello_world_arc32_app_spec.clear_program, schema={ "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, - "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, - "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, }, ) ) @@ -384,7 +384,7 @@ def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: sender=sender.address, approval_program=approval_program, clear_state_program=clear_state_program, - schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, ) result = transaction_sender.app_create(params) From a0efebcfc132569415b6baf2439d52d5df368819 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 22 Jan 2025 13:53:38 +0100 Subject: [PATCH 12/31] refactor: remove AlgorandClientProtocol --- src/algokit_utils/__init__.py | 2 - src/algokit_utils/applications/app_client.py | 57 ++------- src/algokit_utils/applications/app_factory.py | 44 +------ src/algokit_utils/clients/client_manager.py | 18 +-- src/algokit_utils/protocols/__init__.py | 2 +- src/algokit_utils/protocols/client.py | 62 ---------- src/algokit_utils/protocols/typed_clients.py | 110 ++++++++++++++++++ 7 files changed, 134 insertions(+), 161 deletions(-) delete mode 100644 src/algokit_utils/protocols/client.py create mode 100644 src/algokit_utils/protocols/typed_clients.py diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 9eeebbee..5aacda4a 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -14,7 +14,6 @@ from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.errors.logic_error import LogicError from algokit_utils.clients.algorand_client import AlgorandClient -from algokit_utils.protocols import AlgorandClientProtocol # Common managers/clients that are frequently used entry points from algokit_utils.accounts.account_manager import AccountManager @@ -139,7 +138,6 @@ "AlgorandClient", "DELETABLE_TEMPLATE_NAME", "UPDATABLE_TEMPLATE_NAME", - "AlgorandClientProtocol", # Common managers/clients "AccountManager", "AppClient", diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index de0904fb..0da95420 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -6,12 +6,11 @@ import os from collections.abc import Sequence from dataclasses import dataclass, fields -from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar import algosdk from algosdk.source_map import SourceMap from algosdk.transaction import OnComplete, Transaction -from typing_extensions import Self from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps from algokit_utils.applications.abi import ( @@ -68,9 +67,9 @@ from algokit_utils.applications.app_deployer import AppLookup from algokit_utils.applications.app_manager import AppManager + from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams - from algokit_utils.protocols.client import AlgorandClientProtocol __all__ = [ "AppClient", @@ -94,7 +93,6 @@ "BaseAppClientMethodCallParams", "BaseOnCompleteParams", "FundAppAccountParams", - "TypedAppClientProtocol", ] # TEAL opcodes for constant blocks @@ -166,45 +164,6 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 return max(bytecblock_offset or 0, intcblock_offset or 0) -class TypedAppClientProtocol(Protocol): - @classmethod - def from_creator_and_name( - cls, - *, - creator_address: str, - app_name: str, - default_sender: str | None = None, - default_signer: TransactionSigner | None = None, - ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, - algorand: AlgorandClientProtocol, - ) -> Self: ... - - @classmethod - def from_network( - cls, - *, - app_name: str | None = None, - default_sender: str | None = None, - default_signer: TransactionSigner | None = None, - approval_source_map: SourceMap | None = None, - clear_source_map: SourceMap | None = None, - algorand: AlgorandClientProtocol, - ) -> Self: ... - - def __init__( - self, - *, - app_id: int, - app_name: str | None = None, - default_sender: str | None = None, - default_signer: TransactionSigner | None = None, - algorand: AlgorandClientProtocol, - approval_source_map: SourceMap | None = None, - clear_source_map: SourceMap | None = None, - ) -> None: ... - - @dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: approval_program: bytes @@ -1006,11 +965,11 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR class AppClientParams: """Full parameters for creating an app client""" - app_spec: Arc56Contract | Arc32Contract | str # Using string quotes since these types may be defined elsewhere - algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere + app_spec: Arc56Contract | Arc32Contract | str + algorand: AlgorandClient app_id: int app_name: str | None = None - default_sender: str | bytes | None = None # Address can be string or bytes + default_sender: str | bytes | None = None default_signer: TransactionSigner | None = None approval_source_map: SourceMap | None = None clear_source_map: SourceMap | None = None @@ -1033,7 +992,7 @@ def __init__(self, params: AppClientParams) -> None: self._create_transaction_accessor = _AppClientMethodCallTransactionCreator(self) @property - def algorand(self) -> AlgorandClientProtocol: + def algorand(self) -> AlgorandClient: return self._algorand @property @@ -1089,7 +1048,7 @@ def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Co @staticmethod def from_network( app_spec: Arc56Contract | Arc32Contract | str, - algorand: AlgorandClientProtocol, + algorand: AlgorandClient, app_name: str | None = None, default_sender: str | bytes | None = None, default_signer: TransactionSigner | None = None, @@ -1133,7 +1092,7 @@ def from_creator_and_name( creator_address: str, app_name: str, app_spec: Arc56Contract | Arc32Contract | str, - algorand: AlgorandClientProtocol, + algorand: AlgorandClient, default_sender: str | bytes | None = None, default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 6e9498be..bc3ae7e6 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -2,7 +2,7 @@ import dataclasses from collections.abc import Callable, Sequence from dataclasses import asdict, dataclass, replace -from typing import Any, Generic, Literal, Protocol, TypeVar +from typing import Any, Generic, Literal, TypeVar from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.source_map import SourceMap @@ -26,7 +26,6 @@ AppClientMethodCallCreateParams, AppClientMethodCallParams, AppClientParams, - TypedAppClientProtocol, ) from algokit_utils.applications.app_deployer import ( AppDeployMetaData, @@ -40,12 +39,12 @@ ) from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Method +from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.application import ( AppSourceMaps, ) from algokit_utils.models.state import TealTemplateParams from algokit_utils.models.transaction import SendParams -from algokit_utils.protocols.client import AlgorandClientProtocol from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCallParams, AppCreateParams, @@ -76,13 +75,12 @@ "SendAppCreateFactoryTransactionResult", "SendAppFactoryTransactionResult", "SendAppUpdateFactoryTransactionResult", - "TypedAppFactoryProtocol", ] @dataclass(kw_only=True, frozen=True) class AppFactoryParams: - algorand: AlgorandClientProtocol + algorand: AlgorandClient app_spec: Arc56Contract | ApplicationSpecification | str app_name: str | None = None default_sender: str | bytes | None = None @@ -506,40 +504,6 @@ def create( ) -CreateParamsT = TypeVar( # noqa: PLC0105 - "CreateParamsT", bound=AppClientMethodCallCreateParams | AppClientBareCallCreateParams, contravariant=True -) -UpdateParamsT = TypeVar("UpdateParamsT", bound=AppClientMethodCallParams | AppClientBareCallParams, contravariant=True) # noqa: PLC0105 -DeleteParamsT = TypeVar("DeleteParamsT", bound=AppClientMethodCallParams | AppClientBareCallParams, contravariant=True) # noqa: PLC0105 - - -class TypedAppFactoryProtocol(Protocol, Generic[CreateParamsT, UpdateParamsT, DeleteParamsT]): - def __init__( - self, - algorand: AlgorandClientProtocol, - **kwargs: Any, - ) -> None: ... - - def deploy( # noqa: PLR0913 - self, - *, - deploy_time_params: TealTemplateParams | None = None, - on_update: OnUpdate = OnUpdate.Fail, - on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, - create_params: CreateParamsT | None = None, - update_params: UpdateParamsT | None = None, - delete_params: DeleteParamsT | None = None, - existing_deployments: AppLookup | None = None, - ignore_cache: bool = False, - updatable: bool | None = None, - deletable: bool | None = None, - app_name: str | None = None, - max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool = False, - ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... - - class AppFactory: def __init__(self, params: AppFactoryParams) -> None: self._app_spec = AppClient.normalise_app_spec(params.app_spec) @@ -566,7 +530,7 @@ def app_spec(self) -> Arc56Contract: return self._app_spec @property - def algorand(self) -> AlgorandClientProtocol: + def algorand(self) -> AlgorandClient: return self._algorand @property diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index aec3d27e..641cd594 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,6 +1,6 @@ import os from dataclasses import dataclass -from typing import Literal, TypeVar +from typing import TYPE_CHECKING, Literal, TypeVar from urllib import parse import algosdk @@ -10,16 +10,18 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -# from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils.applications.app_client import AppClient, AppClientParams, TypedAppClientProtocol +from algokit_utils.applications.app_client import AppClient, AppClientParams from algokit_utils.applications.app_deployer import AppLookup -from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams, TypedAppFactoryProtocol from algokit_utils.applications.app_spec.arc56 import Arc56Contract from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs from algokit_utils.models.state import TealTemplateParams -from algokit_utils.protocols.client import AlgorandClientProtocol +from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol + +if TYPE_CHECKING: + from algokit_utils.applications.app_factory import AppFactory + from algokit_utils.clients.algorand_client import AlgorandClient __all__ = [ "AlgoSdkClients", @@ -64,7 +66,7 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: class ClientManager: - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClientProtocol): + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: "AlgorandClient"): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs elif isinstance(clients_or_configs, AlgoClientConfigs): @@ -142,7 +144,9 @@ def get_app_factory( updatable: bool | None = None, deletable: bool | None = None, deploy_time_params: TealTemplateParams | None = None, - ) -> AppFactory: + ) -> "AppFactory": + from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams + if not self._algorand: raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py index ca59d88f..949dc4a3 100644 --- a/src/algokit_utils/protocols/__init__.py +++ b/src/algokit_utils/protocols/__init__.py @@ -1 +1 @@ -from algokit_utils.protocols.client import * # noqa: F403 +from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol # noqa: F401 diff --git a/src/algokit_utils/protocols/client.py b/src/algokit_utils/protocols/client.py deleted file mode 100644 index e7199427..00000000 --- a/src/algokit_utils/protocols/client.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Protocol, runtime_checkable - -import typing_extensions - -if TYPE_CHECKING: - from algosdk.atomic_transaction_composer import TransactionSigner - from algosdk.transaction import SuggestedParams - - from algokit_utils.accounts import AccountManager - from algokit_utils.applications.app_deployer import AppDeployer - from algokit_utils.applications.app_manager import AppManager - from algokit_utils.assets.asset_manager import AssetManager - from algokit_utils.clients.client_manager import ClientManager - from algokit_utils.transactions.transaction_composer import TransactionComposer - from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator - from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender - -__all__ = [ - "AlgorandClientProtocol", -] - - -@runtime_checkable -class AlgorandClientProtocol(Protocol): - @property - def app(self) -> AppManager: ... - - @property - def app_deployer(self) -> AppDeployer: ... - - @property - def send(self) -> AlgorandClientTransactionSender: ... - - @property - def create_transaction(self) -> AlgorandClientTransactionCreator: ... - - @property - def client(self) -> ClientManager: ... - - @property - def account(self) -> AccountManager: ... - - @property - def asset(self) -> AssetManager: ... - - def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self: ... - - def set_default_signer(self, signer: TransactionSigner) -> typing_extensions.Self: ... - - def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extensions.Self: ... - - def set_suggested_params( - self, suggested_params: SuggestedParams, until: float | None = None - ) -> typing_extensions.Self: ... - - def set_suggested_params_timeout(self, timeout: int) -> typing_extensions.Self: ... - - def get_suggested_params(self) -> SuggestedParams: ... - - def new_group(self) -> TransactionComposer: ... diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py new file mode 100644 index 00000000..747708bc --- /dev/null +++ b/src/algokit_utils/protocols/typed_clients.py @@ -0,0 +1,110 @@ +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar + +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.source_map import SourceMap +from typing_extensions import Self + +from algokit_utils.applications.app_client import ( + AppClientBareCallCreateParams, + AppClientBareCallParams, + AppClientMethodCallCreateParams, + AppClientMethodCallParams, +) +from algokit_utils.applications.app_deployer import ( + AppLookup, + OnSchemaBreak, + OnUpdate, +) +from algokit_utils.models.state import TealTemplateParams + +if TYPE_CHECKING: + from algokit_utils.applications.app_factory import AppFactoryDeployResponse + from algokit_utils.clients.algorand_client import AlgorandClient + +__all__ = [ + "TypedAppClientProtocol", + "TypedAppFactoryProtocol", +] + + +class TypedAppClientProtocol(Protocol): + @classmethod + def from_creator_and_name( + cls, + *, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: AppLookup | None = None, + algorand: "AlgorandClient", + ) -> Self: ... + + @classmethod + def from_network( + cls, + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + algorand: "AlgorandClient", + ) -> Self: ... + + def __init__( + self, + *, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + algorand: "AlgorandClient", + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> None: ... + + +CreateParamsT = TypeVar( # noqa: PLC0105 + "CreateParamsT", + bound=AppClientMethodCallCreateParams | AppClientBareCallCreateParams, + contravariant=True, +) +UpdateParamsT = TypeVar( # noqa: PLC0105 + "UpdateParamsT", + bound=AppClientMethodCallParams | AppClientBareCallParams, + contravariant=True, +) +DeleteParamsT = TypeVar( # noqa: PLC0105 + "DeleteParamsT", + bound=AppClientMethodCallParams | AppClientBareCallParams, + contravariant=True, +) + + +class TypedAppFactoryProtocol(Protocol, Generic[CreateParamsT, UpdateParamsT, DeleteParamsT]): + def __init__( + self, + algorand: "AlgorandClient", + **kwargs: Any, + ) -> None: ... + + def deploy( # noqa: PLR0913 + self, + *, + deploy_time_params: TealTemplateParams | None = None, + on_update: OnUpdate = OnUpdate.Fail, + on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + create_params: CreateParamsT | None = None, + update_params: UpdateParamsT | None = None, + delete_params: DeleteParamsT | None = None, + existing_deployments: AppLookup | None = None, + ignore_cache: bool = False, + updatable: bool | None = None, + deletable: bool | None = None, + app_name: str | None = None, + max_rounds_to_wait: int | None = None, + suppress_log: bool = False, + populate_app_call_resources: bool = False, + ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... From 06a59a9c09a64256ab29af470855067e66b53127 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 22 Jan 2025 23:09:55 +0100 Subject: [PATCH 13/31] refactor: removing AlgorandClientProtocol; extra type narrowing tweaks --- src/algokit_utils/applications/app_client.py | 22 ++++++-------------- src/algokit_utils/protocols/__init__.py | 2 +- src/algokit_utils/protocols/typed_clients.py | 9 ++++---- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 0da95420..9e72b089 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -91,7 +91,6 @@ "AppClientParams", "AppSourceMaps", "BaseAppClientMethodCallParams", - "BaseOnCompleteParams", "FundAppAccountParams", ] @@ -216,11 +215,10 @@ class AppClientCallParams: ArgsT = TypeVar("ArgsT") MethodT = TypeVar("MethodT") -OnCompleteT = TypeVar("OnCompleteT") @dataclass(kw_only=True, frozen=True) -class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT, OnCompleteT]): +class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]): method: MethodT args: ArgsT | None = None account_references: list[str] | None = None @@ -238,13 +236,14 @@ class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT, OnCompleteT]): static_fee: AlgoAmount | None = None validity_window: int | None = None last_valid_round: int | None = None - on_complete: OnCompleteT | None = None + on_complete: algosdk.transaction.OnComplete | None = None @dataclass(kw_only=True, frozen=True) class AppClientMethodCallParams( BaseAppClientMethodCallParams[ - Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None], str, algosdk.transaction.OnComplete + Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None], + str, ] ): pass @@ -309,17 +308,8 @@ class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, App @dataclass(kw_only=True, frozen=True) -class BaseOnCompleteParams(Generic[OnCompleteT]): - """Combined parameters for bare calls with an OnComplete value""" - - on_complete: OnCompleteT | None = None - - -@dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCallOnCompleteParams( - AppClientBareCallParams, BaseOnCompleteParams[algosdk.transaction.OnComplete] -): - """Combined parameters for bare calls with an OnComplete value""" +class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams): + on_complete: algosdk.transaction.OnComplete | None = None @dataclass(frozen=True) diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py index 949dc4a3..faca1df0 100644 --- a/src/algokit_utils/protocols/__init__.py +++ b/src/algokit_utils/protocols/__init__.py @@ -1 +1 @@ -from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol # noqa: F401 +from algokit_utils.protocols.typed_clients import * # noqa: F403 diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index 747708bc..b9a65d52 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -7,8 +7,7 @@ from algokit_utils.applications.app_client import ( AppClientBareCallCreateParams, AppClientBareCallParams, - AppClientMethodCallCreateParams, - AppClientMethodCallParams, + BaseAppClientMethodCallParams, ) from algokit_utils.applications.app_deployer import ( AppLookup, @@ -68,17 +67,17 @@ def __init__( CreateParamsT = TypeVar( # noqa: PLC0105 "CreateParamsT", - bound=AppClientMethodCallCreateParams | AppClientBareCallCreateParams, + bound=BaseAppClientMethodCallParams | AppClientBareCallCreateParams | None, contravariant=True, ) UpdateParamsT = TypeVar( # noqa: PLC0105 "UpdateParamsT", - bound=AppClientMethodCallParams | AppClientBareCallParams, + bound=BaseAppClientMethodCallParams | AppClientBareCallParams | None, contravariant=True, ) DeleteParamsT = TypeVar( # noqa: PLC0105 "DeleteParamsT", - bound=AppClientMethodCallParams | AppClientBareCallParams, + bound=BaseAppClientMethodCallParams | AppClientBareCallParams | None, contravariant=True, ) From dd75634d0f545acbfc9035f2ebfbe5ae6957fe37 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Thu, 23 Jan 2025 12:20:54 +0100 Subject: [PATCH 14/31] refactor: addressing pr comments --- pyproject.toml | 1 - src/algokit_utils/__init__.py | 2 +- src/algokit_utils/_debugging.py | 15 +--- .../_legacy_v2/application_client.py | 2 +- src/algokit_utils/_legacy_v2/asset.py | 4 +- src/algokit_utils/_legacy_v2/deploy.py | 4 +- .../_legacy_v2/network_clients.py | 8 +-- .../algorand_client.py => algorand.py} | 0 src/algokit_utils/applications/app_client.py | 69 ++++++++++--------- src/algokit_utils/applications/app_factory.py | 26 +++---- src/algokit_utils/clients/__init__.py | 1 - src/algokit_utils/clients/client_manager.py | 8 ++- src/algokit_utils/models/amount.py | 50 ++++++-------- src/algokit_utils/protocols/typed_clients.py | 2 +- .../transactions/transaction_composer.py | 6 +- tests/accounts/test_account_manager.py | 2 +- tests/applications/test_app_client.py | 6 +- tests/applications/test_app_factory.py | 2 +- tests/applications/test_app_manager.py | 2 +- tests/assets/test_asset_manager.py | 2 +- .../clients/algorand_client/test_transfer.py | 5 +- tests/conftest.py | 2 +- tests/test_debug_utils.py | 2 +- tests/transactions/test_resource_packing.py | 2 +- .../transactions/test_transaction_composer.py | 2 +- .../transactions/test_transaction_creator.py | 2 +- tests/transactions/test_transaction_sender.py | 2 +- 27 files changed, 114 insertions(+), 115 deletions(-) rename src/algokit_utils/{clients/algorand_client.py => algorand.py} (100%) diff --git a/pyproject.toml b/pyproject.toml index 5d7106cc..80b45a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ allow-star-arg-any = true suppress-none-returning = true [tool.ruff.lint.per-file-ignores] -"src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"] "src/algokit_utils/applications/app_client.py" = ["SLF001"] "src/algokit_utils/applications/app_factory.py" = ["SLF001"] "tests/clients/test_algorand_client.py" = ["ERA001"] diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 5aacda4a..0bbf868d 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -13,7 +13,7 @@ from algokit_utils.models.account import Account from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.errors.logic_error import LogicError -from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.algorand import AlgorandClient # Common managers/clients that are frequently used entry points from algokit_utils.accounts.account_manager import AccountManager diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index c59cc139..e50031d8 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -230,18 +230,9 @@ def simulate_response( extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, simulation_round: int | None = None, - skip_signatures: int | None = None, # noqa: ARG001 TODO: revisit + skip_signatures: bool | None = None, # noqa: ARG001 TODO: revisit ) -> SimulateAtomicTransactionResponse: - """ - Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. - - Args: - atc (AtomicTransactionComposer): An AtomicTransactionComposer object. - algod_client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. - - Returns: - SimulateAtomicTransactionResponse: The simulated response. - """ + """Simulate atomic transaction group execution""" unsigned_txn_groups = atc.build_group() empty_signer = EmptySigner() @@ -274,7 +265,7 @@ def simulate_and_persist_response( # noqa: PLR0913 extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, simulation_round: int | None = None, - skip_signatures: int | None = None, + skip_signatures: bool | None = None, ) -> SimulateAtomicTransactionResponse: """ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index 0334c832..ca635c26 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -87,7 +87,7 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: @deprecated( "Use AppClient from algokit_utils.applications instead. Example:\n" "```python\n" - "from algokit_utils.clients import AlgorandClient\n" + "from algokit_utils import AlgorandClient\n" "from algokit_utils.models.application import Arc56Contract\n" "algorand_client = AlgorandClient.from_environment()\n" "app_client = AppClient.from_network(app_spec=Arc56Contract.from_json(app_spec_json), " diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 409523c9..85c39f5a 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -70,7 +70,7 @@ def _ensure_asset_balance_conditions( @deprecated( - "Use TransactionComposer.add_asset_opt_in() or AlgorandClient.asset.opt_in() instead. " + "Use TransactionComposer.add_asset_opt_in() or AlgorandClient.asset.bulk_opt_in() instead. " "Example: composer.add_asset_opt_in(AssetOptInParams(sender=account.address, asset_id=123))" ) def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: @@ -122,7 +122,7 @@ def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) @deprecated( - "Use TransactionComposer.add_asset_opt_out() or AlgorandClient.asset.opt_out() instead. " + "Use TransactionComposer.add_asset_opt_out() or AlgorandClient.asset.bulk_opt_out() instead. " "Example: composer.add_asset_opt_out(AssetOptOutParams(sender=account.address, asset_id=123, creator=creator_address))" ) def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index 6a73ba45..3ec196f1 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -175,7 +175,7 @@ def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: return None -@deprecated("Deprecated") +@deprecated("Use algorand.appDeployer.get_creator_apps_by_name() instead. ") def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified creator that have a transaction note containing {py:class}`AppDeployMetaData` @@ -256,7 +256,7 @@ class AppChanges: schema_change_description: str | None -@deprecated("Deprecated") +@deprecated("The algokit_utils.AppDeployer now handles checking for app changes implicitly as part of `deploy` method") def check_for_app_changes( algod_client: "AlgodClient", *, diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index f3de3a11..9a5bc9aa 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -48,7 +48,7 @@ def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> A return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) -@deprecated("Use AlgorandClient.client.testnet() or AlgorandClient.mainnet() instead") +@deprecated("Use AlgorandClient.testnet() or AlgorandClient.mainnet() instead") def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str ) -> AlgoClientConfig: @@ -59,7 +59,7 @@ def get_algonode_config( ) -@deprecated("Use AlgorandClient.client.from_environment() instead. Example: client = AlgorandClient.from_environment()") +@deprecated("Use AlgorandClient.from_environment() instead.") def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment @@ -69,7 +69,7 @@ def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: return AlgodClient(config.token, config.server, headers) -@deprecated("Use AlgorandClient.client.default_localnet().kmd instead") +@deprecated("Use AlgorandClient.default_localnet().kmd instead") def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment @@ -109,7 +109,7 @@ def is_testnet(client: AlgodClient) -> bool: return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] -@deprecated("Use AlgorandClient.client.default_localnet().kmd instead") +@deprecated("Use AlgorandClient.default_localnet().kmd instead") def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/algorand.py similarity index 100% rename from src/algokit_utils/clients/algorand_client.py rename to src/algokit_utils/algorand.py diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 9e72b089..9144aaea 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -65,9 +65,9 @@ from algosdk.atomic_transaction_composer import TransactionSigner + from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_deployer import AppLookup from algokit_utils.applications.app_manager import AppManager - from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams @@ -100,6 +100,9 @@ T = TypeVar("T") # For generic return type in _handle_call_errors +# Sentinel to detect missing arguments in clone() method of AppClient +_MISSING = object() + def get_constant_block_offset(program: bytes) -> int: # noqa: C901 """Calculate the offset after constant blocks in TEAL program. @@ -376,7 +379,7 @@ def get_map(self, map_name: str) -> dict[str, ABIValue]: return self._get_map(map_name) -class _AppClientStateAccessor: +class _StateAccessor: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand @@ -555,7 +558,7 @@ def get_global_state(self) -> dict[str, AppState]: return self._algorand.app.get_global_state(self._app_id) -class _AppClientBareParamsAccessor: +class _BareParamsBuilder: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand @@ -621,16 +624,16 @@ def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) return call_params -class _AppClientMethodCallParamsAccessor: +class _MethodParamsBuilder: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand self._app_id = client._app_id self._app_spec = client._app_spec - self._bare_params_accessor = _AppClientBareParamsAccessor(client) + self._bare_params_accessor = _BareParamsBuilder(client) @property - def bare(self) -> _AppClientBareParamsAccessor: + def bare(self) -> _BareParamsBuilder: return self._bare_params_accessor def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: @@ -701,7 +704,6 @@ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transacti input_params["app_id"] = self._app_id input_params["on_complete"] = on_complete - input_params["sender"] = self._client._get_sender(params["sender"]) input_params["signer"] = self._client._get_signer(params["sender"], params["signer"]) @@ -716,7 +718,7 @@ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transacti return input_params -class _AppClientBareCreateTransactionMethods: +class _AppClientBareCallCreateTransactionMethods: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand @@ -752,16 +754,16 @@ def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) ) -class _AppClientMethodCallTransactionCreator: +class _TransactionCreator: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand self._app_id = client._app_id self._app_spec = client._app_spec - self._bare_create_transaction_methods = _AppClientBareCreateTransactionMethods(client) + self._bare_create_transaction_methods = _AppClientBareCallCreateTransactionMethods(client) @property - def bare(self) -> _AppClientBareCreateTransactionMethods: + def bare(self) -> _AppClientBareCallCreateTransactionMethods: return self._bare_create_transaction_methods def fund_app_account(self, params: FundAppAccountParams) -> Transaction: @@ -807,7 +809,7 @@ def update( The result of sending the transaction """ params = params or AppClientBareCallWithCompilationAndSendParams() - compiled = self._client.compile_sourcemaps(params.deploy_time_params, params.updatable, params.deletable) + compiled = self._client.compile_app(params.deploy_time_params, params.updatable, params.deletable) bare_params = self._client.params.bare.update(params) bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) @@ -855,7 +857,7 @@ def call( ) -class _AppClientSendAccessor: +class _TransactionSender: def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand @@ -976,10 +978,10 @@ def __init__(self, params: AppClientParams) -> None: self._default_signer = params.default_signer self._approval_source_map = params.approval_source_map self._clear_source_map = params.clear_source_map - self._state_accessor = _AppClientStateAccessor(self) - self._params_accessor = _AppClientMethodCallParamsAccessor(self) - self._send_accessor = _AppClientSendAccessor(self) - self._create_transaction_accessor = _AppClientMethodCallTransactionCreator(self) + self._state_accessor = _StateAccessor(self) + self._params_accessor = _MethodParamsBuilder(self) + self._send_accessor = _TransactionSender(self) + self._create_transaction_accessor = _TransactionCreator(self) @property def algorand(self) -> AlgorandClient: @@ -1002,19 +1004,19 @@ def app_spec(self) -> Arc56Contract: return self._app_spec @property - def state(self) -> _AppClientStateAccessor: + def state(self) -> _StateAccessor: return self._state_accessor @property - def params(self) -> _AppClientMethodCallParamsAccessor: + def params(self) -> _MethodParamsBuilder: return self._params_accessor @property - def send(self) -> _AppClientSendAccessor: + def send(self) -> _TransactionSender: return self._send_accessor @property - def create_transaction(self) -> _AppClientMethodCallTransactionCreator: + def create_transaction(self) -> _TransactionCreator: return self._create_transaction_accessor @staticmethod @@ -1264,7 +1266,7 @@ def get_line_for_pc(input_pc: int) -> int | None: return e # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' - def compile_sourcemaps( + def compile_app( self, deploy_time_params: TealTemplateParams | None = None, updatable: bool | None = None, @@ -1281,22 +1283,25 @@ def compile_sourcemaps( def clone( self, - app_name: str | None = None, - default_sender: str | bytes | None = None, - default_signer: TransactionSigner | None = None, - approval_source_map: SourceMap | None = None, - clear_source_map: SourceMap | None = None, + app_name: str | None = _MISSING, # type: ignore[assignment] + default_sender: str | bytes | None = _MISSING, # type: ignore[assignment] + default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment] + approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] + clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] ) -> AppClient: + """Create a cloned AppClient instance with optionally overridden parameters.""" return AppClient( AppClientParams( app_id=self._app_id, algorand=self._algorand, app_spec=self._app_spec, - app_name=app_name or self._app_name, - default_sender=default_sender or self._default_sender, - default_signer=default_signer or self._default_signer, - approval_source_map=approval_source_map or self._approval_source_map, - clear_source_map=clear_source_map or self._clear_source_map, + app_name=self._app_name if app_name is _MISSING else app_name, + default_sender=self._default_sender if default_sender is _MISSING else default_sender, + default_signer=self._default_signer if default_signer is _MISSING else default_signer, + approval_source_map=( + self._approval_source_map if approval_source_map is _MISSING else approval_source_map + ), + clear_source_map=(self._clear_source_map if clear_source_map is _MISSING else clear_source_map), ) ) diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index bc3ae7e6..4acb4075 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -10,6 +10,7 @@ from typing_extensions import Self from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.abi import ( ABIReturn, Arc56ReturnValueType, @@ -39,7 +40,6 @@ ) from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Method -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.application import ( AppSourceMaps, ) @@ -220,7 +220,7 @@ def to_factory_response( ) -class _AppFactoryBareParamsAccessor: +class _BareParamsBuilder: def __init__(self, factory: "AppFactory") -> None: self._factory = factory self._algorand = factory._algorand @@ -289,13 +289,13 @@ def deploy_delete(self, params: AppClientBareCallParams | None = None) -> AppDel ) -class _AppFactoryParamsAccessor: +class _MethodParamsBuilder: def __init__(self, factory: "AppFactory") -> None: self._factory = factory - self._bare = _AppFactoryBareParamsAccessor(factory) + self._bare = _BareParamsBuilder(factory) @property - def bare(self) -> _AppFactoryBareParamsAccessor: + def bare(self) -> _BareParamsBuilder: return self._bare def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCallParams: @@ -377,7 +377,7 @@ def create(self, params: AppFactoryCreateParams | None = None) -> Transaction: return self._factory._algorand.create_transaction.app_create(self._factory.params.bare.create(params)) -class _AppFactoryCreateTransactionAccessor: +class _TransactionCreator: def __init__(self, factory: "AppFactory") -> None: self._factory = factory self._bare = _AppFactoryBareCreateTransactionAccessor(factory) @@ -443,7 +443,7 @@ def create( ) -class _AppFactorySendAccessor: +class _TransactionSender: def __init__(self, factory: "AppFactory") -> None: self._factory = factory self._algorand = factory._algorand @@ -517,9 +517,9 @@ def __init__(self, params: AppFactoryParams) -> None: self._deletable = params.deletable self._approval_source_map: SourceMap | None = None self._clear_source_map: SourceMap | None = None - self._params_accessor = _AppFactoryParamsAccessor(self) - self._send_accessor = _AppFactorySendAccessor(self) - self._create_transaction_accessor = _AppFactoryCreateTransactionAccessor(self) + self._params_accessor = _MethodParamsBuilder(self) + self._send_accessor = _TransactionSender(self) + self._create_transaction_accessor = _TransactionCreator(self) @property def app_name(self) -> str: @@ -534,15 +534,15 @@ def algorand(self) -> AlgorandClient: return self._algorand @property - def params(self) -> _AppFactoryParamsAccessor: + def params(self) -> _MethodParamsBuilder: return self._params_accessor @property - def send(self) -> _AppFactorySendAccessor: + def send(self) -> _TransactionSender: return self._send_accessor @property - def create_transaction(self) -> _AppFactoryCreateTransactionAccessor: + def create_transaction(self) -> _TransactionCreator: return self._create_transaction_accessor def deploy( # noqa: PLR0913 diff --git a/src/algokit_utils/clients/__init__.py b/src/algokit_utils/clients/__init__.py index 2224016e..1a48b824 100644 --- a/src/algokit_utils/clients/__init__.py +++ b/src/algokit_utils/clients/__init__.py @@ -1,3 +1,2 @@ -from algokit_utils.clients.algorand_client import * # noqa: F403 from algokit_utils.clients.client_manager import * # noqa: F403 from algokit_utils.clients.dispenser_api_client import * # noqa: F403 diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 641cd594..4e43aa86 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -7,6 +7,7 @@ from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.kmd import KMDClient from algosdk.source_map import SourceMap +from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient @@ -20,8 +21,8 @@ from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol if TYPE_CHECKING: + from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_factory import AppFactory - from algokit_utils.clients.algorand_client import AlgorandClient __all__ = [ "AlgoSdkClients", @@ -83,6 +84,7 @@ def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algor self._indexer = _clients.indexer self._kmd = _clients.kmd self._algorand = algorand_client + self._suggested_params: SuggestedParams | None = None @property def algod(self) -> AlgodClient: @@ -108,7 +110,9 @@ def kmd(self) -> KMDClient: return self._kmd def network(self) -> NetworkDetail: - sp = self._algod.suggested_params() # TODO: cache it + if self._suggested_params is None: + self._suggested_params = self._algod.suggested_params() + sp = self._suggested_params return NetworkDetail( is_testnet=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], is_mainnet=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index f364a25a..94fc26d7 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -1,7 +1,5 @@ from __future__ import annotations -from decimal import Decimal - import algosdk from typing_extensions import Self @@ -11,7 +9,7 @@ class AlgoAmount: """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers.""" - def __init__(self, amount: dict[str, int | Decimal]): + def __init__(self, amount: dict[str, int]): """Create a new AlgoAmount instance. :param amount: A dictionary containing either algos, algo, microAlgos, or microAlgo as key @@ -50,7 +48,7 @@ def micro_algo(self) -> int: return self.amount_in_micro_algo @property - def algos(self) -> int | Decimal: + def algos(self) -> int: """Return the amount as a number in Algo. :returns: The amount in Algo. @@ -58,7 +56,7 @@ def algos(self) -> int | Decimal: return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @property - def algo(self) -> int | Decimal: + def algo(self) -> int: """Return the amount as a number in Algo. :returns: The amount in Algo. @@ -66,7 +64,7 @@ def algo(self) -> int | Decimal: return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @staticmethod - def from_algos(amount: int | Decimal) -> AlgoAmount: + def from_algos(amount: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. :param amount: The amount in Algo. @@ -78,7 +76,7 @@ def from_algos(amount: int | Decimal) -> AlgoAmount: return AlgoAmount({"algos": amount}) @staticmethod - def from_algo(amount: int | Decimal) -> AlgoAmount: + def from_algo(amount: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. :param amount: The amount in Algo. @@ -90,7 +88,7 @@ def from_algo(amount: int | Decimal) -> AlgoAmount: return AlgoAmount({"algo": amount}) @staticmethod - def from_micro_algos(amount: int | Decimal) -> AlgoAmount: + def from_micro_algos(amount: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of µAlgo. :param amount: The amount in µAlgo. @@ -102,7 +100,7 @@ def from_micro_algos(amount: int | Decimal) -> AlgoAmount: return AlgoAmount({"microAlgos": amount}) @staticmethod - def from_micro_algo(amount: int | Decimal) -> AlgoAmount: + def from_micro_algo(amount: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of µAlgo. :param amount: The amount in µAlgo. @@ -119,23 +117,19 @@ def __str__(self) -> str: def __int__(self) -> int: return self.micro_algos - def __add__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: + def __add__(self, other: AlgoAmount) -> AlgoAmount: if isinstance(other, AlgoAmount): total_micro_algos = self.micro_algos + other.micro_algos - elif isinstance(other, (int | Decimal)): - total_micro_algos = self.micro_algos + int(other) else: raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") return AlgoAmount.from_micro_algos(total_micro_algos) - def __radd__(self, other: int | Decimal) -> AlgoAmount: + def __radd__(self, other: AlgoAmount) -> AlgoAmount: return self.__add__(other) - def __iadd__(self, other: int | Decimal | AlgoAmount) -> Self: + def __iadd__(self, other: AlgoAmount) -> Self: if isinstance(other, AlgoAmount): self.amount_in_micro_algo += other.micro_algos - elif isinstance(other, (int | Decimal)): - self.amount_in_micro_algo += int(other) else: raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") return self @@ -143,65 +137,61 @@ def __iadd__(self, other: int | Decimal | AlgoAmount) -> Self: def __eq__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo == other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo == int(other) raise TypeError(f"Unsupported operand type(s) for ==: 'AlgoAmount' and '{type(other).__name__}'") def __ne__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo != other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo != int(other) raise TypeError(f"Unsupported operand type(s) for !=: 'AlgoAmount' and '{type(other).__name__}'") def __lt__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo < other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo < int(other) raise TypeError(f"Unsupported operand type(s) for <: 'AlgoAmount' and '{type(other).__name__}'") def __le__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo <= other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo <= int(other) raise TypeError(f"Unsupported operand type(s) for <=: 'AlgoAmount' and '{type(other).__name__}'") def __gt__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo > other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo > int(other) raise TypeError(f"Unsupported operand type(s) for >: 'AlgoAmount' and '{type(other).__name__}'") def __ge__(self, other: object) -> bool: if isinstance(other, AlgoAmount): return self.amount_in_micro_algo >= other.amount_in_micro_algo - elif isinstance(other, int | Decimal): + elif isinstance(other, int): return self.amount_in_micro_algo >= int(other) raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'") - def __sub__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: + def __sub__(self, other: AlgoAmount) -> AlgoAmount: if isinstance(other, AlgoAmount): total_micro_algos = self.micro_algos - other.micro_algos - elif isinstance(other, (int | Decimal)): - total_micro_algos = self.micro_algos - int(other) else: raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") return AlgoAmount.from_micro_algos(total_micro_algos) - def __rsub__(self, other: int | Decimal) -> AlgoAmount: - if isinstance(other, (int | Decimal)): + def __rsub__(self, other: int) -> AlgoAmount: + if isinstance(other, (int)): total_micro_algos = int(other) - self.micro_algos return AlgoAmount.from_micro_algos(total_micro_algos) raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'") - def __isub__(self, other: int | Decimal | AlgoAmount) -> Self: + def __isub__(self, other: AlgoAmount) -> Self: if isinstance(other, AlgoAmount): self.amount_in_micro_algo -= other.micro_algos - elif isinstance(other, (int | Decimal)): - self.amount_in_micro_algo -= int(other) else: raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") return self diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index b9a65d52..bec75bc5 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -17,8 +17,8 @@ from algokit_utils.models.state import TealTemplateParams if TYPE_CHECKING: + from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_factory import AppFactoryDeployResponse - from algokit_utils.clients.algorand_client import AlgorandClient __all__ = [ "TypedAppClientProtocol", diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 5d3663b3..b7b5b2af 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -953,8 +953,12 @@ def simulate( extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, simulation_round: int | None = None, - skip_signatures: int | None = None, + skip_signatures: bool | None = None, ) -> SendAtomicTransactionComposerResults: + """ + Simulate transaction group execution with configurable validation rules. + """ + atc = AtomicTransactionComposer() if skip_signatures else self._atc if skip_signatures: diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py index 716b7690..b14e0e70 100644 --- a/tests/accounts/test_account_manager.py +++ b/tests/accounts/test_account_manager.py @@ -2,7 +2,7 @@ import pytest from algokit_utils import Account -from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.algorand import AlgorandClient from algokit_utils.models.amount import AlgoAmount from tests.conftest import get_unique_name diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index c5d150b8..ba5424f1 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -9,6 +9,7 @@ from algosdk.atomic_transaction_composer import TransactionSigner, TransactionWithSigner from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.abi import ABIType from algokit_utils.applications.app_client import ( AppClient, @@ -18,7 +19,6 @@ ) from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Network -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount @@ -253,6 +253,10 @@ def test_clone_overriding_app_name( assert app_client.app_name == hello_world_arc32_app_spec.contract.name == "HelloWorld" assert cloned_app_client.app_name == cloned_app_name + # Test for explicit None when closning + cloned_app_client = app_client.clone(app_name=None) + assert cloned_app_client.app_name == app_client.app_name + def test_clone_inheriting_app_name_based_on_default_handling( algorand: AlgorandClient, diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 80785c3d..1e9c900e 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -5,6 +5,7 @@ from algosdk.logic import get_application_address from algosdk.transaction import OnComplete +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import ( AppClient, AppClientMethodCallCreateParams, @@ -19,7 +20,6 @@ AppFactoryCreateMethodCallParams, AppFactoryCreateWithSendParams, ) -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index 83a8ed2e..e0610466 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -1,7 +1,7 @@ import pytest +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_manager import AppManager -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from tests.conftest import check_output_stability diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index fbb80c37..a5386c1f 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -2,12 +2,12 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algokit_utils import Account +from algokit_utils.algorand import AlgorandClient from algokit_utils.assets.asset_manager import ( AccountAssetInformation, AssetInformation, BulkAssetOptInOutResult, ) -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AssetCreateParams, diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py index 91a337f5..09b2c5ed 100644 --- a/tests/clients/algorand_client/test_transfer.py +++ b/tests/clients/algorand_client/test_transfer.py @@ -2,7 +2,7 @@ import pytest from pytest_httpx._httpx_mock import HTTPXMock -from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.algorand import AlgorandClient from algokit_utils.clients.dispenser_api_client import DispenserApiConfig, TestNetDispenserApiClient from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount @@ -209,6 +209,9 @@ def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_accoun min_funding_increment=AlgoAmount.from_algos(1), ) + with pytest.raises(Exception, match="account asset info not found"): + algorand.asset.get_account_information(second_account, test_asset_id) + algorand.send.asset_opt_in( AssetOptInParams( sender=second_account.address, diff --git a/tests/conftest.py b/tests/conftest.py index c2eb036a..dfd25858 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,8 +17,8 @@ ensure_funded, replace_template_variables, ) +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.transactions.transaction_composer import AssetCreateParams if TYPE_CHECKING: diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index 8b7c1a4a..4dac294e 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -17,12 +17,12 @@ persist_sourcemaps, simulate_and_persist_response, ) +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import ( AppClient, AppClientMethodCallWithSendParams, ) from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.common import Program from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index fc52c4f0..aa5fed4d 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -7,13 +7,13 @@ from algosdk.transaction import OnComplete, PaymentTxn from algokit_utils import Account +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import ( AppClient, AppClientMethodCallWithSendParams, FundAppAccountParams, ) from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.config import config from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.amount import AlgoAmount diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 66e39e7d..c5e9bd7c 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -14,7 +14,7 @@ ) from algokit_utils._legacy_v2.account import get_account -from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.algorand import AlgorandClient from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index a64a62d2..f68ccddf 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -14,7 +14,7 @@ ) from algokit_utils._legacy_v2.account import get_account -from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.algorand import AlgorandClient from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 6c533f03..27b5a5c5 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -7,9 +7,9 @@ from algokit_utils import Account from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager -from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, From e9a9d630fea275f4c9b5d9a0240c62973f509b84 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Thu, 23 Jan 2025 17:05:26 +0100 Subject: [PATCH 15/31] docs: refining docstrings; enforcing sphinx styled format --- .pre-commit-config.yaml | 7 + poetry.lock | 32 +- pyproject.toml | 2 + src/algokit_utils/_debugging.py | 50 +- .../_legacy_v2/_ensure_funded.py | 18 +- src/algokit_utils/_legacy_v2/_transfer.py | 34 +- src/algokit_utils/_legacy_v2/account.py | 39 +- .../_legacy_v2/application_client.py | 43 +- src/algokit_utils/_legacy_v2/asset.py | 31 +- src/algokit_utils/_legacy_v2/common.py | 10 +- src/algokit_utils/accounts/account_manager.py | 231 ++++--- .../accounts/kmd_account_manager.py | 84 +-- src/algokit_utils/applications/abi.py | 73 +- src/algokit_utils/applications/app_client.py | 627 +++++++++++++++--- src/algokit_utils/applications/app_manager.py | 145 +++- .../applications/app_spec/arc32.py | 9 +- .../applications/app_spec/arc56.py | 248 ++++++- src/algokit_utils/assets/asset_manager.py | 203 ++++-- src/algokit_utils/clients/client_manager.py | 236 +++++-- src/algokit_utils/config.py | 15 +- src/algokit_utils/models/account.py | 67 +- src/algokit_utils/models/amount.py | 19 +- .../transactions/transaction_composer.py | 571 +++++++++------- .../transactions/transaction_creator.py | 18 +- .../transactions/transaction_sender.py | 145 +++- tests/applications/test_app_client.py | 10 - 26 files changed, 2113 insertions(+), 854 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fdfd6d3f..107998e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,3 +35,10 @@ repos: minimum_pre_commit_version: "2.9.2" files: "^(src|tests)/" exclude: "^tests/artifacts/" + - id: docstrings-check + name: docstrings-check + description: "Check docstrings for correctness" + entry: poetry run poe docstrings-check + language: system + types: [python] + files: "^(src)/" diff --git a/poetry.lock b/poetry.lock index 4191a80e..7fb81d7e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -558,6 +558,17 @@ files = [ {file = "docstring_parser-0.14.1.tar.gz", hash = "sha256:2c77522e31b7c88b1ab457a1f3c9ae38947ad719732260ba77ee8a3deb58622a"}, ] +[[package]] +name = "docstring-parser-fork" +version = "0.0.12" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "docstring_parser_fork-0.0.12-py3-none-any.whl", hash = "sha256:55d7cbbc8b367655efd64372b9a0b33a49bae930a8ddd5cdc4c6112312e28a87"}, + {file = "docstring_parser_fork-0.0.12.tar.gz", hash = "sha256:b44c5e0be64ae80f395385f01497d381bd094a57221fd9ff020987d06857b2a0"}, +] + [[package]] name = "docutils" version = "0.18.1" @@ -1603,6 +1614,25 @@ files = [ {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, ] +[[package]] +name = "pydoclint" +version = "0.6.0" +description = "A Python docstring linter that checks arguments, returns, yields, and raises sections" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydoclint-0.6.0-py2.py3-none-any.whl", hash = "sha256:f1fb9676efe70c9a0443c7177186a01001a2227c9100272ef72d7da269ae9bbd"}, + {file = "pydoclint-0.6.0.tar.gz", hash = "sha256:bee5b509f5407c8ae180ff86a6776895084129097a4600b749fd133fd58a1cf4"}, +] + +[package.dependencies] +click = ">=8.1.0" +docstring_parser_fork = ">=0.0.12" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +flake8 = ["flake8 (>=4)"] + [[package]] name = "pygments" version = "2.18.0" @@ -2590,4 +2620,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "9669798ad0a27bb4f0309b4cd4f23b1db96e00dd9d18c40a702a6e40ea265a4b" +content-hash = "be28f13fd9fa25c4a7204d6888efd33ecbed11c7fa903768d423fa737dd4e169" diff --git a/pyproject.toml b/pyproject.toml index 80b45a6a..f01dfa3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ pytest-xdist = "^3.4.0" sphinx-markdown-builder = "^0.6.6" linkify-it-py = "^2.0.3" setuptools = "^75.2.0" +pydoclint = "^0.6.0" [build-system] requires = ["poetry-core"] @@ -134,6 +135,7 @@ suppress-none-returning = true docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" +docstrings-check = "pydoclint src --style sphinx --arg-type-hints-in-docstring false --check-return-types false --exclude src/algokit_utils/_legacy_v2" [tool.pytest.ini_options] pythonpath = ["src", "tests"] diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index e50031d8..fe4a0f49 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -119,12 +119,8 @@ def _upsert_debug_sourcemaps(sourcemaps: list[AVMDebuggerSourceMapEntry], projec This function updates or inserts debug sourcemaps. If path in the sourcemap during iteration leads to non existing file, removes it. Otherwise upserts. - Args: - sourcemaps (list[AVMDebuggerSourceMapEntry]): A list of AVMDebuggerSourceMapEntry objects. - project_root (Path): The root directory of the project. - - Returns: - None + :param sourcemaps: A list of AVMDebuggerSourceMapEntry objects. + :param project_root: The root directory of the project. """ sources_path = project_root / ALGOKIT_DIR / SOURCES_DIR / SOURCES_FILE @@ -197,12 +193,11 @@ def persist_sourcemaps( ) -> None: """ Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts. - Args: - sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects. - project_root (Path): The root directory of the project. - client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. - with_sources (bool): If True, it will dump teal source files along with sourcemaps. - Default is True, as needed by an AlgoKit AVM debugger. + + :param sources: A list of PersistSourceMapInput objects. + :param project_root: The root directory of the project. + :param client: An AlgodClient object for interacting with the Algorand blockchain. + :param with_sources: If True, it will dump teal source files along with sourcemaps. """ sourcemaps = [ @@ -267,20 +262,23 @@ def simulate_and_persist_response( # noqa: PLR0913 simulation_round: int | None = None, skip_signatures: bool | None = None, ) -> SimulateAtomicTransactionResponse: - """ - Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, - and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. - - :param atc: An `AtomicTransactionComposer` object representing the atomic transactions to be - simulated and persisted. - :param project_root: A `Path` object representing the root directory of the project. - :param algod_client: An `AlgodClient` object representing the Algorand client. - :param buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256mb. - :return: None - - Returns: - SimulateAtomicTransactionResponse: The simulated response after persisting it - for AlgoKit AVM Debugger consumption. + """Simulates atomic transactions and persists simulation response to a JSON file. + + Simulates the atomic transactions using the provided AtomicTransactionComposer and AlgodClient, + then persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. + + :param atc: AtomicTransactionComposer containing transactions to simulate and persist + :param project_root: Root directory path of the project + :param algod_client: Algorand client instance + :param buffer_size_mb: Size of trace buffer in megabytes, defaults to 256 + :param allow_more_logs: Flag to allow additional logs, defaults to None + :param allow_empty_signatures: Flag to allow empty signatures, defaults to None + :param allow_unnamed_resources: Flag to allow unnamed resources, defaults to None + :param extra_opcode_budget: Additional opcode budget, defaults to None + :param exec_trace_config: Execution trace configuration, defaults to None + :param simulation_round: Round number for simulation, defaults to None + :param skip_signatures: Flag to skip signatures, defaults to None + :return: Simulated response after persisting for AlgoKit AVM Debugger consumption """ atc_to_simulate = atc.clone() sp = algod_client.suggested_params() diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index 99409b36..ef4d6fbe 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -125,19 +125,15 @@ def ensure_funded( parameters: EnsureBalanceParameters, ) -> EnsureFundedResponse | None: """ - Funds a given account using a funding source such that it has a certain amount of algos free to spend - (accounting for ALGOs locked in minimum balance requirement) - see + Funds a given account using a funding source to ensure it has sufficient spendable ALGOs. + Ensures the target account has enough ALGOs free to spend after accounting for ALGOs locked in minimum balance + requirements. See https://developer.algorand.org/docs/get-details/accounts/#minimum-balance for details on minimum + balance requirements. - Args: - client (AlgodClient): An instance of the AlgodClient class from the AlgoSDK library. - parameters (EnsureBalanceParameters): An instance of the EnsureBalanceParameters class that - specifies the account to fund and the minimum spending balance. - - Returns: - PaymentTxn | str | None: If funds are needed, the function returns a payment transaction or a - string indicating that the dispenser API was used. If no funds are needed, the function returns None. + :param client: An instance of the AlgodClient class from the AlgoSDK library + :param parameters: Parameters specifying the account to fund and minimum spending balance requirements + :return: If funds are needed, returns payment transaction details or dispenser API response. Returns None if no funding needed """ address_to_fund = _get_address_to_fund(parameters) diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index 5ef9d203..e3ae4e5c 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -19,18 +19,16 @@ @dataclasses.dataclass(kw_only=True) class TransferParametersBase: - """Parameters for transferring µALGOs between accounts - - Args: - from_account (Account | AccountTransactionSigner): The account (with private key) or signer that will send - the µALGOs - to_address (str): The account address that will receive the µALGOs - suggested_params (SuggestedParams | None): (optional) transaction parameters - note (str | bytes | None): (optional) transaction note - fee_micro_algos (int | None): (optional) The flat fee you want to pay, useful for covering extra fees in a - transaction group or app call - max_fee_micro_algos (int | None): (optional) The maximum fee that you are happy to pay (default: unbounded) - - if this is set it's possible the transaction could get rejected during network congestion + """Parameters for transferring µALGOs between accounts. + + This class contains the base parameters needed for transferring µALGOs between Algorand accounts. + + :ivar from_account: The account (with private key) or signer that will send the µALGOs + :ivar to_address: The account address that will receive the µALGOs + :ivar suggested_params: Transaction parameters, defaults to None + :ivar note: Transaction note, defaults to None + :ivar fee_micro_algos: The flat fee you want to pay, useful for covering extra fees in a transaction group or app call, defaults to None + :ivar max_fee_micro_algos: The maximum fee that you are happy to pay - if this is set it's possible the transaction could get rejected during network congestion, defaults to None """ from_account: Account | AccountTransactionSigner @@ -50,13 +48,13 @@ class TransferParameters(TransferParametersBase): @dataclasses.dataclass(kw_only=True) class TransferAssetParameters(TransferParametersBase): - """Parameters for transferring assets between accounts + """Parameters for transferring assets between accounts. + + Defines the parameters needed to transfer Algorand Standard Assets (ASAs) between accounts. - Args: - asset_id (int): The asset id that will be transfered - amount (int): The amount to send - clawback_from (str | None): An address of a target account from which to perform a clawback operation. Please - note, in such cases senderAccount must be equal to clawback field on ASA metadata. + :param asset_id: The asset id that will be transferred + :param amount: The amount of the asset to send + :param clawback_from: An address of a target account from which to perform a clawback operation. Please note, in such cases senderAccount must be equal to clawback field on ASA metadata, defaults to None """ asset_id: int diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index 9da21ca1..e9161cd4 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -171,28 +171,23 @@ def get_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: """Returns an Algorand account with private key loaded by convention based on the given name identifier. - - # Convention - - **Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret - Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a - secret storage service rather than the file system. - - **LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will - create it and fund the account for you - - This allows you to write code that will work seamlessly in production and local development (LocalNet) without - manual config locally (including when you reset the LocalNet). - - # Example - If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get - that private key loaded into an account object: - ```python - account = get_account('ACCOUNT', algod) - ``` - - If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account - that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + Returns an Algorand account with private key loaded by convention based on the given name identifier. + + For non-LocalNet environments, loads the mnemonic secret from environment variable {name}_MNEMONIC. + For LocalNet environments, loads or creates an account from a KMD wallet named {name}. + + :example: + >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call: + >>> account = get_account('ACCOUNT', algod) + >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created + >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + + :param client: The Algorand client to use + :param name: The name identifier to use for loading/creating the account + :param fund_with_algos: Amount of Algos to fund new LocalNet accounts with, defaults to 1000 + :param kmd_client: Optional KMD client to use for LocalNet wallet operations + :raises Exception: If required environment variable is missing in non-LocalNet environment + :return: An Account object with loaded private key """ mnemonic_key = f"{name.upper()}_MNEMONIC" diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index ca635c26..ddf10079 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -95,7 +95,28 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: "```" ) class ApplicationClient: - """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" + """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app + + ApplicationClient can be created with an app_id to interact with an existing application, alternatively + it can be created with a creator and indexer_client specified to find existing applications by name and creator. + + :param AlgodClient algod_client: AlgoSDK algod client + :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one + :param int app_id: The app_id of an existing application, to instead find the application by creator and name + use the creator and indexer_client parameters + :param str | Account creator: The address or Account of the app creator to resolve the app_id + :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by + creator and app name + :param AppLookup existing_deployments: + :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and + creator was passed as an Account will use that. + :param str sender: Address to use as the sender for all transactions, will use the address associated with the + signer if not specified. + :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should + *NOT* include the TMPL_ prefix + :param str | None app_name: Name of application to use when deploying, defaults to name defined on the + Application Specification + """ @overload def __init__( @@ -141,26 +162,6 @@ def __init__( # noqa: PLR0913 template_values: au_deploy.TemplateValueMapping | None = None, app_name: str | None = None, ): - """ApplicationClient can be created with an app_id to interact with an existing application, alternatively - it can be created with a creator and indexer_client specified to find existing applications by name and creator. - - :param AlgodClient algod_client: AlgoSDK algod client - :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one - :param int app_id: The app_id of an existing application, to instead find the application by creator and name - use the creator and indexer_client parameters - :param str | Account creator: The address or Account of the app creator to resolve the app_id - :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by - creator and app name - :param AppLookup existing_deployments: - :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and - creator was passed as an Account will use that. - :param str sender: Address to use as the sender for all transactions, will use the address associated with the - signer if not specified. - :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should - *NOT* include the TMPL_ prefix - :param str | None app_name: Name of application to use when deploying, defaults to name defined on the - Application Specification - """ self.algod_client = algod_client self.app_spec = ( au_spec.ApplicationSpecification.from_json(app_spec.read_text()) if isinstance(app_spec, Path) else app_spec diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 85c39f5a..7aa3b9d6 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -1,16 +1,12 @@ import logging -from typing import TYPE_CHECKING +from enum import Enum, auto from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner from algosdk.constants import TX_GROUP_LIMIT from algosdk.transaction import AssetTransferTxn +from algosdk.v2client.algod import AlgodClient from typing_extensions import deprecated -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - -from enum import Enum, auto - from algokit_utils.models.account import Account __all__ = ["opt_in", "opt_out"] @@ -79,13 +75,11 @@ def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. - account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. - asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values - are the transaction IDs for opting-in to each asset. + :param algod_client: An instance of the AlgodClient class from the algosdk library. + :param account: An instance of the Account class representing the account that wants to opt-in to the assets. + :param asset_ids: A list of integers representing the asset IDs to opt-in to. + :return: A dictionary where the keys are the asset IDs and the values are the transaction IDs for opting-in to each asset. + :rtype: dict[int, str] """ _ensure_account_is_valid(algod_client, account) _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN) @@ -133,14 +127,11 @@ def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero. - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. - account (Account): An instance of the Account class that holds the private key and address for an account. - asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of + :param AlgodClient algod_client: An instance of the AlgodClient class from the algosdk library. + :param Account account: An instance of the Account class representing the account that wants to opt-out from the assets. + :param list[int] asset_ids: A list of integers representing the asset IDs to opt-out from. + :return dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of the executed transactions. - """ _ensure_account_is_valid(algod_client, account) _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT) diff --git a/src/algokit_utils/_legacy_v2/common.py b/src/algokit_utils/_legacy_v2/common.py index cd412f82..65051a60 100644 --- a/src/algokit_utils/_legacy_v2/common.py +++ b/src/algokit_utils/_legacy_v2/common.py @@ -14,13 +14,13 @@ class Program: - """A compiled TEAL program""" + """A compiled TEAL program + + :param program: The TEAL program source code + :param client: The AlgodClient instance to use for compiling the program + """ def __init__(self, program: str, client: "AlgodClient"): - """ - Fully compile the program source to binary and generate a - source map for matching pc to line number - """ self.teal = program result: dict = client.compile(strip_comments(self.teal), source_map=True) self.raw_binary = base64.b64decode(result["result"]) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 37fc3ef1..d1a81e56 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -63,36 +63,36 @@ class AccountInformation: See `https://developer.algorand.org/docs/rest-apis/algod/#account` for detailed field descriptions. - :param str address: The account's address - :param int amount: The account's current balance in microAlgos - :param int amount_without_pending_rewards: The account's balance in microAlgos without the pending rewards - :param int min_balance: The account's minimum required balance in microAlgos - :param int pending_rewards: The amount of pending rewards in microAlgos - :param int rewards: The amount of rewards earned in microAlgos - :param int round: The round for which this information is relevant - :param str status: The account's status (e.g., 'Offline', 'Online') - :param int|None total_apps_opted_in: Number of applications this account has opted into - :param int|None total_assets_opted_in: Number of assets this account has opted into - :param int|None total_box_bytes: Total number of box bytes used by this account - :param int|None total_boxes: Total number of boxes used by this account - :param int|None total_created_apps: Number of applications created by this account - :param int|None total_created_assets: Number of assets created by this account - :param list[dict]|None apps_local_state: Local state of applications this account has opted into - :param int|None apps_total_extra_pages: Number of extra pages allocated to applications - :param dict|None apps_total_schema: Total schema for all applications - :param list[dict]|None assets: Assets held by this account - :param str|None auth_addr: If rekeyed, the authorized address - :param int|None closed_at_round: Round when this account was closed - :param list[dict]|None created_apps: Applications created by this account - :param list[dict]|None created_assets: Assets created by this account - :param int|None created_at_round: Round when this account was created - :param bool|None deleted: Whether this account is deleted - :param bool|None incentive_eligible: Whether this account is eligible for incentives - :param int|None last_heartbeat: Last heartbeat round for this account - :param int|None last_proposed: Last round this account proposed a block - :param dict|None participation: Participation information for this account - :param int|None reward_base: Base reward for this account - :param str|None sig_type: Signature type for this account + :ivar str address: The account's address + :ivar int amount: The account's current balance in microAlgos + :ivar int amount_without_pending_rewards: The account's balance in microAlgos without the pending rewards + :ivar int min_balance: The account's minimum required balance in microAlgos + :ivar int pending_rewards: The amount of pending rewards in microAlgos + :ivar int rewards: The amount of rewards earned in microAlgos + :ivar int round: The round for which this information is relevant + :ivar str status: The account's status (e.g., 'Offline', 'Online') + :ivar int|None total_apps_opted_in: Number of applications this account has opted into + :ivar int|None total_assets_opted_in: Number of assets this account has opted into + :ivar int|None total_box_bytes: Total number of box bytes used by this account + :ivar int|None total_boxes: Total number of boxes used by this account + :ivar int|None total_created_apps: Number of applications created by this account + :ivar int|None total_created_assets: Number of assets created by this account + :ivar list[dict]|None apps_local_state: Local state of applications this account has opted into + :ivar int|None apps_total_extra_pages: Number of extra pages allocated to applications + :ivar dict|None apps_total_schema: Total schema for all applications + :ivar list[dict]|None assets: Assets held by this account + :ivar str|None auth_addr: If rekeyed, the authorized address + :ivar int|None closed_at_round: Round when this account was closed + :ivar list[dict]|None created_apps: Applications created by this account + :ivar list[dict]|None created_assets: Assets created by this account + :ivar int|None created_at_round: Round when this account was created + :ivar bool|None deleted: Whether this account is deleted + :ivar bool|None incentive_eligible: Whether this account is eligible for incentives + :ivar int|None last_heartbeat: Last heartbeat round for this account + :ivar int|None last_proposed: Last round this account proposed a block + :ivar dict|None participation: Participation information for this account + :ivar int|None reward_base: Base reward for this account + :ivar str|None sig_type: Signature type for this account """ address: str @@ -133,17 +133,14 @@ class AccountManager: This class provides functionality to create, track, and manage various types of accounts including mnemonic-based, rekeyed, multisig, and logic signature accounts. - """ - def __init__(self, client_manager: ClientManager): - """ - Create a new account manager. + :param client_manager: The ClientManager client to use for algod and kmd clients - :param ClientManager client_manager: The ClientManager client to use for algod and kmd clients + :example: + >>> account_manager = AccountManager(client_manager) + """ - :example: - >>> account_manager = AccountManager(client_manager) - """ + def __init__(self, client_manager: ClientManager): self._client_manager = client_manager self._kmd_account_manager = KmdAccountManager(client_manager) self._signers = dict[str, TransactionSigner]() @@ -156,7 +153,7 @@ def set_default_signer(self, signer: TransactionSigner) -> Self: If this isn't set and a transaction needs signing for a given sender then an error will be thrown from `get_signer` / `get_account`. - :param TransactionSigner signer: A `TransactionSigner` signer to use. + :param signer: A `TransactionSigner` signer to use. :returns: The `AccountManager` so method calls can be chained :example: @@ -173,8 +170,8 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: """ Tracks the given `TransactionSigner` against the given sender address for later signing. - :param str sender: The sender address to use this signer for - :param TransactionSigner signer: The `TransactionSigner` to sign transactions with for the given sender + :param sender: The sender address to use this signer for + :param signer: The `TransactionSigner` to sign transactions with for the given sender :returns: The `AccountManager` instance for method chaining :example: @@ -190,7 +187,7 @@ def set_signer_from_account(self, account: Account | LogicSigAccount | MultiSigA Note: If you are generating accounts via the various methods on `AccountManager` (like `random`, `from_mnemonic`, `logic_sig`, etc.) then they automatically get tracked. - :param Account|LogicSigAccount|MultiSigAccount account: The account to register + :param account: The account to register :returns: The `AccountManager` instance for method chaining :example: @@ -213,7 +210,7 @@ def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSign If no signer has been registered for that address then the default signer is used if registered. - :param str|Account|LogicSigAccount sender: The sender address or account + :param sender: The sender address or account :returns: The `TransactionSigner` :raises ValueError: If no signer is found and no default signer is set @@ -229,7 +226,7 @@ def get_account(self, sender: str) -> Account: """ Returns the `Account` for the given sender address. - :param str sender: The sender address + :param sender: The sender address :returns: The `Account` :raises ValueError: If no account is found or if the account is not a regular account @@ -250,7 +247,7 @@ def get_logic_sig_account(self, sender: str) -> LogicSigAccount: """ Returns the `LogicSigAccount` for the given sender address. - :param str sender: The sender address + :param sender: The sender address :returns: The `LogicSigAccount` :raises ValueError: If no account is found or if the account is not a logic signature account """ @@ -268,7 +265,7 @@ def get_information(self, sender: str | Account) -> AccountInformation: See ``_ for response data schema details. - :param str|Account sender: The address of the sender/account to look up + :param sender: The address of the sender/account to look up :returns: The account information :example: @@ -284,7 +281,7 @@ def _register_account(self, private_key: str) -> Account: """ Helper method to create and register an account with its signer. - :param str private_key: The private key for the account + :param private_key: The private key for the account :returns: The registered Account instance """ account = Account(private_key=private_key) @@ -295,8 +292,8 @@ def _register_logic_sig(self, program: bytes, args: list[bytes] | None = None) - """ Helper method to create and register a logic signature account. - :param bytes program: The bytes that make up the compiled logic signature - :param list[bytes]|None args: The (binary) arguments to pass into the logic signature + :param program: The bytes that make up the compiled logic signature + :param args: The (binary) arguments to pass into the logic signature :returns: The registered LogicSigAccount instance """ logic_sig = LogicSigAccount(program, args) @@ -309,10 +306,10 @@ def _register_multi_sig( """ Helper method to create and register a multisig account. - :param int version: The version of the multisig account - :param int threshold: The threshold number of signatures required - :param list[str] addrs: The list of addresses that can sign - :param list[Account] signing_accounts: The list of accounts that are present to sign + :param version: The version of the multisig account + :param threshold: The threshold number of signatures required + :param addrs: The list of addresses that can sign + :param signing_accounts: The list of accounts that are present to sign :returns: The registered MultisigAccount instance """ msig_account = MultiSigAccount( @@ -326,7 +323,7 @@ def from_mnemonic(self, mnemonic: str) -> Account: """ Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. - :param str mnemonic: The mnemonic secret representing the private key of an account + :param mnemonic: The mnemonic secret representing the private key of an account :returns: The account .. warning:: @@ -346,8 +343,8 @@ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Ac This allows you to write code that will work seamlessly in production and local development (LocalNet) without manual config locally (including when you reset the LocalNet). - :param str name: The name identifier of the account - :param AlgoAmount|None fund_with: Optional amount to fund the account with when it gets created + :param name: The name identifier of the account + :param fund_with: Optional amount to fund the account with when it gets created (when targeting LocalNet) :returns: The account :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} @@ -384,9 +381,9 @@ def from_kmd( """ Tracks and returns an Algorand account with private key loaded from the given KMD wallet. - :param str name: The name of the wallet to retrieve an account from - :param Callable[[dict[str, Any]], bool]|None predicate: Optional filter to use to find the account - :param str|None sender: Optional sender address to use this signer for (aka a rekeyed account) + :param name: The name of the wallet to retrieve an account from + :param predicate: Optional filter to use to find the account + :param sender: Optional sender address to use this signer for (aka a rekeyed account) :returns: The account :raises ValueError: If unable to find KMD account with given name and predicate @@ -406,8 +403,8 @@ def logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSig """ Tracks and returns an account that represents a logic signature. - :param bytes program: The bytes that make up the compiled logic signature - :param list[bytes]|None args: Optional (binary) arguments to pass into the logic signature + :param program: The bytes that make up the compiled logic signature + :param args: Optional (binary) arguments to pass into the logic signature :returns: A logic signature account wrapper :example: @@ -421,10 +418,10 @@ def multi_sig( """ Tracks and returns an account that supports partial or full multisig signing. - :param int version: The version of the multisig account - :param int threshold: The threshold number of signatures required - :param list[str] addrs: The list of addresses that can sign - :param list[Account] signing_accounts: The signers that are currently present + :param version: The version of the multisig account + :param threshold: The threshold number of signatures required + :param addrs: The list of addresses that can sign + :param signing_accounts: The signers that are currently present :returns: A multisig account wrapper :example: @@ -483,8 +480,8 @@ def rekeyed(self, sender: Account | str, account: Account) -> Account: """ Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. - :param Account|str sender: The account or address to use as the sender - :param Account account: The account to use as the signer for this new rekeyed account + :param sender: The account or address to use as the sender + :param account: The account to use as the signer for this new rekeyed account :returns: The rekeyed account :example: @@ -514,18 +511,18 @@ def rekey_account( # noqa: PLR0913 """ Rekey an account to a new address. - :param str|Account account: The account to rekey - :param str|Account rekey_to: The address or account to rekey to - :param TransactionSigner|None signer: Optional transaction signer - :param bytes|None note: Optional transaction note - :param bytes|None lease: Optional transaction lease - :param AlgoAmount|None static_fee: Optional static fee - :param AlgoAmount|None extra_fee: Optional extra fee - :param AlgoAmount|None max_fee: Optional max fee - :param int|None validity_window: Optional validity window - :param int|None first_valid_round: Optional first valid round - :param int|None last_valid_round: Optional last valid round - :param bool|None suppress_log: Optional flag to suppress logging + :param account: The account to rekey + :param rekey_to: The address or account to rekey to + :param signer: Optional transaction signer + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional max fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round + :param suppress_log: Optional flag to suppress logging :returns: The result of the transaction and the transaction that was sent .. warning:: @@ -616,24 +613,24 @@ def ensure_funded( # noqa: PLR0913 See ``_ for details. - :param str|Account account_to_fund: The account to fund - :param str|Account dispenser_account: The account to use as a dispenser funding source - :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account + :param account_to_fund: The account to fund + :param dispenser_account: The account to use as a dispenser funding source + :param min_spending_balance: The minimum balance of Algo that the account should have available to spend - :param AlgoAmount|None min_funding_increment: Optional minimum funding increment - :param int|None max_rounds_to_wait: Optional maximum rounds to wait for transaction - :param bool|None suppress_log: Optional flag to suppress logging - :param bool|None populate_app_call_resources: Optional flag to populate app call resources - :param TransactionSigner|None signer: Optional transaction signer - :param str|None rekey_to: Optional rekey address - :param bytes|None note: Optional transaction note - :param bytes|None lease: Optional transaction lease - :param AlgoAmount|None static_fee: Optional static fee - :param AlgoAmount|None extra_fee: Optional extra fee - :param AlgoAmount|None max_fee: Optional maximum fee - :param int|None validity_window: Optional validity window - :param int|None first_valid_round: Optional first valid round - :param int|None last_valid_round: Optional last valid round + :param min_funding_increment: Optional minimum funding increment + :param max_rounds_to_wait: Optional maximum rounds to wait for transaction + :param suppress_log: Optional flag to suppress logging + :param populate_app_call_resources: Optional flag to populate app call resources + :param signer: Optional transaction signer + :param rekey_to: Optional rekey address + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional maximum fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or None if no funds were needed @@ -726,23 +723,23 @@ def ensure_funded_from_environment( # noqa: PLR0913 See ``_ for details. - :param str|Account account_to_fund: The account to fund - :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account should have available to + :param account_to_fund: The account to fund + :param min_spending_balance: The minimum balance of Algo that the account should have available to spend - :param AlgoAmount|None min_funding_increment: Optional minimum funding increment - :param int|None max_rounds_to_wait: Optional maximum rounds to wait for transaction - :param bool|None suppress_log: Optional flag to suppress logging - :param bool|None populate_app_call_resources: Optional flag to populate app call resources - :param TransactionSigner|None signer: Optional transaction signer - :param str|None rekey_to: Optional rekey address - :param bytes|None note: Optional transaction note - :param bytes|None lease: Optional transaction lease - :param AlgoAmount|None static_fee: Optional static fee - :param AlgoAmount|None extra_fee: Optional extra fee - :param AlgoAmount|None max_fee: Optional maximum fee - :param int|None validity_window: Optional validity window - :param int|None first_valid_round: Optional first valid round - :param int|None last_valid_round: Optional last valid round + :param min_funding_increment: Optional minimum funding increment + :param max_rounds_to_wait: Optional maximum rounds to wait for transaction + :param suppress_log: Optional flag to suppress logging + :param populate_app_call_resources: Optional flag to populate app call resources + :param signer: Optional transaction signer + :param rekey_to: Optional rekey address + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional maximum fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or None if no funds were needed @@ -814,7 +811,7 @@ def ensure_funded_from_testnet_dispenser_api( account_to_fund: str | Account, dispenser_client: TestNetDispenserApiClient, min_spending_balance: AlgoAmount, - *, # Force remaining params to be keyword-only + *, min_funding_increment: AlgoAmount | None = None, ) -> EnsureFundedFromTestnetDispenserApiResponse | None: """ @@ -825,11 +822,11 @@ def ensure_funded_from_testnet_dispenser_api( See ``_ for details. - :param str|Account account_to_fund: The account to fund - :param TestNetDispenserApiClient dispenser_client: The TestNet dispenser funding client - :param AlgoAmount min_spending_balance: The minimum balance of Algo that the account should have + :param account_to_fund: The account to fund + :param dispenser_client: The TestNet dispenser funding client + :param min_spending_balance: The minimum balance of Algo that the account should have available to spend - :param AlgoAmount|None min_funding_increment: Optional minimum funding increment + :param min_funding_increment: Optional minimum funding increment :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or None if no funds were needed :raises ValueError: If attempting to fund on non-TestNet network diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index 701ac0a1..a7dc6636 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -15,15 +15,15 @@ class KmdAccount(Account): - """Account retrieved from KMD with signing capabilities, extending base Account""" + """Account retrieved from KMD with signing capabilities, extending base Account. - def __init__(self, private_key: str, address: str | None = None) -> None: - """Initialize KMD account with private key and optional address override + Provides an account implementation that can be used to sign transactions using keys stored in KMD. - Args: - private_key: Base64 encoded private key - address: Optional address override (for rekeyed accounts) - """ + :param private_key: Base64 encoded private key + :param address: Optional address override for rekeyed accounts, defaults to None + """ + + def __init__(self, private_key: str, address: str | None = None) -> None: super().__init__(private_key=private_key, address=address or "") @@ -33,11 +33,6 @@ class KmdAccountManager: _kmd: KMDClient | None def __init__(self, client_manager: ClientManager) -> None: - """Create a new KMD manager. - - Args: - client_manager: ClientManager to use for account management - """ self._client_manager = client_manager try: self._kmd = client_manager.kmd @@ -45,13 +40,10 @@ def __init__(self, client_manager: ClientManager) -> None: self._kmd = None def kmd(self) -> KMDClient: - """Get the KMD client, initializing it if needed. - - Returns: - KMDClient: The initialized KMD client + """Returns the KMD client, initializing it if needed. - Raises: - Exception: If KMD is not configured + :raises Exception: If KMD client is not configured and not running against LocalNet + :return: The KMD client """ if self._kmd is None: if self._client_manager.is_localnet(): @@ -69,23 +61,15 @@ def get_wallet_account( ) -> KmdAccount | None: """Returns an Algorand signing account with private key loaded from the given KMD wallet. - Args: - wallet_name: The name of the wallet to retrieve an account from - predicate: Optional filter to use to find the account (otherwise returns a random account from the wallet) - sender: Optional sender address to use this signer for (aka a rekeyed account) + Retrieves an account from a KMD wallet that matches the given predicate, or a random account + if no predicate is provided. - Returns: - Optional[KmdAccount]: The signing account or None if no matching wallet or account was found - - Example: - ```python - # Get default funded account in a LocalNet - default_dispenser = kmd_manager.get_wallet_account( - "unencrypted-default-wallet", - lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 - ) - ``` + :param wallet_name: The name of the wallet to retrieve an account from + :param predicate: Optional filter to use to find the account (otherwise gets a random account from the wallet) + :param sender: Optional sender address to use this signer for (aka a rekeyed account) + :return: The signing account or None if no matching wallet or account was found """ + kmd_client = self.kmd() wallets = kmd_client.list_wallets() wallet = next((w for w in wallets if w["name"] == wallet_name), None) @@ -115,25 +99,13 @@ def get_wallet_account( def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount: """Gets or creates a funded account in a KMD wallet of the given name. - This is useful to get idempotent accounts from LocalNet without having to specify the private key - (which will change when resetting the LocalNet). - - Args: - name: The name of the wallet to retrieve / create - fund_with: The number of Algos to fund the account with when created (default: 1000) + Provides idempotent access to accounts from LocalNet without specifying the private key. - Returns: - KmdAccount: An Algorand account with private key loaded - - Example: - ```python - # Idempotently get (if exists) or create (if doesn't exist) an account by name using KMD - # if creating it then fund it with 2 ALGO from the default dispenser account - new_account = kmd_manager.get_or_create_wallet_account("account1", 2) - # This will return the same account as above since the name matches - existing_account = kmd_manager.get_or_create_wallet_account("account1") - ``` + :param name: The name of the wallet to retrieve / create + :param fund_with: The number of Algos to fund the account with when created + :return: An Algorand account with private key loaded """ + existing = self.get_wallet_account(name) if existing: return existing @@ -168,16 +140,10 @@ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = def get_localnet_dispenser_account(self) -> KmdAccount: """Returns an Algorand account with private key loaded for the default LocalNet dispenser account. - Returns: - KmdAccount: The default LocalNet dispenser account - - Raises: - Exception: If not running against LocalNet or dispenser account not found + Retrieves the default funded account from LocalNet that can be used to fund other accounts. - Example: - ```python - dispenser = kmd_manager.get_localnet_dispenser_account() - ``` + :raises Exception: If not running against LocalNet or dispenser account not found + :return: The default LocalNet dispenser account """ if not self._client_manager.is_localnet(): raise Exception("Can't get LocalNet dispenser account from non LocalNet network") diff --git a/src/algokit_utils/applications/abi.py b/src/algokit_utils/applications/abi.py index 7ce82a20..2dbfd6b1 100644 --- a/src/algokit_utils/applications/abi.py +++ b/src/algokit_utils/applications/abi.py @@ -38,6 +38,17 @@ @dataclass(kw_only=True) class ABIReturn: + """Represents the return value from an ABI method call. + + Wraps the raw return value and decoded value along with any decode errors. + + :ivar result: The ABIResult object containing the method call results + :ivar raw_value: The raw return value from the method call + :ivar value: The decoded return value from the method call + :ivar method: The ABI method definition + :ivar decode_error: The exception that occurred during decoding, if any + """ + raw_value: bytes | None = None value: ABIValue | None = None method: AlgorandABIMethod | None = None @@ -52,18 +63,35 @@ def __init__(self, result: ABIResult) -> None: @property def is_success(self) -> bool: - """Returns True if the ABI call was successful (no decode error)""" + """Returns True if the ABI call was successful (no decode error) + + :return: True if no decode error occurred, False otherwise + """ return self.decode_error is None def get_arc56_value( self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] ) -> Arc56ReturnValueType: + """Gets the ARC-56 formatted return value. + + :param method: The ABI method definition + :param structs: Dictionary of struct definitions + :return: The decoded return value in ARC-56 format + """ return get_arc56_value(self, method, structs) def get_arc56_value( abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] ) -> Arc56ReturnValueType: + """Gets the ARC-56 formatted return value from an ABI return. + + :param abi_return: The ABI return value to decode + :param method: The ABI method definition + :param structs: Dictionary of struct definitions + :raises ValueError: If there was an error decoding the return value + :return: The decoded return value in ARC-56 format + """ if isinstance(method, AlgorandABIMethod): type_str = method.returns.type struct = None # AlgorandABIMethod doesn't have struct info @@ -97,6 +125,14 @@ def get_arc56_value( def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: PLR0911, ANN401 + """Encodes a value according to its ABI type. + + :param value: The value to encode + :param type_str: The ABI type string + :param structs: Dictionary of struct definitions + :raises ValueError: If the value cannot be encoded for the given type + :return: The ABI encoded bytes + """ if isinstance(value, (bytes | bytearray)): return value if type_str == "AVMUint64": @@ -122,6 +158,13 @@ def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[Str def get_abi_decoded_value( value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[StructField]] ) -> ABIValue: + """Decodes a value according to its ABI type. + + :param value: The value to decode + :param type_str: The ABI type string or type object + :param structs: Dictionary of struct definitions + :return: The decoded ABI value + """ type_value = str(type_str) if type_value == "AVMBytes" or not isinstance(value, bytes): @@ -142,6 +185,14 @@ def get_abi_tuple_from_abi_struct( struct_fields: list[StructField], structs: dict[str, list[StructField]], ) -> list[Any]: + """Converts an ABI struct to a tuple representation. + + :param struct_value: The struct value as a dictionary + :param struct_fields: List of struct field definitions + :param structs: Dictionary of struct definitions + :raises ValueError: If a required field is missing from the struct + :return: The struct as a tuple + """ result = [] for field in struct_fields: key = field.name @@ -161,6 +212,13 @@ def get_abi_tuple_from_abi_struct( def get_abi_tuple_type_from_abi_struct_definition( struct_def: list[StructField], structs: dict[str, list[StructField]] ) -> algosdk.abi.TupleType: + """Creates a TupleType from a struct definition. + + :param struct_def: The struct field definitions + :param structs: Dictionary of struct definitions + :raises ValueError: If a field type is invalid + :return: The TupleType representing the struct + """ types = [] for field in struct_def: field_type = field.type @@ -181,6 +239,13 @@ def get_abi_struct_from_abi_tuple( struct_fields: list[StructField], structs: dict[str, list[StructField]], ) -> dict[str, Any]: + """Converts a decoded tuple to an ABI struct. + + :param decoded_tuple: The tuple to convert + :param struct_fields: List of struct field definitions + :param structs: Dictionary of struct definitions + :return: The tuple as a struct dictionary + """ result = {} for i, field in enumerate(struct_fields): key = field.name @@ -197,5 +262,11 @@ def get_abi_struct_from_abi_tuple( @dataclass(kw_only=True, frozen=True) class BoxABIValue: + """Represents an ABI value stored in a box. + + :ivar name: The name of the box + :ivar value: The ABI value stored in the box + """ + name: BoxName value: ABIValue diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 9144aaea..b6515ef0 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -92,6 +92,7 @@ "AppSourceMaps", "BaseAppClientMethodCallParams", "FundAppAccountParams", + "get_constant_block_offset", ] # TEAL opcodes for constant blocks @@ -107,11 +108,10 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 """Calculate the offset after constant blocks in TEAL program. - Args: - program: The compiled TEAL program bytes + Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations. - Returns: - The maximum offset after bytecblock/intcblock operations + :param program: The compiled TEAL program as bytes + :return: The maximum offset position after any constant block operations """ bytes_list = list(program) program_size = len(bytes_list) @@ -168,6 +168,16 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 @dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: + """Result of compiling an application's TEAL code. + + Contains the compiled approval and clear state programs along with optional compilation artifacts. + + :ivar approval_program: The compiled approval program bytes + :ivar clear_state_program: The compiled clear state program bytes + :ivar compiled_approval: Optional compilation artifacts for approval program + :ivar compiled_clear: Optional compilation artifacts for clear state program + """ + approval_program: bytes clear_state_program: bytes compiled_approval: CompiledTeal | None = None @@ -176,6 +186,13 @@ class AppClientCompilationResult: @dataclass(kw_only=True, frozen=True) class AppClientCompilationParams: + """Parameters for compiling an application's TEAL code. + + :ivar deploy_time_params: Optional template parameters to use during compilation + :ivar updatable: Optional flag indicating if app should be updatable + :ivar deletable: Optional flag indicating if app should be deletable + """ + deploy_time_params: TealTemplateParams | None = None updatable: bool | None = None deletable: bool | None = None @@ -183,6 +200,27 @@ class AppClientCompilationParams: @dataclass(kw_only=True) class FundAppAccountParams: + """Parameters for funding an application's account. + + :ivar sender: Optional sender address + :ivar signer: Optional transaction signer + :ivar rekey_to: Optional address to rekey to + :ivar note: Optional transaction note + :ivar lease: Optional lease + :ivar static_fee: Optional static fee + :ivar extra_fee: Optional extra fee + :ivar max_fee: Optional maximum fee + :ivar validity_window: Optional validity window in rounds + :ivar first_valid_round: Optional first valid round + :ivar last_valid_round: Optional last valid round + :ivar amount: Amount to fund + :ivar close_remainder_to: Optional address to close remainder to + :ivar max_rounds_to_wait: Optional maximum rounds to wait + :ivar suppress_log: Optional flag to suppress logging + :ivar populate_app_call_resources: Optional flag to populate app call resources + :ivar on_complete: Optional on complete action + """ + sender: str | None = None signer: TransactionSigner | None = None rekey_to: str | None = None @@ -204,16 +242,30 @@ class FundAppAccountParams: @dataclass(kw_only=True) class AppClientCallParams: - method: str | None = None # If calling ABI method, name or signature - args: list | None = None # Arguments to pass to the method - boxes: list | None = None # Box references to load - accounts: list[str] | None = None # Account addresses to load - apps: list[int] | None = None # App IDs to load - assets: list[int] | None = None # Asset IDs to load - lease: (str | bytes) | None = None # Optional lease - sender: str | None = None # Optional sender account - note: (bytes | dict | str) | None = None # Transaction note - send_params: dict | None = None # Parameters to control transaction sending + """Parameters for calling an application. + + :ivar method: Optional ABI method name or signature + :ivar args: Optional arguments to pass to method + :ivar boxes: Optional box references to load + :ivar accounts: Optional account addresses to load + :ivar apps: Optional app IDs to load + :ivar assets: Optional asset IDs to load + :ivar lease: Optional lease + :ivar sender: Optional sender address + :ivar note: Optional transaction note + :ivar send_params: Optional parameters to control transaction sending + """ + + method: str | None = None + args: list | None = None + boxes: list | None = None + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + lease: (str | bytes) | None = None + sender: str | None = None + note: (bytes | dict | str) | None = None + send_params: dict | None = None ArgsT = TypeVar("ArgsT") @@ -222,6 +274,28 @@ class AppClientCallParams: @dataclass(kw_only=True, frozen=True) class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]): + """Base parameters for application method calls. + + :ivar method: Method to call + :ivar args: Optional arguments to pass to method + :ivar account_references: Optional account references + :ivar app_references: Optional application references + :ivar asset_references: Optional asset references + :ivar box_references: Optional box references + :ivar extra_fee: Optional extra fee + :ivar first_valid_round: Optional first valid round + :ivar lease: Optional lease + :ivar max_fee: Optional maximum fee + :ivar note: Optional note + :ivar rekey_to: Optional rekey to address + :ivar sender: Optional sender address + :ivar signer: Optional transaction signer + :ivar static_fee: Optional static fee + :ivar validity_window: Optional validity window + :ivar last_valid_round: Optional last valid round + :ivar on_complete: Optional on complete action + """ + method: MethodT args: ArgsT | None = None account_references: list[str] | None = None @@ -249,28 +323,48 @@ class AppClientMethodCallParams( str, ] ): - pass + """Parameters for application method calls.""" @dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithCompilationParams(AppClientMethodCallParams, AppClientCompilationParams): - """Combined parameters for method calls with compilation""" + """Combined parameters for method calls with compilation.""" @dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithSendParams(AppClientMethodCallParams, SendParams): - """Combined parameters for method calls with send options""" + """Combined parameters for method calls with send options.""" @dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithCompilationAndSendParams( AppClientMethodCallParams, AppClientCompilationParams, SendParams ): - """Combined parameters for method calls with compilation and send options""" + """Combined parameters for method calls with compilation and send options.""" @dataclass(kw_only=True, frozen=True) class AppClientBareCallParams: + """Parameters for bare application calls. + + :ivar signer: Optional transaction signer + :ivar rekey_to: Optional rekey to address + :ivar lease: Optional lease + :ivar static_fee: Optional static fee + :ivar extra_fee: Optional extra fee + :ivar max_fee: Optional maximum fee + :ivar validity_window: Optional validity window + :ivar first_valid_round: Optional first valid round + :ivar last_valid_round: Optional last valid round + :ivar sender: Optional sender address + :ivar note: Optional note + :ivar args: Optional arguments + :ivar account_references: Optional account references + :ivar app_references: Optional application references + :ivar asset_references: Optional asset references + :ivar box_references: Optional box references + """ + signer: TransactionSigner | None = None rekey_to: str | None = None lease: bytes | None = None @@ -291,38 +385,49 @@ class AppClientBareCallParams: @dataclass(frozen=True) class AppClientCreateSchema: + """Schema for application creation. + + :ivar extra_program_pages: Optional number of extra program pages + :ivar schema: Optional application creation schema + """ + extra_program_pages: int | None = None schema: AppCreateSchema | None = None @dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCompilationParams(AppClientBareCallParams, AppClientCompilationParams): - """Combined parameters for bare calls with compilation""" + """Combined parameters for bare calls with compilation.""" @dataclass(kw_only=True, frozen=True) class AppClientBareCallWithSendParams(AppClientBareCallParams, SendParams): - """Combined parameters for bare calls with send options""" + """Combined parameters for bare calls with send options.""" @dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, AppClientCompilationParams, SendParams): - """Combined parameters for bare calls with compilation and send options""" + """Combined parameters for bare calls with compilation and send options.""" @dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams): + """Parameters for bare calls with on complete action. + + :ivar on_complete: Optional on complete action + """ + on_complete: algosdk.transaction.OnComplete | None = None @dataclass(frozen=True) class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallWithCallOnCompleteParams): - pass + """Parameters for creating application with bare call.""" @dataclass(frozen=True) class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams): - pass + """Parameters for creating application with method call.""" class _AppClientStateMethods: @@ -409,8 +514,6 @@ def box(self) -> _AppClientBoxMethods: return self._get_box_methods() def _get_box_methods(self) -> _AppClientBoxMethods: - """Get methods to access box storage for the current app.""" - def get_all() -> dict[str, Any]: """Returns all single-key box values in a dict keyed by the key name.""" return {key: get_value(key) for key in self._app_spec.state.keys.box} @@ -418,8 +521,8 @@ def get_all() -> dict[str, Any]: def get_value(name: str) -> ABIValue | None: """Returns a single box value for the current app with the value a decoded ABI value. - Args: - name: The name of the box value to retrieve + :param name: The name of the box value to retrieve + :return: The decoded ABI value from the box storage, or None if not found """ metadata = self._app_spec.state.keys.box[name] value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(metadata.key)) @@ -428,10 +531,12 @@ def get_value(name: str) -> ABIValue | None: def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 """Get a value from a box map. - Args: - map_name: The name of the map to read from - key: The key within the map (without any map prefix) as either bytes or a value - that will be converted to bytes by encoding it using the specified ABI key type + Retrieves a value from a box map storage using the provided map name and key. + + :param map_name: The name of the map to read from + :param key: The key within the map (without any map prefix) as either bytes or a value + that will be converted to bytes by encoding it using the specified ABI key type + :return: The decoded value from the box map storage """ metadata = self._app_spec.state.maps.box[map_name] prefix = base64.b64decode(metadata.prefix or "") @@ -443,8 +548,11 @@ def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 def get_map(map_name: str) -> dict[str, ABIValue]: """Get all key-value pairs from a box map. - Args: - map_name: The name of the map to read from + Retrieves all key-value pairs stored in a box map for the current app. + + :param map_name: The name of the map to read from + :return: A dictionary mapping string keys to their corresponding ABI-decoded values + :raises ValueError: If there is an error decoding any key or value in the map """ metadata = self._app_spec.state.maps.box[map_name] prefix = base64.b64decode(metadata.prefix or "") @@ -568,15 +676,6 @@ def __init__(self, client: AppClient) -> None: def _get_bare_params( self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete ) -> dict[str, Any]: - """Get bare parameters for application calls. - - Args: - params: The parameters to process - on_complete: The OnComplete value for the transaction - - Returns: - The processed parameters with defaults filled in - """ params = params or {} sender = self._client._get_sender(params.get("sender")) return { @@ -588,36 +687,66 @@ def _get_bare_params( } def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> AppUpdateParams: + """Create parameters for updating an application. + + :param params: Optional compilation and send parameters, defaults to None + :return: Parameters for updating the application + """ call_params: AppUpdateParams = AppUpdateParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.UpdateApplicationOC) ) return call_params def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + """Create parameters for opting into an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for opting into the application + """ call_params: AppCallParams = AppCallParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.OptInOC) ) return call_params def delete(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + """Create parameters for deleting an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for deleting the application + """ call_params: AppCallParams = AppCallParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.DeleteApplicationOC) ) return call_params def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + """Create parameters for clearing application state. + + :param params: Optional send parameters, defaults to None + :return: Parameters for clearing application state + """ call_params: AppCallParams = AppCallParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.ClearStateOC) ) return call_params def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + """Create parameters for closing out of an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for closing out of the application + """ call_params: AppCallParams = AppCallParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.CloseOutOC) ) return call_params def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> AppCallParams: + """Create parameters for calling an application. + + :param params: Optional call parameters with on complete action, defaults to None + :return: Parameters for calling the application + """ call_params: AppCallParams = AppCallParams( **self._get_bare_params(params.__dict__ if params else {}, OnComplete.NoOpOC) ) @@ -637,6 +766,12 @@ def bare(self) -> _BareParamsBuilder: return self._bare_params_accessor def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: + """Create parameters for funding an application account. + + :param params: Parameters for funding the application account + :return: Parameters for sending a payment transaction to fund the application account + """ + def random_note() -> bytes: return base64.b64encode(os.urandom(16)) @@ -658,14 +793,29 @@ def random_note() -> bytes: ) def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: + """Create parameters for opting into an application. + + :param params: Parameters for the opt-in call + :return: Parameters for opting into the application + """ input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) return AppCallMethodCallParams(**input_params) def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: + """Create parameters for calling an application method. + + :param params: Parameters for the method call + :return: Parameters for calling the application method + """ input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) return AppCallMethodCallParams(**input_params) def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: + """Create parameters for deleting an application. + + :param params: Parameters for the delete call + :return: Parameters for deleting the application + """ input_params = self._get_abi_params( params.__dict__, on_complete=algosdk.transaction.OnComplete.DeleteApplicationOC ) @@ -674,6 +824,11 @@ def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams def update( self, params: AppClientMethodCallParams | AppClientMethodCallWithCompilationAndSendParams ) -> AppUpdateMethodCallParams: + """Create parameters for updating an application. + + :param params: Parameters for the update call, optionally including compilation parameters + :return: Parameters for updating the application + """ compile_params = ( self._client.compile( app_spec=self._client.app_spec, @@ -696,6 +851,11 @@ def update( return AppUpdateMethodCallParams(**filtered_input_params) def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: + """Create parameters for closing out of an application. + + :param params: Parameters for the close-out call + :return: Parameters for closing out of the application + """ input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.CloseOutOC) return AppCallMethodCallParams(**input_params) @@ -724,31 +884,73 @@ def __init__(self, client: AppClient) -> None: self._algorand = client._algorand def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> Transaction: + """Create a transaction to update an application. + + Creates a transaction that will update an existing application with new approval and clear state programs. + + :param params: Parameters for the update call including compilation and transaction options, defaults to None + :return: The constructed application update transaction + """ return self._algorand.create_transaction.app_update( self._client.params.bare.update(params or AppClientBareCallWithCompilationAndSendParams()) ) def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + """Create a transaction to opt into an application. + + Creates a transaction that will opt the sender account into using this application. + + :param params: Parameters for the opt-in call including transaction options, defaults to None + :return: The constructed opt-in transaction + """ return self._algorand.create_transaction.app_call( self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) ) def delete(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + """Create a transaction to delete an application. + + Creates a transaction that will delete this application from the blockchain. + + :param params: Parameters for the delete call including transaction options, defaults to None + :return: The constructed delete transaction + """ return self._algorand.create_transaction.app_call( self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) ) def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + """Create a transaction to clear application state. + + Creates a transaction that will clear the sender's local state for this application. + + :param params: Parameters for the clear state call including transaction options, defaults to None + :return: The constructed clear state transaction + """ return self._algorand.create_transaction.app_call( self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) ) def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + """Create a transaction to close out of an application. + + Creates a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including transaction options, defaults to None + :return: The constructed close out transaction + """ return self._algorand.create_transaction.app_call( self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) ) def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> Transaction: + """Create a transaction to call an application. + + Creates a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including on complete action, defaults to None + :return: The constructed application call transaction + """ return self._algorand.create_transaction.app_call( self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) ) @@ -767,21 +969,63 @@ def bare(self) -> _AppClientBareCallCreateTransactionMethods: return self._bare_create_transaction_methods def fund_app_account(self, params: FundAppAccountParams) -> Transaction: + """Create a transaction to fund an application account. + + Creates a payment transaction to fund the application account with the specified parameters. + + :param params: Parameters for funding the application account including amount and transaction options + :return: The constructed payment transaction + """ return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params)) def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to opt into an application. + + Creates a transaction that will opt the sender into this application with the specified parameters. + + :param params: Parameters for the opt-in call including method arguments and transaction options + :return: The constructed opt-in transaction(s) + """ return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params)) def update(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to update an application. + + Creates a transaction that will update this application with new approval and clear state programs. + + :param params: Parameters for the update call including method arguments and transaction options + :return: The constructed update transaction(s) + """ return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params)) def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to delete an application. + + Creates a transaction that will delete this application. + + :param params: Parameters for the delete call including method arguments and transaction options + :return: The constructed delete transaction(s) + """ return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params)) def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to close out of an application. + + Creates a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including method arguments and transaction options + :return: The constructed close out transaction(s) + """ return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params)) def call(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to call an application. + + Creates a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including method arguments and transaction options + :return: The constructed application call transaction(s) + """ return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params)) @@ -798,15 +1042,12 @@ def update( ) -> SendAppTransactionResult[ABIReturn]: """Send an application update transaction. - Args: - params: The parameters for the update call - compilation: Optional compilation parameters - max_rounds_to_wait: The maximum number of rounds to wait for confirmation - suppress_log: Whether to suppress log output - populate_app_call_resources: Whether to populate app call resources + Sends a transaction to update an existing application with new approval and clear state programs. - Returns: - The result of sending the transaction + :param params: The parameters for the update call, including optional compilation parameters, + deploy time parameters, and transaction configuration + :return: The result of sending the transaction, including compilation artifacts and ABI return + value if applicable """ params = params or AppClientBareCallWithCompilationAndSendParams() compiled = self._client.compile_app(params.deploy_time_params, params.updatable, params.deletable) @@ -820,6 +1061,13 @@ def update( ) def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + """Send an application opt-in transaction. + + Creates and sends a transaction that will opt the sender's account into this application. + + :param params: Parameters for the opt-in call including transaction options, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) @@ -827,6 +1075,13 @@ def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> SendA ) def delete(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + """Send an application delete transaction. + + Creates and sends a transaction that will delete this application. + + :param params: Parameters for the delete call including transaction options, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) @@ -834,6 +1089,13 @@ def delete(self, params: AppClientBareCallWithSendParams | None = None) -> SendA ) def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + """Send an application clear state transaction. + + Creates and sends a transaction that will clear the sender's local state for this application. + + :param params: Parameters for the clear state call including transaction options, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) @@ -841,6 +1103,13 @@ def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> ) def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + """Send an application close out transaction. + + Creates and sends a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including transaction options, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) @@ -850,6 +1119,13 @@ def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Se def call( self, params: AppClientBareCallWithCallOnCompleteParams | None = None ) -> SendAppTransactionResult[ABIReturn]: + """Send an application call transaction. + + Creates and sends a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including transaction options, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) @@ -867,14 +1143,32 @@ def __init__(self, client: AppClient) -> None: @property def bare(self) -> _AppClientBareSendAccessor: + """Get accessor for bare application calls. + + :return: Accessor for making bare application calls without ABI encoding + """ return self._bare_send_accessor def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + """Send funds to the application account. + + Creates and sends a payment transaction to fund the application account. + + :param params: Parameters for funding the app account including amount and transaction options + :return: The result of sending the payment transaction + """ return self._client._handle_call_errors( # type: ignore[no-any-return] lambda: self._algorand.send.payment(self._client.params.fund_app_account(params)) ) def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application opt-in transaction. + + Creates and sends a transaction that will opt the sender into this application. + + :param params: Parameters for the opt-in call including method and transaction options + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)), @@ -883,6 +1177,13 @@ def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactio ) def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application delete transaction. + + Creates and sends a transaction that will delete this application. + + :param params: Parameters for the delete call including method and transaction options + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)), @@ -893,6 +1194,13 @@ def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactio def update( self, params: AppClientMethodCallWithCompilationAndSendParams ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]: + """Send an application update transaction. + + Creates and sends a transaction that will update this application's program. + + :param params: Parameters for the update call including method, compilation and transaction options + :return: The result of sending the transaction, including ABI return value if applicable + """ result = self._client._handle_call_errors( lambda: self._client._process_method_call_return( lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)), @@ -903,6 +1211,13 @@ def update( return result def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application close out transaction. + + Creates and sends a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including method and transaction options + :return: The result of sending the transaction, including ABI return value if applicable + """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)), @@ -911,6 +1226,14 @@ def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransac ) def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application call transaction. + + Creates and sends a transaction that will call this application with the specified parameters. + For read-only calls, simulates the transaction instead of sending it. + + :param params: Parameters for the application call including method and transaction options + :return: The result of sending or simulating the transaction, including ABI return value if applicable + """ is_read_only_call = ( params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None ) and self._app_spec.get_arc56_method(params.method).readonly @@ -968,6 +1291,14 @@ class AppClientParams: class AppClient: + """A client for interacting with an Algorand smart contract application. + + Provides a high-level interface for interacting with Algorand smart contracts, including + methods for calling application methods, managing state, and handling transactions. + + :param params: Parameters for creating the app client + """ + def __init__(self, params: AppClientParams) -> None: self._app_id = params.app_id self._app_spec = self.normalise_app_spec(params.app_spec) @@ -985,42 +1316,84 @@ def __init__(self, params: AppClientParams) -> None: @property def algorand(self) -> AlgorandClient: + """Get the Algorand client instance. + + :return: The Algorand client used by this app client + """ return self._algorand @property def app_id(self) -> int: + """Get the application ID. + + :return: The ID of the Algorand application + """ return self._app_id @property def app_address(self) -> str: + """Get the application's Algorand address. + + :return: The Algorand address associated with this application + """ return self._app_address @property def app_name(self) -> str: + """Get the application name. + + :return: The name of the application + """ return self._app_name @property def app_spec(self) -> Arc56Contract: + """Get the application specification. + + :return: The ARC-56 contract specification for this application + """ return self._app_spec @property def state(self) -> _StateAccessor: + """Get the state accessor. + + :return: The state accessor for this application + """ return self._state_accessor @property def params(self) -> _MethodParamsBuilder: + """Get the method parameters builder. + + :return: The method parameters builder for this application + """ return self._params_accessor @property def send(self) -> _TransactionSender: + """Get the transaction sender. + + :return: The transaction sender for this application + """ return self._send_accessor @property def create_transaction(self) -> _TransactionCreator: + """Get the transaction creator. + + :return: The transaction creator for this application + """ return self._create_transaction_accessor @staticmethod def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Contract: + """Normalize an application specification to ARC-56 format. + + :param app_spec: The application specification to normalize + :return: The normalized ARC-56 contract specification + :raises ValueError: If the app spec format is invalid + """ if isinstance(app_spec, str): spec_dict = json.loads(app_spec) spec = Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict @@ -1047,6 +1420,18 @@ def from_network( approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: + """Create an AppClient instance from network information. + + :param app_spec: The application specification + :param algorand: The Algorand client instance + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :return: A new AppClient instance + :raises Exception: If no app ID is found for the network + """ network = algorand.client.network() app_spec = AppClient.normalise_app_spec(app_spec) network_names = [network.genesis_hash] @@ -1092,6 +1477,21 @@ def from_creator_and_name( ignore_cache: bool | None = None, app_lookup_cache: AppLookup | None = None, ) -> AppClient: + """Create an AppClient instance from creator address and application name. + + :param creator_address: The address of the application creator + :param app_name: The name of the application + :param app_spec: The application specification + :param algorand: The Algorand client instance + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :return: A new AppClient instance + :raises ValueError: If the app is not found for the creator and name + """ app_spec_ = AppClient.normalise_app_spec(app_spec) app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name( creator_address=creator_address, ignore_cache=ignore_cache or False @@ -1121,6 +1521,17 @@ def compile( updatable: bool | None = None, deletable: bool | None = None, ) -> AppClientCompilationResult: + """Compile the application's TEAL code. + + :param app_spec: The application specification + :param app_manager: The application manager instance + :param deploy_time_params: Optional deployment time parameters + :param updatable: Optional flag indicating if app is updatable + :param deletable: Optional flag indicating if app is deletable + :return: The compilation result + :raises ValueError: If attempting to compile without source or byte code + """ + def is_base64(s: str) -> bool: try: return base64.b64encode(base64.b64decode(s)).decode() == s @@ -1183,7 +1594,6 @@ def _expose_logic_error_static( # noqa: C901 approval_source_info: ProgramSourceInfo | None = None, clear_source_info: ProgramSourceInfo | None = None, ) -> Exception: - """Takes an error that may include a logic error and re-exposes it with source info.""" source_map = clear_source_map if is_clear_state_program else approval_source_map error_details = parse_logic_error(str(e)) @@ -1265,13 +1675,19 @@ def get_line_for_pc(input_pc: int) -> int | None: return e - # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' def compile_app( self, deploy_time_params: TealTemplateParams | None = None, updatable: bool | None = None, deletable: bool | None = None, ) -> AppClientCompilationResult: + """Compile the application's TEAL code. + + :param deploy_time_params: Optional deployment time parameters + :param updatable: Optional flag indicating if app is updatable + :param deletable: Optional flag indicating if app is deletable + :return: The compilation result + """ result = AppClient.compile(self._app_spec, self._algorand.app, deploy_time_params, updatable, deletable) if result.compiled_approval: @@ -1289,7 +1705,15 @@ def clone( approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] ) -> AppClient: - """Create a cloned AppClient instance with optionally overridden parameters.""" + """Create a cloned AppClient instance with optionally overridden parameters. + + :param app_name: Optional new application name + :param default_sender: Optional new default sender + :param default_signer: Optional new default signer + :param approval_source_map: Optional new approval source map + :param clear_source_map: Optional new clear source map + :return: A new AppClient instance with the specified parameters + """ return AppClient( AppClientParams( app_id=self._app_id, @@ -1306,6 +1730,11 @@ def clone( ) def export_source_maps(self) -> AppSourceMaps: + """Export the application's source maps. + + :return: The application's source maps + :raises ValueError: If source maps haven't been loaded + """ if not self._approval_source_map or not self._clear_source_map: raise ValueError( "Unable to export source maps; they haven't been loaded into this client - " @@ -1318,6 +1747,11 @@ def export_source_maps(self) -> AppSourceMaps: ) def import_source_maps(self, source_maps: AppSourceMaps) -> None: + """Import source maps for the application. + + :param source_maps: The source maps to import + :raises ValueError: If source maps are invalid or missing + """ if not source_maps.approval_source_map: raise ValueError("Approval source map is required") if not source_maps.clear_source_map: @@ -1344,21 +1778,50 @@ def import_source_maps(self, source_maps: AppSourceMaps) -> None: ) def get_local_state(self, address: str) -> dict[str, AppState]: + """Get local state for an account. + + :param address: The account address + :return: The account's local state for this application + """ return self._state_accessor.get_local_state(address) def get_global_state(self) -> dict[str, AppState]: + """Get the application's global state. + + :return: The application's global state + """ return self._state_accessor.get_global_state() def get_box_names(self) -> list[BoxName]: + """Get all box names for the application. + + :return: List of box names + """ return self._algorand.app.get_box_names(self._app_id) def get_box_value(self, name: BoxIdentifier) -> bytes: + """Get the value of a box. + + :param name: The box identifier + :return: The box value as bytes + """ return self._algorand.app.get_box_value(self._app_id, name) def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + """Get a box value decoded according to an ABI type. + + :param name: The box identifier + :param abi_type: The ABI type to decode as + :return: The decoded box value + """ return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type) def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]: + """Get values for multiple boxes. + + :param filter_func: Optional function to filter box names + :return: List of box values + """ names = [n for n in self.get_box_names() if not filter_func or filter_func(n)] values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names]) return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)] @@ -1366,35 +1829,31 @@ def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) - def get_box_values_from_abi_type( self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None ) -> list[BoxABIValue]: - # Get box names and apply filter if provided + """Get multiple box values decoded according to an ABI type. + + :param abi_type: The ABI type to decode as + :param filter_func: Optional function to filter box names + :return: List of decoded box values + """ names = self.get_box_names() if filter_func: names = [name for name in names if filter_func(name)] - # Get values for filtered names and decode them values = self._algorand.app.get_box_values_from_abi_type( self.app_id, [name.name_raw for name in names], abi_type ) - # Return list of BoxABIValue objects return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: - return self.send.fund_app_account(params) - - def _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT001, FBT002 - """Takes an error that may include a logic error from a call to the current app and re-exposes the - error to include source code information via the source map and ARC-56 spec. - - Args: - e: The error to parse - is_clear_state_program: Whether the code was running the clear state program (defaults to approval program) + """Fund the application's account. - Returns: - The new error, or if there was no logic error or source map then the wrapped error with source details + :param params: The funding parameters + :return: The transaction result """ + return self.send.fund_app_account(params) - # Get source info based on program type + def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception: source_info = None if hasattr(self._app_spec, "source_info") and self._app_spec.source_info: source_info = ( @@ -1405,7 +1864,6 @@ def _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False program: bytes | None = None if pc_offset_method == "cblocks": - # TODO: Cache this if we deploy the app and it's not updateable app_info = self._algorand.app.get_by_id(self.app_id) program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program @@ -1421,7 +1879,6 @@ def _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False ) def _handle_call_errors(self, call: Callable[[], T]) -> T: - """Make the given call and catch any errors, augmenting with debugging information before re-throwing.""" try: return call() except Exception as e: @@ -1438,15 +1895,6 @@ def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> T return signer or self._default_signer if not sender or sender == self._default_sender else None def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: - """Get bare parameters for application calls. - - Args: - params: The parameters to process - on_complete: The OnComplete value for the transaction - - Returns: - The processed parameters with defaults filled in - """ sender = self._get_sender(params.get("sender")) return { **params, @@ -1463,28 +1911,13 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 args: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, sender: str, ) -> list[Any]: - """Get ABI args with default values filled in. - - Args: - method_name_or_signature: Method name or ABI signature - args: Optional list of argument values - sender: Sender address - - Returns: - List of argument values with defaults filled in - - Raises: - ValueError: If required argument is missing or default value lookup fails - """ method = self._app_spec.get_arc56_method(method_name_or_signature) result = [] for i, method_arg in enumerate(method.args): - # Get provided arg value if any arg_value = args[i] if args and i < len(args) else None if arg_value is not None: - # Convert struct to tuple if needed if method_arg.struct and isinstance(arg_value, dict): arg_value = get_abi_tuple_from_abi_struct( arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs @@ -1492,7 +1925,6 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 result.append(arg_value) continue - # Handle default value if arg not provided default_value = method_arg.default_value if default_value: match default_value.source: @@ -1502,7 +1934,6 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) case "method": - # Get method return value default_method = self._app_spec.get_arc56_method(default_value.data) empty_args = [None] * len(default_method.args) call_result = self.send.call( @@ -1517,7 +1948,6 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 raise ValueError("Default value method call did not return a value") if isinstance(call_result.abi_return, dict): - # Convert struct return value to tuple result.append( get_abi_tuple_from_abi_struct( call_result.abi_return, @@ -1529,7 +1959,6 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 result.append(call_result.abi_return) case "local" | "global": - # Get state value state = ( self.get_global_state() if default_value.source == "global" @@ -1549,14 +1978,12 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 result.append(value.value) case "box": - # Get box value box_name = base64.b64decode(default_value.data) box_value = self._algorand.app.get_box_value(self._app_id, box_name) value_type = default_value.type or method_arg.type result.append(get_abi_decoded_value(box_value, value_type, self._app_spec.structs)) elif not algosdk.abi.is_abi_transaction_type(method_arg.type): - # Error if required non-txn arg missing raise ValueError( f"No value provided for required argument " f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}" diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index fcbdfe77..54269963 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -46,9 +46,6 @@ def _last_token_base64(line: str, idx: int) -> bool: def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. - Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING - Returns None if not found""" if end < 0: end = len(line) @@ -58,8 +55,8 @@ def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) - if token_idx is None: break trailing_idx = token_idx + len(token) - if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start - trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end + if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( + trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) ): return token_idx idx = trailing_idx @@ -67,9 +64,6 @@ def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) - def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. - Returns None if not found""" - if end < 0: end = len(line) idx = start @@ -77,22 +71,15 @@ def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) while idx < end: current_char = line[idx] match current_char: - # enter base64 case " " | "(" if not in_quotes and _last_token_base64(line, idx): in_base64 = True - # exit base64 case " " | ")" if not in_quotes and in_base64: in_base64 = False - # escaped char case "\\" if in_quotes: - # skip next character idx += 1 - # quote boundary case '"': in_quotes = not in_quotes - # can test for match case _ if not in_quotes and not in_base64 and line.startswith(token, idx): - # only match if not in quotes and string matches return idx idx += 1 return None @@ -126,11 +113,25 @@ def _replace_template_variable(program_lines: list[str], template_variable: str, class AppManager: + """A manager class for interacting with Algorand applications. + + Provides functionality for compiling TEAL code, managing application state, + and interacting with application boxes. + + :param algod_client: The Algorand client instance to use for interacting with the network + """ + def __init__(self, algod_client: algod.AlgodClient): self._algod = algod_client self._compilation_results: dict[str, CompiledTeal] = {} def compile_teal(self, teal_code: str) -> CompiledTeal: + """Compile TEAL source code. + + :param teal_code: The TEAL source code to compile + :return: The compiled TEAL code and associated metadata + """ + if teal_code in self._compilation_results: return self._compilation_results[teal_code] @@ -151,6 +152,14 @@ def compile_teal_template( template_params: TealTemplateParams | None = None, deployment_metadata: Mapping[str, bool | None] | None = None, ) -> CompiledTeal: + """Compile a TEAL template with parameters. + + :param teal_template_code: The TEAL template code to compile + :param template_params: Parameters to substitute in the template + :param deployment_metadata: Deployment control parameters + :return: The compiled TEAL code and associated metadata + """ + teal_code = AppManager.strip_teal_comments(teal_template_code) teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) @@ -160,9 +169,21 @@ def compile_teal_template( return self.compile_teal(teal_code) def get_compilation_result(self, teal_code: str) -> CompiledTeal | None: + """Get cached compilation result for TEAL code if available. + + :param teal_code: The TEAL source code + :return: The cached compilation result if available, None otherwise + """ + return self._compilation_results.get(teal_code) def get_by_id(self, app_id: int) -> AppInformation: + """Get information about an application by ID. + + :param app_id: The application ID + :return: Information about the application + """ + app = self._algod.application_info(app_id) assert isinstance(app, dict) app_params = app["params"] @@ -182,9 +203,23 @@ def get_by_id(self, app_id: int) -> AppInformation: ) def get_global_state(self, app_id: int) -> dict[str, AppState]: + """Get the global state of an application. + + :param app_id: The application ID + :return: The application's global state + """ + return self.get_by_id(app_id).global_state def get_local_state(self, app_id: int, address: str) -> dict[str, AppState]: + """Get the local state for an account in an application. + + :param app_id: The application ID + :param address: The account address + :return: The account's local state for the application + :raises ValueError: If local state is not found + """ + app_info = self._algod.account_application_info(address, app_id) assert isinstance(app_info, dict) if not app_info.get("app-local-state", {}).get("key-value"): @@ -192,6 +227,12 @@ def get_local_state(self, app_id: int, address: str) -> dict[str, AppState]: return self.decode_app_state(app_info["app-local-state"]["key-value"]) def get_box_names(self, app_id: int) -> list[BoxName]: + """Get names of all boxes for an application. + + :param app_id: The application ID + :return: List of box names + """ + box_result = self._algod.application_boxes(app_id) assert isinstance(box_result, dict) return [ @@ -204,15 +245,38 @@ def get_box_names(self, app_id: int) -> list[BoxName]: ] def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: + """Get the value stored in a box. + + :param app_id: The application ID + :param box_name: The box identifier + :return: The box value as bytes + """ + name = AppManager.get_box_reference(box_name)[1] box_result = self._algod.application_box_by_name(app_id, name) assert isinstance(box_result, dict) return base64.b64decode(box_result["value"]) def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: + """Get values for multiple boxes. + + :param app_id: The application ID + :param box_names: List of box identifiers + :return: List of box values as bytes + """ + return [self.get_box_value(app_id, box_name) for box_name in box_names] def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + """Get and decode a box value using an ABI type. + + :param app_id: The application ID + :param box_name: The box identifier + :param abi_type: The ABI type to decode with + :return: The decoded box value + :raises ValueError: If decoding fails + """ + value = self.get_box_value(app_id, box_name) try: parse_to_tuple = isinstance(abi_type, algosdk.abi.TupleType) @@ -224,10 +288,25 @@ def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_ def get_box_values_from_abi_type( self, app_id: int, box_names: list[BoxIdentifier], abi_type: ABIType ) -> list[ABIValue]: + """Get and decode multiple box values using an ABI type. + + :param app_id: The application ID + :param box_names: List of box identifiers + :param abi_type: The ABI type to decode with + :return: List of decoded box values + """ + return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] @staticmethod def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes]: + """Get standardized box reference from various identifier types. + + :param box_id: The box identifier + :return: Tuple of (app_id, box_name_bytes) + :raises ValueError: If box identifier type is invalid + """ + if isinstance(box_id, (BoxReference | AlgosdkBoxReference)): return box_id.app_index, box_id.name @@ -249,15 +328,20 @@ def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes] def get_abi_return( confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None ) -> ABIReturn | None: - """Get the ABI return value from a transaction confirmation.""" + """Get the ABI return value from a transaction confirmation. + + :param confirmation: The transaction confirmation + :param method: The ABI method + :return: The parsed ABI return value, or None if not available + """ + if not method: return None - # Use the SDK's built-in ABI result parsing atc = algosdk.atomic_transaction_composer.AtomicTransactionComposer() abi_result = atc.parse_result( - method, # Map of transaction index to ABI method - "dummy_txn", # List of transaction info + method, + "dummy_txn", confirmation, # type: ignore[arg-type] ) @@ -268,6 +352,13 @@ def get_abi_return( @staticmethod def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: + """Decode application state from raw format. + + :param state: The raw application state + :return: Decoded application state + :raises ValueError: If unknown state data type is encountered + """ + state_values: dict[str, AppState] = {} def decode_bytes_to_str(value: bytes) -> str: @@ -310,6 +401,14 @@ def decode_bytes_to_str(value: bytes) -> str: @staticmethod def replace_template_variables(program: str, template_values: TealTemplateParams) -> str: + """Replace template variables in TEAL code. + + :param program: The TEAL program code + :param template_values: Template variable values to substitute + :return: TEAL code with substituted values + :raises ValueError: If template value type is unexpected + """ + program_lines = program.splitlines() for template_variable_name, template_value in template_values.items(): match template_value: @@ -332,6 +431,14 @@ def replace_template_variables(program: str, template_values: TealTemplateParams def replace_teal_template_deploy_time_control_params( teal_template_code: str, params: Mapping[str, bool | None] ) -> str: + """Replace deploy-time control parameters in TEAL template. + + :param teal_template_code: The TEAL template code + :param params: The deploy-time control parameters + :return: TEAL code with substituted control parameters + :raises ValueError: If template variables not found in code + """ + updatable = params.get("updatable") if updatable is not None: if UPDATABLE_TEMPLATE_NAME not in teal_template_code: diff --git a/src/algokit_utils/applications/app_spec/arc32.py b/src/algokit_utils/applications/app_spec/arc32.py index 505e2b27..ff3b8f6b 100644 --- a/src/algokit_utils/applications/app_spec/arc32.py +++ b/src/algokit_utils/applications/app_spec/arc32.py @@ -187,10 +187,13 @@ def from_json(application_spec: str) -> "Arc32Contract": ) def export(self, directory: Path | str | None = None) -> None: - """write out the artifacts generated by the application to disk + """Write out the artifacts generated by the application to disk. - Args: - directory(optional): path to the directory where the artifacts should be written + Writes the approval program, clear program, contract specification and application specification + to files in the specified directory. + + :param directory: Path to the directory where the artifacts should be written. If not specified, + uses the current working directory """ if directory is None: output_dir = Path.cwd() diff --git a/src/algokit_utils/applications/app_spec/arc56.py b/src/algokit_utils/applications/app_spec/arc56.py index ffb61534..b743b27d 100644 --- a/src/algokit_utils/applications/app_spec/arc56.py +++ b/src/algokit_utils/applications/app_spec/arc56.py @@ -58,6 +58,12 @@ class _ActionType(str, Enum): @dataclass class StructField: + """Represents a field in a struct type. + + :ivar name: Name of the struct field + :ivar type: Type of the struct field, either a string or list of StructFields + """ + name: str type: list[StructField] | str @@ -69,6 +75,8 @@ def from_dict(data: dict[str, Any]) -> StructField: class CallEnum(str, Enum): + """Enum representing different call types for application transactions.""" + CLEAR_STATE = "ClearState" CLOSE_OUT = "CloseOut" DELETE_APPLICATION = "DeleteApplication" @@ -78,6 +86,8 @@ class CallEnum(str, Enum): class CreateEnum(str, Enum): + """Enum representing different create types for application transactions.""" + DELETE_APPLICATION = "DeleteApplication" NO_OP = "NoOp" OPT_IN = "OptIn" @@ -85,6 +95,12 @@ class CreateEnum(str, Enum): @dataclass class BareActions: + """Represents bare call and create actions for an application. + + :ivar call: List of allowed call actions + :ivar create: List of allowed create actions + """ + call: list[CallEnum] create: list[CreateEnum] @@ -95,6 +111,12 @@ def from_dict(data: dict[str, Any]) -> BareActions: @dataclass class ByteCode: + """Represents the approval and clear program bytecode. + + :ivar approval: Base64 encoded approval program bytecode + :ivar clear: Base64 encoded clear program bytecode + """ + approval: str clear: str @@ -104,12 +126,22 @@ def from_dict(data: dict[str, Any]) -> ByteCode: class Compiler(str, Enum): + """Enum representing different compiler types.""" + ALGOD = "algod" PUYA = "puya" @dataclass class CompilerVersion: + """Represents compiler version information. + + :ivar commit_hash: Git commit hash of the compiler + :ivar major: Major version number + :ivar minor: Minor version number + :ivar patch: Patch version number + """ + commit_hash: str | None = None major: int | None = None minor: int | None = None @@ -122,6 +154,12 @@ def from_dict(data: dict[str, Any]) -> CompilerVersion: @dataclass class CompilerInfo: + """Information about the compiler used. + + :ivar compiler: Type of compiler used + :ivar compiler_version: Version information for the compiler + """ + compiler: Compiler compiler_version: CompilerVersion @@ -133,6 +171,11 @@ def from_dict(data: dict[str, Any]) -> CompilerInfo: @dataclass class Network: + """Network-specific application information. + + :ivar app_id: Application ID on the network + """ + app_id: int @staticmethod @@ -142,6 +185,12 @@ def from_dict(data: dict[str, Any]) -> Network: @dataclass class ScratchVariables: + """Information about scratch space variables. + + :ivar slot: Scratch slot number + :ivar type: Type of the scratch variable + """ + slot: int type: str @@ -152,6 +201,12 @@ def from_dict(data: dict[str, Any]) -> ScratchVariables: @dataclass class Source: + """Source code for approval and clear programs. + + :ivar approval: Base64 encoded approval program source + :ivar clear: Base64 encoded clear program source + """ + approval: str clear: str @@ -160,9 +215,17 @@ def from_dict(data: dict[str, Any]) -> Source: return Source(**data) def get_decoded_approval(self) -> str: + """Get decoded approval program source. + + :return: Decoded approval program source code + """ return self._decode_source(self.approval) def get_decoded_clear(self) -> str: + """Get decoded clear program source. + + :return: Decoded clear program source code + """ return self._decode_source(self.clear) def _decode_source(self, b64_text: str) -> str: @@ -171,6 +234,12 @@ def _decode_source(self, b64_text: str) -> str: @dataclass class Global: + """Global state schema. + + :ivar bytes: Number of byte slices in global state + :ivar ints: Number of integers in global state + """ + bytes: int ints: int @@ -181,6 +250,12 @@ def from_dict(data: dict[str, Any]) -> Global: @dataclass class Local: + """Local state schema. + + :ivar bytes: Number of byte slices in local state + :ivar ints: Number of integers in local state + """ + bytes: int ints: int @@ -191,6 +266,12 @@ def from_dict(data: dict[str, Any]) -> Local: @dataclass class Schema: + """Application state schema. + + :ivar global_state: Global state schema + :ivar local_state: Local state schema + """ + global_state: Global # actual schema field is "global" since it's a reserved word local_state: Local # actual schema field is "local" for consistency with renamed "global" @@ -203,6 +284,12 @@ def from_dict(data: dict[str, Any]) -> Schema: @dataclass class TemplateVariables: + """Template variable information. + + :ivar type: Type of the template variable + :ivar value: Optional value of the template variable + """ + type: str value: str | None = None @@ -213,6 +300,14 @@ def from_dict(data: dict[str, Any]) -> TemplateVariables: @dataclass class EventArg: + """Event argument information. + + :ivar type: Type of the event argument + :ivar desc: Optional description of the argument + :ivar name: Optional name of the argument + :ivar struct: Optional struct type name + """ + type: str desc: str | None = None name: str | None = None @@ -225,6 +320,13 @@ def from_dict(data: dict[str, Any]) -> EventArg: @dataclass class Event: + """Event information. + + :ivar args: List of event arguments + :ivar name: Name of the event + :ivar desc: Optional description of the event + """ + args: list[EventArg] name: str desc: str | None = None @@ -237,6 +339,12 @@ def from_dict(data: dict[str, Any]) -> Event: @dataclass class Actions: + """Method actions information. + + :ivar call: Optional list of allowed call actions + :ivar create: Optional list of allowed create actions + """ + call: list[CallEnum] | None = None create: list[CreateEnum] | None = None @@ -247,6 +355,13 @@ def from_dict(data: dict[str, Any]) -> Actions: @dataclass class DefaultValue: + """Default value information for method arguments. + + :ivar data: Default value data + :ivar source: Source of the default value + :ivar type: Optional type of the default value + """ + data: str source: Literal["box", "global", "local", "literal", "method"] type: str | None = None @@ -258,6 +373,15 @@ def from_dict(data: dict[str, Any]) -> DefaultValue: @dataclass class MethodArg: + """Method argument information. + + :ivar type: Type of the argument + :ivar default_value: Optional default value + :ivar desc: Optional description + :ivar name: Optional name + :ivar struct: Optional struct type name + """ + type: str default_value: DefaultValue | None = None desc: str | None = None @@ -273,6 +397,14 @@ def from_dict(data: dict[str, Any]) -> MethodArg: @dataclass class Boxes: + """Box storage requirements. + + :ivar key: Box key + :ivar read_bytes: Number of bytes to read + :ivar write_bytes: Number of bytes to write + :ivar app: Optional application ID + """ + key: str read_bytes: int write_bytes: int @@ -285,6 +417,15 @@ def from_dict(data: dict[str, Any]) -> Boxes: @dataclass class Recommendations: + """Method execution recommendations. + + :ivar accounts: Optional list of accounts + :ivar apps: Optional list of applications + :ivar assets: Optional list of assets + :ivar boxes: Optional box storage requirements + :ivar inner_transaction_count: Optional inner transaction count + """ + accounts: list[str] | None = None apps: list[int] | None = None assets: list[int] | None = None @@ -300,6 +441,13 @@ def from_dict(data: dict[str, Any]) -> Recommendations: @dataclass class Returns: + """Method return information. + + :ivar type: Return type + :ivar desc: Optional description + :ivar struct: Optional struct type name + """ + type: str desc: str | None = None struct: str | None = None @@ -311,6 +459,18 @@ def from_dict(data: dict[str, Any]) -> Returns: @dataclass class Method: + """Method information. + + :ivar actions: Allowed actions + :ivar args: Method arguments + :ivar name: Method name + :ivar returns: Return information + :ivar desc: Optional description + :ivar events: Optional list of events + :ivar readonly: Optional readonly flag + :ivar recommendations: Optional execution recommendations + """ + actions: Actions args: list[MethodArg] name: str @@ -326,6 +486,11 @@ def __post_init__(self) -> None: self._abi_method = AlgosdkMethod.undictify(asdict(self)) def to_abi_method(self) -> AlgosdkMethod: + """Convert to ABI method. + + :raises ValueError: If underlying ABI method is not initialized + :return: ABI method + """ if self._abi_method is None: raise ValueError("Underlying core ABI method class is not initialized!") return self._abi_method @@ -343,12 +508,22 @@ def from_dict(data: dict[str, Any]) -> Method: class PcOffsetMethod(str, Enum): + """PC offset method types.""" + CBLOCKS = "cblocks" NONE = "none" @dataclass class SourceInfo: + """Source code location information. + + :ivar pc: List of program counter values + :ivar error_message: Optional error message + :ivar source: Optional source code + :ivar teal: Optional TEAL version + """ + pc: list[int] error_message: str | None = None source: str | None = None @@ -361,6 +536,14 @@ def from_dict(data: dict[str, Any]) -> SourceInfo: @dataclass class StorageKey: + """Storage key information. + + :ivar key: Storage key + :ivar key_type: Type of the key + :ivar value_type: Type of the value + :ivar desc: Optional description + """ + key: str key_type: str value_type: str @@ -373,6 +556,14 @@ def from_dict(data: dict[str, Any]) -> StorageKey: @dataclass class StorageMap: + """Storage map information. + + :ivar key_type: Type of map keys + :ivar value_type: Type of map values + :ivar desc: Optional description + :ivar prefix: Optional key prefix + """ + key_type: str value_type: str desc: str | None = None @@ -385,6 +576,13 @@ def from_dict(data: dict[str, Any]) -> StorageMap: @dataclass class Keys: + """Storage keys for different storage types. + + :ivar box: Box storage keys + :ivar global_state: Global state storage keys + :ivar local_state: Local state storage keys + """ + box: dict[str, StorageKey] global_state: dict[str, StorageKey] # actual schema field is "global" since it's a reserved word local_state: dict[str, StorageKey] # actual schema field is "local" for consistency with renamed "global" @@ -399,6 +597,13 @@ def from_dict(data: dict[str, Any]) -> Keys: @dataclass class Maps: + """Storage maps for different storage types. + + :ivar box: Box storage maps + :ivar global_state: Global state storage maps + :ivar local_state: Local state storage maps + """ + box: dict[str, StorageMap] global_state: dict[str, StorageMap] # actual schema field is "global" since it's a reserved word local_state: dict[str, StorageMap] # actual schema field is "local" for consistency with renamed "global" @@ -413,6 +618,13 @@ def from_dict(data: dict[str, Any]) -> Maps: @dataclass class State: + """Application state information. + + :ivar keys: Storage keys + :ivar maps: Storage maps + :ivar schema: State schema + """ + keys: Keys maps: Maps schema: Schema @@ -427,6 +639,12 @@ def from_dict(data: dict[str, Any]) -> State: @dataclass class ProgramSourceInfo: + """Program source information. + + :ivar pc_offset_method: PC offset method + :ivar source_info: List of source info entries + """ + pc_offset_method: PcOffsetMethod source_info: list[SourceInfo] @@ -438,6 +656,12 @@ def from_dict(data: dict[str, Any]) -> ProgramSourceInfo: @dataclass class SourceInfoModel: + """Source information for approval and clear programs. + + :ivar approval: Approval program source info + :ivar clear: Clear program source info + """ + approval: ProgramSourceInfo clear: ProgramSourceInfo @@ -660,8 +884,25 @@ def dict_factory(entries: list[tuple[str, Any]]) -> dict[str, Any]: @dataclass class Arc56Contract: - """ARC-0056 application specification + """ARC-0056 application specification. + See https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md + + :ivar arcs: List of supported ARC version numbers + :ivar bare_actions: Bare call and create actions + :ivar methods: List of contract methods + :ivar name: Contract name + :ivar state: Contract state information + :ivar structs: Contract struct definitions + :ivar byte_code: Optional bytecode for approval and clear programs + :ivar compiler_info: Optional compiler information + :ivar desc: Optional contract description + :ivar events: Optional list of contract events + :ivar networks: Optional network deployment information + :ivar scratch_variables: Optional scratch variable information + :ivar source: Optional source code + :ivar source_info: Optional source code information + :ivar template_variables: Optional template variable information """ arcs: list[int] @@ -682,6 +923,11 @@ class Arc56Contract: @staticmethod def from_dict(application_spec: dict) -> Arc56Contract: + """Create Arc56Contract from dictionary. + + :param application_spec: Dictionary containing contract specification + :return: Arc56Contract instance + """ data = _dict_keys_to_snake_case(application_spec) data["bare_actions"] = BareActions.from_dict(data["bare_actions"]) data["methods"] = [Method.from_dict(item) for item in data["methods"]] diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index bfbe79b7..c0b1e3e9 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -1,12 +1,12 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any import algosdk from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner from algosdk.v2client import algod from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AssetOptInParams, AssetOptOutParams, @@ -18,87 +18,93 @@ @dataclass(kw_only=True, frozen=True) class AccountAssetInformation: - """Information about an account's holding of a particular asset.""" + """Information about an account's holding of a particular asset. + + :ivar asset_id: The ID of the asset + :ivar balance: The amount of the asset held by the account + :ivar frozen: Whether the asset is frozen for this account + :ivar round: The round this information was retrieved at + """ asset_id: int - """The ID of the asset.""" balance: int - """The amount of the asset held by the account.""" frozen: bool - """Whether the asset is frozen for this account.""" round: int - """The round this information was retrieved at.""" @dataclass(kw_only=True, frozen=True) class AssetInformation: - """Information about an asset.""" + """Information about an Algorand Standard Asset (ASA). + + :ivar asset_id: The ID of the asset + :ivar creator: The address of the account that created the asset + :ivar total: The total amount of the smallest divisible units that were created of the asset + :ivar decimals: The amount of decimal places the asset was created with + :ivar default_frozen: Whether the asset was frozen by default for all accounts, defaults to None + :ivar manager: The address of the optional account that can manage the configuration of the asset and destroy it, + defaults to None + :ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, + defaults to None + :ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, + defaults to None + :ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, + defaults to None + :ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None + :ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None + :ivar asset_name: The optional name of the asset, defaults to None + :ivar asset_name_b64: The optional name of the asset as bytes, defaults to None + :ivar url: Optional URL where more information about the asset can be retrieved, defaults to None + :ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None + :ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, + defaults to None + """ asset_id: int - """The ID of the asset.""" creator: str - """The address of the account that created the asset.""" total: int - """The total amount of the smallest divisible units that were created of the asset.""" decimals: int - """The amount of decimal places the asset was created with.""" default_frozen: bool | None = None - """Whether the asset was frozen by default for all accounts.""" manager: str | None = None - """The address of the optional account that can manage the configuration of the asset and destroy it.""" reserve: str | None = None - """The address of the optional account that holds the reserve (uncirculated supply) units of the asset.""" freeze: str | None = None - """The address of the optional account that can be used to freeze or unfreeze holdings of this asset.""" clawback: str | None = None - """The address of the optional account that can clawback holdings of this asset from any account.""" unit_name: str | None = None - """The optional name of the unit of this asset (e.g. ticker name).""" unit_name_b64: bytes | None = None - """The optional name of the unit of this asset as bytes.""" asset_name: str | None = None - """The optional name of the asset.""" asset_name_b64: bytes | None = None - """The optional name of the asset as bytes.""" url: str | None = None - """Optional URL where more information about the asset can be retrieved.""" url_b64: bytes | None = None - """Optional URL where more information about the asset can be retrieved as bytes.""" metadata_hash: bytes | None = None - """32-byte hash of some metadata that is relevant to the asset and/or asset holders.""" @dataclass(kw_only=True, frozen=True) class BulkAssetOptInOutResult: - """Individual result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.""" + """Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. + + :ivar asset_id: The ID of the asset opted into / out of + :ivar transaction_id: The transaction ID of the resulting opt in / out + """ asset_id: int - """The ID of the asset opted into / out of""" transaction_id: str - """The transaction ID of the resulting opt in / out""" class AssetManager: - """A manager for Algorand assets.""" + """A manager for Algorand Standard Assets (ASAs). - def __init__(self, algod_client: algod.AlgodClient, new_group: Callable[[], TransactionComposer]): - """Create a new asset manager. + :param algod_client: An algod client + :param new_group: A function that creates a new TransactionComposer transaction group + """ - Args: - algod_client: An algod client - new_group: A function that creates a new `TransactionComposer` transaction group - """ + def __init__(self, algod_client: algod.AlgodClient, new_group: Callable[[], TransactionComposer]): self._algod = algod_client self._new_group = new_group def get_by_id(self, asset_id: int) -> AssetInformation: """Returns the current asset information for the asset with the given ID. - Args: - asset_id: The ID of the asset - - Returns: - The asset information + :param asset_id: The ID of the asset + :return: The asset information """ asset = self._algod.asset_info(asset_id) assert isinstance(asset, dict) @@ -128,12 +134,9 @@ def get_account_information( ) -> AccountAssetInformation: """Returns the given sender account's asset holding for a given asset. - Args: - sender: The address of the sender/account to look up - asset_id: The ID of the asset to return a holding for - - Returns: - The account asset holding information + :param sender: The address of the sender/account to look up + :param asset_id: The ID of the asset to return a holding for + :return: The account asset holding information """ address = self._get_address_from_sender(sender) info = self._algod.account_asset_info(address, asset_id) @@ -146,24 +149,44 @@ def get_account_information( round=info["round"], ) - def bulk_opt_in( + def bulk_opt_in( # noqa: PLR0913 self, account: str | Account | TransactionSigner, asset_ids: list[int], *, suppress_log: bool = False, - **transaction_params: Any, + max_rounds_to_wait: int | None = None, + populate_app_call_resources: bool | None = None, + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, ) -> list[BulkAssetOptInOutResult]: """Opt an account in to a list of Algorand Standard Assets. - Args: - account: The account to opt-in - asset_ids: The list of asset IDs to opt-in to - suppress_log: Whether to suppress logging - **transaction_params: Any additional transaction parameters - - Returns: - An array of records matching asset ID to transaction ID of the opt in + :param account: The account to opt-in + :param asset_ids: The list of asset IDs to opt-in to + :param suppress_log: Whether to suppress logging, defaults to False + :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, + defaults to None + :param populate_app_call_resources: Whether to populate app call resources, defaults to None + :param signer: The signer to use for the transaction, defaults to None + :param rekey_to: The address to rekey the account to, defaults to None + :param note: The note to include in the transaction, defaults to None + :param lease: The lease to include in the transaction, defaults to None + :param static_fee: The static fee to include in the transaction, defaults to None + :param extra_fee: The extra fee to include in the transaction, defaults to None + :param max_fee: The maximum fee to include in the transaction, defaults to None + :param validity_window: The validity window to include in the transaction, defaults to None + :param first_valid_round: The first valid round to include in the transaction, defaults to None + :param last_valid_round: The last valid round to include in the transaction, defaults to None + :return: An array of records matching asset ID to transaction ID of the opt in """ results: list[BulkAssetOptInOutResult] = [] sender = self._get_address_from_sender(account) @@ -175,7 +198,19 @@ def bulk_opt_in( params = AssetOptInParams( sender=sender, asset_id=asset_id, - **transaction_params, + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, ) composer.add_asset_opt_in(params) @@ -186,26 +221,47 @@ def bulk_opt_in( return results - def bulk_opt_out( # noqa: C901 + def bulk_opt_out( # noqa: C901, PLR0913 self, account: str | Account | TransactionSigner, asset_ids: list[int], *, ensure_zero_balance: bool = True, suppress_log: bool = False, - **transaction_params: Any, + max_rounds_to_wait: int | None = None, + populate_app_call_resources: bool | None = None, + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, ) -> list[BulkAssetOptInOutResult]: """Opt an account out of a list of Algorand Standard Assets. - Args: - account: The account to opt-out - asset_ids: The list of asset IDs to opt-out of - ensure_zero_balance: Whether to check if the account has a zero balance first - suppress_log: Whether to suppress logging - **transaction_params: Any additional transaction parameters - - Returns: - An array of records matching asset ID to transaction ID of the opt out + :param account: The account to opt-out + :param asset_ids: The list of asset IDs to opt-out of + :param ensure_zero_balance: Whether to check if the account has a zero balance first, defaults to True + :param suppress_log: Whether to suppress logging, defaults to False + :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, + defaults to None + :param populate_app_call_resources: Whether to populate app call resources, defaults to None + :param signer: The signer to use for the transaction, defaults to None + :param rekey_to: The address to rekey the account to, defaults to None + :param note: The note to include in the transaction, defaults to None + :param lease: The lease to include in the transaction, defaults to None + :param static_fee: The static fee to include in the transaction, defaults to None + :param extra_fee: The extra fee to include in the transaction, defaults to None + :param max_fee: The maximum fee to include in the transaction, defaults to None + :param validity_window: The validity window to include in the transaction, defaults to None + :param first_valid_round: The first valid round to include in the transaction, defaults to None + :param last_valid_round: The last valid round to include in the transaction, defaults to None + :raises ValueError: If ensure_zero_balance is True and account has non-zero balance or is not opted in + :return: An array of records matching asset ID to transaction ID of the opt out """ results: list[BulkAssetOptInOutResult] = [] sender = self._get_address_from_sender(account) @@ -242,7 +298,19 @@ def bulk_opt_out( # noqa: C901 sender=sender, asset_id=asset_id, creator=asset_info.creator, - **transaction_params, + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, ) composer.add_asset_opt_out(params) @@ -265,5 +333,4 @@ def _get_address_from_sender(sender: str | Account | TransactionSigner) -> str: def _chunk_array(array: list, size: int) -> list[list]: - """Split an array into chunks of the given size.""" return [array[i : i + size] for i in range(0, len(array), size)] diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 4e43aa86..0543cbcf 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -35,6 +35,15 @@ class AlgoSdkClients: + """Container for Algorand SDK client instances. + + Holds references to Algod, Indexer and KMD clients. + + :param algod: Algod client instance + :param indexer: Optional Indexer client instance + :param kmd: Optional KMD client instance + """ + def __init__( self, algod: algosdk.v2client.algod.AlgodClient, @@ -48,6 +57,11 @@ def __init__( @dataclass(kw_only=True, frozen=True) class NetworkDetail: + """Details about an Algorand network. + + Contains network type flags and genesis information. + """ + is_testnet: bool is_mainnet: bool is_localnet: bool @@ -67,6 +81,14 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: class ClientManager: + """Manager for Algorand SDK clients. + + Provides access to Algod, Indexer and KMD clients and helper methods for working with them. + + :param clients_or_configs: Either client instances or client configurations + :param algorand_client: AlgorandClient instance + """ + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: "AlgorandClient"): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs @@ -88,28 +110,47 @@ def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algor @property def algod(self) -> AlgodClient: - """Returns an algosdk Algod API client.""" + """Returns an algosdk Algod API client. + + :return: Algod client instance + """ return self._algod @property def indexer(self) -> IndexerClient: - """Returns an algosdk Indexer API client or raises an error if it's not been provided.""" + """Returns an algosdk Indexer API client. + + :raises ValueError: If no Indexer client is configured + :return: Indexer client instance + """ if not self._indexer: raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured") return self._indexer @property def indexer_if_present(self) -> IndexerClient | None: + """Returns the Indexer client if configured, otherwise None. + + :return: Indexer client instance or None + """ return self._indexer @property def kmd(self) -> KMDClient: - """Returns an algosdk KMD API client or raises an error if it's not been provided.""" + """Returns an algosdk KMD API client. + + :raises ValueError: If no KMD client is configured + :return: KMD client instance + """ if not self._kmd: raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured") return self._kmd def network(self) -> NetworkDetail: + """Get details about the connected Algorand network. + + :return: Network details including type and genesis information + """ if self._suggested_params is None: self._suggested_params = self._algod.suggested_params() sp = self._suggested_params @@ -122,17 +163,35 @@ def network(self) -> NetworkDetail: ) def is_localnet(self) -> bool: + """Check if connected to a local network. + + :return: True if connected to a local network + """ return self.network().is_localnet def is_testnet(self) -> bool: + """Check if connected to TestNet. + + :return: True if connected to TestNet + """ return self.network().is_testnet def is_mainnet(self) -> bool: + """Check if connected to MainNet. + + :return: True if connected to MainNet + """ return self.network().is_mainnet def get_testnet_dispenser( self, auth_token: str | None = None, request_timeout: int | None = None ) -> TestNetDispenserApiClient: + """Get a TestNet dispenser API client. + + :param auth_token: Optional authentication token + :param request_timeout: Optional request timeout in seconds + :return: TestNet dispenser client instance + """ if request_timeout: return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout) @@ -149,6 +208,19 @@ def get_app_factory( deletable: bool | None = None, deploy_time_params: TealTemplateParams | None = None, ) -> "AppFactory": + """Get an application factory for deploying smart contracts. + + :param app_spec: Application specification + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param version: Optional version string + :param updatable: Optional flag to make app updatable + :param deletable: Optional flag to make app deletable + :param deploy_time_params: Optional deployment parameters + :raises ValueError: If no Algorand client is configured + :return: Application factory instance + """ from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams if not self._algorand: @@ -178,6 +250,18 @@ def get_app_client_by_id( approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: + """Get an application client for an existing application by ID. + + :param app_spec: Application specification + :param app_id: Application ID + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Application client instance + """ if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -203,6 +287,17 @@ def get_app_client_by_network( approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: + """Get an application client for an existing application by network. + + :param app_spec: Application specification + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Application client instance + """ if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -228,6 +323,19 @@ def get_app_client_by_creator_and_name( approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: + """Get an application client by creator address and name. + + :param creator_address: Creator address + :param app_name: Application name + :param app_spec: Application specification + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :return: Application client instance + """ return AppClient.from_creator_and_name( creator_address=creator_address, app_name=app_name, @@ -243,45 +351,67 @@ def get_app_client_by_creator_and_name( @staticmethod def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: - """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment + """Get an Algod client from config or environment. - If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" + :param config: Optional client configuration + :return: Algod client instance + """ config = config or _get_config_from_environment("ALGOD") headers = {"X-Algo-API-Token": config.token or ""} return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers) @staticmethod def get_algod_client_from_environment() -> AlgodClient: + """Get an Algod client from environment variables. + + :return: Algod client instance + """ return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) @staticmethod def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: - """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment + """Get a KMD client from config or environment. - If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" + :param config: Optional client configuration + :return: KMD client instance + """ config = config or _get_config_from_environment("KMD") return KMDClient(config.token, config.server) @staticmethod def get_kmd_client_from_environment() -> KMDClient: + """Get a KMD client from environment variables. + + :return: KMD client instance + """ return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) @staticmethod def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: - """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. + """Get an Indexer client from config or environment. - If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and - `INDEXER_TOKEN`""" + :param config: Optional client configuration + :return: Indexer client instance + """ config = config or _get_config_from_environment("INDEXER") headers = {"X-Indexer-API-Token": config.token} return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers) @staticmethod def get_indexer_client_from_environment() -> IndexerClient: + """Get an Indexer client from environment variables. + + :return: Indexer client instance + """ return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) @staticmethod def genesis_id_is_localnet(genesis_id: str) -> bool: + """Check if a genesis ID indicates a local network. + + :param genesis_id: Genesis ID to check + :return: True if genesis ID indicates a local network + """ return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] def get_typed_app_client_by_creator_and_name( @@ -295,6 +425,18 @@ def get_typed_app_client_by_creator_and_name( ignore_cache: bool | None = None, app_lookup_cache: AppLookup | None = None, ) -> TypedAppClientT: + """Get a typed application client by creator address and name. + + :param typed_client: Typed client class + :param creator_address: Creator address + :param app_name: Application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :raises ValueError: If no Algorand client is configured + :return: Typed application client instance + """ if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -319,6 +461,18 @@ def get_typed_app_client_by_id( approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> TypedAppClientT: + """Get a typed application client by ID. + + :param typed_client: Typed client class + :param app_id: Application ID + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Typed application client instance + """ if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -347,13 +501,14 @@ def get_typed_app_client_by_network( Uses pre-determined network-specific app IDs specified in the ARC-56 app spec. If no IDs are in the app spec or the network isn't recognised, an error is thrown. - Args: - typed_client: The typed client class to instantiate - default_sender: Optional default sender address - default_signer: Optional default transaction signer - - Returns: - The typed client instance + :param typed_client: The typed client class to instantiate + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: The typed client instance """ if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -379,6 +534,19 @@ def get_typed_app_factory( deletable: bool | None = None, deploy_time_params: TealTemplateParams | None = None, ) -> TypedFactoryT: + """Get a typed application factory. + + :param typed_factory: Typed factory class + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param version: Optional version string + :param updatable: Optional flag to make app updatable + :param deletable: Optional flag to make app deletable + :param deploy_time_params: Optional deployment parameters + :raises ValueError: If no Algorand client is configured + :return: Typed application factory instance + """ if not self._algorand: raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") @@ -400,8 +568,7 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: If ALGOD_SERVER is set in environment variables, it will use environment configuration, otherwise it will use default localnet configuration. - Returns: - AlgoClientConfigs: Configuration for algod, indexer, and optionally kmd + :return: Configuration for algod, indexer, and optionally kmd """ algod_server = os.getenv("ALGOD_SERVER") @@ -436,6 +603,11 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: @staticmethod def get_default_localnet_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: + """Get default configuration for local network services. + + :param config_or_port: Service name or port number + :return: Client configuration for local network + """ port = ( config_or_port if isinstance(config_or_port, int) @@ -447,24 +619,18 @@ def get_default_localnet_config(config_or_port: Literal["algod", "indexer", "kmd @staticmethod def get_algod_config_from_environment() -> AlgoClientConfig: """Retrieve the algod configuration from environment variables. + Will raise an error if ALGOD_SERVER environment variable is not set - Expects ALGOD_SERVER to be defined in environment variables. - ALGOD_PORT and ALGOD_TOKEN are optional. - - Raises: - ValueError: If ALGOD_SERVER environment variable is not set + :return: Algod client configuration """ return _get_config_from_environment("ALGOD") @staticmethod def get_indexer_config_from_environment() -> AlgoClientConfig: """Retrieve the indexer configuration from environment variables. + Will raise an error if INDEXER_SERVER environment variable is not set - Expects INDEXER_SERVER to be defined in environment variables. - INDEXER_PORT and INDEXER_TOKEN are optional. - - Raises: - ValueError: If INDEXER_SERVER environment variable is not set + :return: Indexer client configuration """ return _get_config_from_environment("INDEXER") @@ -472,8 +638,7 @@ def get_indexer_config_from_environment() -> AlgoClientConfig: def get_kmd_config_from_environment() -> AlgoClientConfig: """Retrieve the kmd configuration from environment variables. - Expects KMD_SERVER to be defined in environment variables. - KMD_PORT and KMD_TOKEN are optional. + :return: KMD client configuration """ return _get_config_from_environment("KMD") @@ -483,12 +648,9 @@ def get_algonode_config( ) -> AlgoClientConfig: """Returns the Algorand configuration to point to the free tier of the AlgoNode service. - Args: - network: Which network to connect to - TestNet or MainNet - config: Which algod config to return - Algod or Indexer - - Returns: - AlgoClientConfig: Configuration for the specified network and service + :param network: Which network to connect to - TestNet or MainNet + :param config: Which algod config to return - Algod or Indexer + :return: Configuration for the specified network and service """ service_type = "api" if config == "algod" else "idx" return AlgoClientConfig( diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index 6fd99446..6c5d7cd0 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -140,16 +140,13 @@ def configure( If you are executing the config from an algokit compliant project, you can simply call `config.configure(debug=True)`. - Args: - debug (bool): Indicates whether debug mode is enabled. - project_root (Path | None, optional): The path to the project root directory. Defaults to None. - trace_all (bool, optional): Indicates whether to trace all operations. Defaults to False. Which implies that + :param debug: Indicates whether debug mode is enabled. + :param project_root: The path to the project root directory. Defaults to None. + :param trace_all: Indicates whether to trace all operations. Defaults to False. Which implies that only the operations that are failed will be traced by default. - trace_buffer_size_mb (float, optional): The size of the trace buffer in megabytes. Defaults to 512mb. - max_search_depth (int, optional): The maximum depth to search for a specific file. Defaults to 10. - - Returns: - None + :param trace_buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256 + :param max_search_depth: The maximum depth to search for a specific file. Defaults to 10 + :param populate_app_call_resources: Indicates whether to populate app call resources. Defaults to False """ if debug is not None: diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index d90a426b..2f1c46da 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -13,7 +13,10 @@ @dataclasses.dataclass(kw_only=True) class Account: - """Holds the private_key and address for an account""" + """Holds the private key and address for an account. + + Provides access to the account's private key, address, public key and transaction signer. + """ private_key: str """Base64 encoded private key""" @@ -26,24 +29,39 @@ def __post_init__(self) -> None: @property def public_key(self) -> bytes: - """The public key for this account""" + """The public key for this account. + + :return: The public key as bytes + """ public_key = algosdk.encoding.decode_address(self.address) assert isinstance(public_key, bytes) return public_key @property def signer(self) -> AccountTransactionSigner: - """An AccountTransactionSigner for this account""" + """Get an AccountTransactionSigner for this account. + + :return: A transaction signer for this account + """ return AccountTransactionSigner(self.private_key) @staticmethod def new_account() -> "Account": + """Create a new random account. + + :return: A new Account instance + """ private_key, address = algosdk.account.generate_account() return Account(private_key=private_key) @dataclasses.dataclass(kw_only=True) class MultisigMetadata: + """Metadata for a multisig account. + + Contains the version, threshold and addresses for a multisig account. + """ + version: int threshold: int addresses: list[str] @@ -51,7 +69,13 @@ class MultisigMetadata: @dataclasses.dataclass(kw_only=True) class MultiSigAccount: - """Account wrapper that supports partial or full multisig signing.""" + """Account wrapper that supports partial or full multisig signing. + + Provides functionality to manage and sign transactions for a multisig account. + + :param multisig_params: The parameters for the multisig account + :param signing_accounts: The list of accounts that can sign + """ _params: MultisigMetadata _signing_accounts: list[Account] @@ -60,12 +84,6 @@ class MultiSigAccount: _multisig: Multisig def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[Account]) -> None: - """Initialize a new multisig account. - - Args: - multisig_params: The parameters for the multisig account - signing_accounts: The list of accounts that can sign - """ self._params = multisig_params self._signing_accounts = signing_accounts self._multisig = Multisig(multisig_params.version, multisig_params.threshold, multisig_params.addresses) @@ -77,32 +95,41 @@ def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[Acc @property def params(self) -> MultisigMetadata: - """The parameters for the multisig account.""" + """Get the parameters for the multisig account. + + :return: The multisig account parameters + """ return self._params @property def signing_accounts(self) -> list[Account]: - """The list of accounts that are present to sign.""" + """Get the list of accounts that are present to sign. + + :return: The list of signing accounts + """ return self._signing_accounts @property def address(self) -> str: - """The address of the multisig account.""" + """Get the address of the multisig account. + + :return: The multisig account address + """ return self._addr @property def signer(self) -> TransactionSigner: - """The transaction signer for this multisig account.""" + """Get the transaction signer for this multisig account. + + :return: The multisig transaction signer + """ return self._signer def sign(self, transaction: algosdk.transaction.Transaction) -> MultisigTransaction: - """Sign the given transaction. - - Args: - transaction: Either a transaction object or a raw, partially signed transaction + """Sign the given transaction with all present signers. - Returns: - The transaction signed by the present signers + :param transaction: Either a transaction object or a raw, partially signed transaction + :return: The transaction signed by the present signers """ msig_txn = MultisigTransaction( transaction, diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index 94fc26d7..e8888389 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -7,19 +7,18 @@ class AlgoAmount: - """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers.""" + """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. - def __init__(self, amount: dict[str, int]): - """Create a new AlgoAmount instance. + :param amount: A dictionary containing either algos, algo, microAlgos, or microAlgo as key + and their corresponding value as an integer or Decimal. + :raises ValueError: If an invalid amount format is provided. - :param amount: A dictionary containing either algos, algo, microAlgos, or microAlgo as key - and their corresponding value as an integer or Decimal. - :raises ValueError: If an invalid amount format is provided. + :example: + >>> amount = AlgoAmount({"algos": 1}) + >>> amount = AlgoAmount({"microAlgos": 1_000_000}) + """ - :example: - >>> amount = AlgoAmount({"algos": 1}) - >>> amount = AlgoAmount({"microAlgos": 1_000_000}) - """ + def __init__(self, amount: dict[str, int]): if "microAlgos" in amount: self.amount_in_micro_algo = int(amount["microAlgos"]) elif "microAlgo" in amount: diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index b7b5b2af..a9c85cff 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -77,23 +77,6 @@ @dataclass(kw_only=True, frozen=True) class _CommonTxnParams: - """ - Common transaction parameters. - - :param signer: The function used to sign transactions. - :param rekey_to: Change the signing key of the sender to the given address. - :param note: Note to attach to the transaction. - :param lease: Prevent multiple transactions with the same lease being included within the validity window. - :param static_fee: The transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be - covered by another transaction. - :param extra_fee: The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. - :param max_fee: Throw an error if the fee for the transaction is more than this amount. - :param validity_window: How many rounds the transaction should be valid for. - :param first_valid_round: Set the first round this transaction is valid. If left undefined, the value from algod - will be used. Only set this when you intentionally want this to be some time in the future. - :param last_valid_round: The last round this transaction is valid. It is recommended to use validity_window instead. - """ - sender: str signer: TransactionSigner | None = None rekey_to: str | None = None @@ -113,15 +96,13 @@ class _CommonTxnWithSendParams(_CommonTxnParams, SendParams): @dataclass(kw_only=True, frozen=True) -class PaymentParams( - _CommonTxnWithSendParams, -): - """ - Payment transaction parameters. +class PaymentParams(_CommonTxnWithSendParams): + """Parameters for a payment transaction. - :param receiver: The account that will receive the ALGO. - :param amount: Amount to send. - :param close_remainder_to: If given, close the sender account and send the remaining balance to this address. + :ivar receiver: The account that will receive the ALGO + :ivar amount: Amount to send + :ivar close_remainder_to: If given, close the sender account and send the remaining balance to this address, + defaults to None """ receiver: str @@ -130,26 +111,20 @@ class PaymentParams( @dataclass(kw_only=True, frozen=True) -class AssetCreateParams( - _CommonTxnWithSendParams, -): - """ - Asset creation parameters. - - :param total: The total amount of the smallest divisible unit to create. - :param decimals: The amount of decimal places the asset should have. - :param default_frozen: Whether the asset is frozen by default in the creator address. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. - There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. - Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. - Clawback will be permanently disabled if undefined or an empty string. - :param unit_name: The short ticker name for the asset. - :param asset_name: The full name of the asset. - :param url: The metadata URL for the asset. - :param metadata_hash: Hash of the metadata contained in the metadata URL. +class AssetCreateParams(_CommonTxnWithSendParams): + """Parameters for creating a new asset. + + :ivar total: The total amount of the smallest divisible unit to create + :ivar decimals: The amount of decimal places the asset should have, defaults to None + :ivar default_frozen: Whether the asset is frozen by default in the creator address, defaults to None + :ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + :ivar reserve: The address that holds the uncirculated supply, defaults to None + :ivar freeze: The address that can freeze the asset in any account, defaults to None + :ivar clawback: The address that can clawback the asset from any account, defaults to None + :ivar unit_name: The short ticker name for the asset, defaults to None + :ivar asset_name: The full name of the asset, defaults to None + :ivar url: The metadata URL for the asset, defaults to None + :ivar metadata_hash: Hash of the metadata contained in the metadata URL, defaults to None """ total: int @@ -166,20 +141,14 @@ class AssetCreateParams( @dataclass(kw_only=True, frozen=True) -class AssetConfigParams( - _CommonTxnWithSendParams, -): - """ - Asset configuration parameters. - - :param asset_id: ID of the asset. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. - There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. - Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. - Clawback will be permanently disabled if undefined or an empty string. +class AssetConfigParams(_CommonTxnWithSendParams): + """Parameters for configuring an existing asset. + + :ivar asset_id: ID of the asset + :ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + :ivar reserve: The address that holds the uncirculated supply, defaults to None + :ivar freeze: The address that can freeze the asset in any account, defaults to None + :ivar clawback: The address that can clawback the asset from any account, defaults to None """ asset_id: int @@ -190,15 +159,12 @@ class AssetConfigParams( @dataclass(kw_only=True, frozen=True) -class AssetFreezeParams( - _CommonTxnWithSendParams, -): - """ - Asset freeze parameters. +class AssetFreezeParams(_CommonTxnWithSendParams): + """Parameters for freezing an asset. - :param asset_id: The ID of the asset. - :param account: The account to freeze or unfreeze. - :param frozen: Whether the assets in the account should be frozen. + :ivar asset_id: The ID of the asset + :ivar account: The account to freeze or unfreeze + :ivar frozen: Whether the assets in the account should be frozen """ asset_id: int @@ -207,34 +173,25 @@ class AssetFreezeParams( @dataclass(kw_only=True, frozen=True) -class AssetDestroyParams( - _CommonTxnWithSendParams, -): - """ - Asset destruction parameters. +class AssetDestroyParams(_CommonTxnWithSendParams): + """Parameters for destroying an asset. - :param asset_id: ID of the asset. + :ivar asset_id: ID of the asset """ asset_id: int @dataclass(kw_only=True, frozen=True) -class OnlineKeyRegistrationParams( - _CommonTxnWithSendParams, -): - """ - Online key registration parameters. - - :param vote_key: The root participation public key. - :param selection_key: The VRF public key. - :param vote_first: The first round that the participation key is valid. - Not to be confused with the `first_valid` round of the keyreg transaction. - :param vote_last: The last round that the participation key is valid. - Not to be confused with the `last_valid` round of the keyreg transaction. - :param vote_key_dilution: This is the dilution for the 2-level participation key. - It determines the interval (number of rounds) for generating new ephemeral keys. - :param state_proof_key: The 64 byte state proof public key commitment. +class OnlineKeyRegistrationParams(_CommonTxnWithSendParams): + """Parameters for online key registration. + + :ivar vote_key: The root participation public key + :ivar selection_key: The VRF public key + :ivar vote_first: The first round that the participation key is valid + :ivar vote_last: The last round that the participation key is valid + :ivar vote_key_dilution: The dilution for the 2-level participation key + :ivar state_proof_key: The 64 byte state proof public key commitment, defaults to None """ vote_key: str @@ -247,25 +204,23 @@ class OnlineKeyRegistrationParams( @dataclass(kw_only=True, frozen=True) class OfflineKeyRegistrationParams(_CommonTxnWithSendParams): - """ - Offline key registration parameters. + """Parameters for offline key registration. + + :ivar prevent_account_from_ever_participating_again: Whether to prevent the account from ever participating again """ prevent_account_from_ever_participating_again: bool @dataclass(kw_only=True, frozen=True) -class AssetTransferParams( - _CommonTxnWithSendParams, -): - """ - Asset transfer parameters. - - :param asset_id: ID of the asset. - :param amount: Amount of the asset to transfer (smallest divisible unit). - :param receiver: The account to send the asset to. - :param clawback_target: The account to take the asset from. - :param close_asset_to: The account to close the asset to. +class AssetTransferParams(_CommonTxnWithSendParams): + """Parameters for transferring an asset. + + :ivar asset_id: ID of the asset + :ivar amount: Amount of the asset to transfer (smallest divisible unit) + :ivar receiver: The account to send the asset to + :ivar clawback_target: The account to take the asset from, defaults to None + :ivar close_asset_to: The account to close the asset to, defaults to None """ asset_id: int @@ -276,24 +231,21 @@ class AssetTransferParams( @dataclass(kw_only=True, frozen=True) -class AssetOptInParams( - _CommonTxnWithSendParams, -): - """ - Asset opt-in parameters. +class AssetOptInParams(_CommonTxnWithSendParams): + """Parameters for opting into an asset. - :param asset_id: ID of the asset. + :ivar asset_id: ID of the asset """ asset_id: int @dataclass(kw_only=True, frozen=True) -class AssetOptOutParams( - _CommonTxnWithSendParams, -): - """ - Asset opt-out parameters. +class AssetOptOutParams(_CommonTxnWithSendParams): + """Parameters for opting out of an asset. + + :ivar asset_id: ID of the asset + :ivar creator: The creator address of the asset """ asset_id: int @@ -302,20 +254,19 @@ class AssetOptOutParams( @dataclass(kw_only=True, frozen=True) class AppCallParams(_CommonTxnWithSendParams): - """ - Application call parameters. - - :param on_complete: The OnComplete action. - :param app_id: ID of the application. - :param approval_program: The program to execute for all OnCompletes other than ClearState. - :param clear_state_program: The program to execute for ClearState OnComplete. - :param schema: The state schema for the app. This is immutable. - :param args: Application arguments. - :param account_references: Account references. - :param app_references: App references. - :param asset_references: Asset references. - :param extra_pages: Number of extra pages required for the programs. - :param box_references: Box references. + """Parameters for calling an application. + + :ivar on_complete: The OnComplete action + :ivar app_id: ID of the application, defaults to None + :ivar approval_program: The program to execute for all OnCompletes other than ClearState, defaults to None + :ivar clear_state_program: The program to execute for ClearState OnComplete, defaults to None + :ivar schema: The state schema for the app. This is immutable, defaults to None + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar extra_pages: Number of extra pages required for the programs, defaults to None + :ivar box_references: Box references, defaults to None """ on_complete: OnComplete @@ -340,21 +291,20 @@ class AppCreateSchema(TypedDict): @dataclass(kw_only=True, frozen=True) class AppCreateParams(_CommonTxnWithSendParams): - """ - Application create parameters. + """Parameters for creating an application. - :param approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) + :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) or compiled teal (bytes) - :param clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) + :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) or compiled teal (bytes) - :param schema: The state schema for the app. This is immutable. - :param on_complete: The OnComplete action (cannot be ClearState) - :param args: Application arguments - :param account_references: Account references - :param app_references: App references - :param asset_references: Asset references - :param box_references: Box references - :param extra_program_pages: Number of extra pages required for the programs + :ivar schema: The state schema for the app. This is immutable, defaults to None + :ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar extra_program_pages: Number of extra pages required for the programs, defaults to None """ approval_program: str | bytes @@ -370,17 +320,20 @@ class AppCreateParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AppUpdateParams( - _CommonTxnWithSendParams, -): - """ - Application update parameters. +class AppUpdateParams(_CommonTxnWithSendParams): + """Parameters for updating an application. - :param app_id: ID of the application - :param approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) or - compiled teal (bytes) - :param clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) or compiled - teal (bytes) + :ivar app_id: ID of the application + :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) + or compiled teal (bytes) + :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar on_complete: The OnComplete action, defaults to None """ app_id: int @@ -395,13 +348,16 @@ class AppUpdateParams( @dataclass(kw_only=True, frozen=True) -class AppDeleteParams( - _CommonTxnWithSendParams, -): - """ - Application delete parameters. - - :param app_id: ID of the application +class AppDeleteParams(_CommonTxnWithSendParams): + """Parameters for deleting an application. + + :ivar app_id: ID of the application + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC """ app_id: int @@ -415,8 +371,6 @@ class AppDeleteParams( @dataclass(kw_only=True, frozen=True) class _BaseAppMethodCall(_CommonTxnWithSendParams): - """Base class for ABI method calls.""" - app_id: int method: Method args: list | None = None @@ -429,13 +383,16 @@ class _BaseAppMethodCall(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) class AppMethodCallParams(_CommonTxnWithSendParams): - """ - Method call parameters. - - :param app_id: ID of the application - :param method: The ABI method to call - :param args: Arguments to the ABI method - :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) + """Parameters for calling an application method. + + :ivar app_id: ID of the application + :ivar method: The ABI method to call + :ivar args: Arguments to the ABI method, defaults to None + :ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None """ app_id: int @@ -452,15 +409,11 @@ class AppMethodCallParams(_CommonTxnWithSendParams): class AppCallMethodCallParams(_BaseAppMethodCall): """Parameters for a regular ABI method call. - :param app_id: ID of the application - :param method: The ABI method to call - :param args: Arguments to the ABI method, either: - * An ABI value - * A transaction with explicit signer - * A transaction (where the signer will be automatically assigned) - * Another method call - * None (represents a placeholder transaction argument) - :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) + :ivar app_id: ID of the application + :ivar method: The ABI method to call + :ivar args: Arguments to the ABI method, either an ABI value, transaction with explicit signer, + transaction, another method call, or None + :ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None """ app_id: int @@ -471,11 +424,11 @@ class AppCallMethodCallParams(_BaseAppMethodCall): class AppCreateMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that creates an application. - :param approval_program: The program to execute for all OnCompletes other than ClearState - :param clear_state_program: The program to execute for ClearState OnComplete - :param schema: The state schema for the app - :param on_complete: The OnComplete action (cannot be ClearState) - :param extra_program_pages: Number of extra pages required for the programs + :ivar approval_program: The program to execute for all OnCompletes other than ClearState + :ivar clear_state_program: The program to execute for ClearState OnComplete + :ivar schema: The state schema for the app, defaults to None + :ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None + :ivar extra_program_pages: Number of extra pages required for the programs, defaults to None """ approval_program: str | bytes @@ -489,9 +442,10 @@ class AppCreateMethodCallParams(_BaseAppMethodCall): class AppUpdateMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that updates an application. - :param app_id: ID of the application - :param approval_program: The program to execute for all OnCompletes other than ClearState - :param clear_state_program: The program to execute for ClearState OnComplete + :ivar app_id: ID of the application + :ivar approval_program: The program to execute for all OnCompletes other than ClearState + :ivar clear_state_program: The program to execute for ClearState OnComplete + :ivar on_complete: The OnComplete action, defaults to UpdateApplicationOC """ app_id: int @@ -504,20 +458,19 @@ class AppUpdateMethodCallParams(_BaseAppMethodCall): class AppDeleteMethodCallParams(_BaseAppMethodCall): """Parameters for an ABI method call that deletes an application. - :param app_id: ID of the application + :ivar app_id: ID of the application + :ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC """ app_id: int on_complete: OnComplete = OnComplete.DeleteApplicationOC -# Type alias for all possible method call types MethodCallParams = ( AppCallMethodCallParams | AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams ) -# Type alias for transaction arguments in method calls AppMethodCallTransactionArgument = ( TransactionWithSigner | algosdk.transaction.Transaction @@ -548,12 +501,11 @@ class AppDeleteMethodCallParams(_BaseAppMethodCall): @dataclass(frozen=True) class BuiltTransactions: - """ - Set of transactions built by TransactionComposer. + """Set of transactions built by TransactionComposer. - :param transactions: The built transactions. - :param method_calls: Any ABIMethod objects associated with any of the transactions in a map keyed by txn id. - :param signers: Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id. + :ivar transactions: The built transactions + :ivar method_calls: Any ABIMethod objects associated with any of the transactions in a map keyed by txn id + :ivar signers: Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id """ transactions: list[algosdk.transaction.Transaction] @@ -563,6 +515,13 @@ class BuiltTransactions: @dataclass class TransactionComposerBuildResult: + """Result of building transactions with TransactionComposer. + + :ivar atc: The AtomicTransactionComposer instance + :ivar transactions: The list of transactions with signers + :ivar method_calls: Map of transaction index to ABI method + """ + atc: AtomicTransactionComposer transactions: list[TransactionWithSigner] method_calls: dict[int, Method] @@ -570,18 +529,21 @@ class TransactionComposerBuildResult: @dataclass class SendAtomicTransactionComposerResults: - """Results from sending an AtomicTransactionComposer transaction group""" + """Results from sending an AtomicTransactionComposer transaction group. + + :ivar group_id: The group ID if this was a transaction group + :ivar confirmations: The confirmation info for each transaction + :ivar tx_ids: The transaction IDs that were sent + :ivar transactions: The transactions that were sent + :ivar returns: The ABI return values from any ABI method calls + :ivar simulate_response: The simulation response if simulation was performed, defaults to None + """ group_id: str - """The group ID if this was a transaction group""" confirmations: list[algosdk.v2client.algod.AlgodResponseType] - """The confirmation info for each transaction""" tx_ids: list[str] - """The transaction IDs that were sent""" transactions: list[TransactionWrapper] - """The transactions that were sent""" returns: list[ABIReturn] - """The ABI return values from any ABI method calls""" simulate_response: dict[str, Any] | None = None @@ -594,21 +556,19 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 suppress_log: bool | None = None, populate_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: - """Send an AtomicTransactionComposer transaction group - - Args: - atc: The AtomicTransactionComposer to send - algod: The Algod client to use - max_rounds_to_wait: Maximum number of rounds to wait for confirmation - skip_waiting: If True, don't wait for transaction confirmation - suppress_log: If True, suppress logging - populate_resources: If True, populate app call resources - - Returns: - The results of sending the transaction group - - Raises: - Exception: If there is an error sending the transactions + """Send an AtomicTransactionComposer transaction group. + + Executes a group of transactions atomically using the AtomicTransactionComposer. + + :param atc: The AtomicTransactionComposer instance containing the transaction group to send + :param algod: The Algod client to use for sending the transactions + :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation, defaults to 5 + :param skip_waiting: If True, don't wait for transaction confirmation, defaults to False + :param suppress_log: If True, suppress logging, defaults to None + :param populate_resources: If True, populate app call resources, defaults to None + :return: Results from sending the transaction group + :raises Exception: If there is an error sending the transactions + :raises error: If there is an error from the Algorand node """ try: @@ -710,24 +670,22 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 class TransactionComposer: - """ - A class for composing and managing Algorand transactions using the Algosdk library. - - Attributes: - txn_method_map (dict[str, algosdk.abi.Method]): A dictionary that maps transaction IDs to their - corresponding ABI methods. - txns (List[Union[TransactionWithSigner, TxnParams, AtomicTransactionComposer]]): A list of transactions - that have not yet been composed. - atc (AtomicTransactionComposer): An instance of AtomicTransactionComposer used to compose transactions. - algod (AlgodClient): The AlgodClient instance used by the composer for suggested params. - get_suggested_params (Callable[[], algosdk.future.transaction.SuggestedParams]): A function that returns - suggested parameters for transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a - TransactionSigner for that address. - default_validity_window (int): The default validity window for transactions. + """A class for composing and managing Algorand transactions. + + Provides a high-level interface for building and executing transaction groups using the Algosdk library. + Supports various transaction types including payments, asset operations, application calls, and key registrations. + + :cvar _NULL_SIGNER: A constant TransactionSigner representing an empty signer + :vartype _NULL_SIGNER: TransactionSigner + :param algod: An instance of AlgodClient used to get suggested params and send transactions + :param get_signer: A function that takes an address and returns a TransactionSigner for that address + :param get_suggested_params: Optional function to get suggested transaction parameters, + defaults to using algod.suggested_params() + :param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 + :param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None """ - NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() + _NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() def __init__( self, @@ -737,19 +695,6 @@ def __init__( default_validity_window: int | None = None, app_manager: AppManager | None = None, ): - """ - Initialize an instance of the TransactionComposer class. - - Args: - algod (AlgodClient): An instance of AlgodClient used to get suggested params and send transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and - returns a TransactionSigner for that address. - get_suggested_params (Optional[Callable[[], algosdk.future.transaction.SuggestedParams]], optional): A - function that returns suggested parameters for transactions. If not provided, it defaults to using - algod.suggested_params(). Defaults to None. - default_validity_window (Optional[int], optional): The default validity window for transactions. If not - provided, it defaults to 10. Defaults to None. - """ self._txn_method_map: dict[str, algosdk.abi.Method] = {} self._txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] self._atc: AtomicTransactionComposer = AtomicTransactionComposer() @@ -763,89 +708,198 @@ def __init__( def add_transaction( self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None ) -> TransactionComposer: + """Add a raw transaction to the composer. + + :param transaction: The transaction to add + :param signer: Optional transaction signer, defaults to getting signer from transaction sender + :return: The transaction composer instance for chaining + """ self._txns.append(TransactionWithSigner(txn=transaction, signer=signer or self._get_signer(transaction.sender))) return self def add_payment(self, params: PaymentParams) -> TransactionComposer: + """Add a payment transaction. + + :param params: The payment transaction parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_create(self, params: AssetCreateParams) -> TransactionComposer: + """Add an asset creation transaction. + + :param params: The asset creation parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_config(self, params: AssetConfigParams) -> TransactionComposer: + """Add an asset configuration transaction. + + :param params: The asset configuration parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_freeze(self, params: AssetFreezeParams) -> TransactionComposer: + """Add an asset freeze transaction. + + :param params: The asset freeze parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_destroy(self, params: AssetDestroyParams) -> TransactionComposer: + """Add an asset destruction transaction. + + :param params: The asset destruction parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_transfer(self, params: AssetTransferParams) -> TransactionComposer: + """Add an asset transfer transaction. + + :param params: The asset transfer parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_opt_in(self, params: AssetOptInParams) -> TransactionComposer: + """Add an asset opt-in transaction. + + :param params: The asset opt-in parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_asset_opt_out(self, params: AssetOptOutParams) -> TransactionComposer: + """Add an asset opt-out transaction. + + :param params: The asset opt-out parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_create(self, params: AppCreateParams) -> TransactionComposer: + """Add an application creation transaction. + + :param params: The application creation parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_update(self, params: AppUpdateParams) -> TransactionComposer: + """Add an application update transaction. + + :param params: The application update parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_delete(self, params: AppDeleteParams) -> TransactionComposer: + """Add an application deletion transaction. + + :param params: The application deletion parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_call(self, params: AppCallParams) -> TransactionComposer: + """Add an application call transaction. + + :param params: The application call parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> TransactionComposer: + """Add an application creation method call transaction. + + :param params: The application creation method call parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> TransactionComposer: + """Add an application update method call transaction. + + :param params: The application update method call parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> TransactionComposer: + """Add an application deletion method call transaction. + + :param params: The application deletion method call parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_app_call_method_call(self, params: AppCallMethodCallParams) -> TransactionComposer: + """Add an application call method call transaction. + + :param params: The application call method call parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> TransactionComposer: + """Add an online key registration transaction. + + :param params: The online key registration parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> TransactionComposer: + """Add an offline key registration transaction. + + :param params: The offline key registration parameters + :return: The transaction composer instance for chaining + """ self._txns.append(params) return self def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer: + """Add an existing AtomicTransactionComposer's transactions. + + :param atc: The AtomicTransactionComposer to add + :return: The transaction composer instance for chaining + """ self._txns.append(atc) return self def count(self) -> int: + """Get the total number of transactions. + + :return: The number of transactions + """ return len(self.build_transactions().transactions) def build(self) -> TransactionComposerBuildResult: + """Build the transaction group. + + :return: The built transaction group result + """ if self._atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: suggested_params = self._get_suggested_params() txn_with_signers: list[TransactionWithSigner] = [] @@ -866,10 +920,18 @@ def build(self) -> TransactionComposerBuildResult: ) def rebuild(self) -> TransactionComposerBuildResult: + """Rebuild the transaction group from scratch. + + :return: The rebuilt transaction group result + """ self._atc = AtomicTransactionComposer() return self.build() def build_transactions(self) -> BuiltTransactions: + """Build and return the transactions without executing them. + + :return: The built transactions result + """ suggested_params = self._get_suggested_params() transactions: list[algosdk.transaction.Transaction] = [] @@ -888,7 +950,7 @@ def build_transactions(self) -> BuiltTransactions: for ts in txn_with_signers: transactions.append(ts.txn) - if ts.signer and ts.signer != self.NULL_SIGNER: + if ts.signer and ts.signer != self._NULL_SIGNER: signers[idx] = ts.signer method = self._txn_method_map.get(ts.txn.get_txid()) if method: @@ -914,6 +976,14 @@ def send( suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: + """Send the transaction group to the network. + + :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation + :param suppress_log: Whether to suppress transaction logging + :param populate_app_call_resources: Whether to populate app call resources + :return: The transaction send results + :raises Exception: If the transaction fails + """ group = self.build().transactions wait_rounds = max_rounds_to_wait @@ -955,8 +1025,16 @@ def simulate( simulation_round: int | None = None, skip_signatures: bool | None = None, ) -> SendAtomicTransactionComposerResults: - """ - Simulate transaction group execution with configurable validation rules. + """Simulate transaction group execution with configurable validation rules. + + :param allow_more_logs: Whether to allow more logs than the standard limit + :param allow_empty_signatures: Whether to allow transactions without signatures + :param allow_unnamed_resources: Whether to allow unnamed resources + :param extra_opcode_budget: Additional opcode budget to allocate + :param exec_trace_config: Configuration for execution tracing + :param simulation_round: Round number to simulate at + :param skip_signatures: Whether to skip signature validation + :return: The simulation results """ atc = AtomicTransactionComposer() if skip_signatures else self._atc @@ -965,7 +1043,7 @@ def simulate( allow_empty_signatures = True transactions = self.build_transactions() for txn in transactions.transactions: - atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer.NULL_SIGNER)) + atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer._NULL_SIGNER)) atc.method_dict = transactions.method_calls else: self.build() @@ -1023,12 +1101,13 @@ def simulate( @staticmethod def arc2_note(note: Arc2TransactionNote) -> bytes: - """ - Create an encoded transaction note that follows the ARC-2 spec. + """Create an encoded transaction note that follows the ARC-2 spec. https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md - :param note: The ARC-2 note to encode. + :param note: The ARC-2 note to encode + :return: The encoded note bytes + :raises ValueError: If the dapp_name is invalid """ pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$" diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py index f0c5397f..cff47510 100644 --- a/src/algokit_utils/transactions/transaction_creator.py +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -35,22 +35,20 @@ class AlgorandClientTransactionCreator: - """A creator for Algorand transactions.""" + """A creator for Algorand transactions. - def __init__(self, new_group: Callable[[], TransactionComposer]) -> None: - """ - Creates a new `AlgorandClientTransactionCreator`. + Provides methods to create various types of Algorand transactions including payments, + asset operations, application calls and key registrations. + + :param new_group: A lambda that starts a new TransactionComposer transaction group + """ - Args: - new_group: A lambda that starts a new `TransactionComposer` transaction group - """ + def __init__(self, new_group: Callable[[], TransactionComposer]) -> None: self._new_group = new_group def _transaction( self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] ) -> Callable[[TxnParam], Transaction]: - """Generic method to create a single transaction.""" - def create_transaction(params: TxnParam) -> Transaction: composer = self._new_group() result = c(composer)(params).build_transactions() @@ -61,8 +59,6 @@ def create_transaction(params: TxnParam) -> Transaction: def _transactions( self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] ) -> Callable[[TxnParam], BuiltTransactions]: - """Generic method to create multiple transactions.""" - def create_transactions(params: TxnParam) -> BuiltTransactions: composer = self._new_group() return c(composer)(params).build_transactions() diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 34d486c1..94333c6a 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -53,6 +53,11 @@ @dataclass(frozen=True, kw_only=True) class SendSingleTransactionResult: + """Base class for transaction results. + + Represents the result of sending a single transaction. + """ + transaction: TransactionWrapper # Last transaction confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation @@ -100,6 +105,11 @@ def from_composer_result(cls, result: SendAtomicTransactionComposerResults, inde @dataclass(frozen=True, kw_only=True) class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): + """Result of creating a new ASA (Algorand Standard Asset). + + Contains the asset ID of the newly created asset. + """ + asset_id: int @@ -108,23 +118,42 @@ class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): @dataclass(frozen=True) class SendAppTransactionResult(SendSingleTransactionResult, Generic[ABIReturnT]): + """Result of an application transaction. + + Contains the ABI return value if applicable. + """ + abi_return: ABIReturnT | None = None @dataclass(frozen=True) class SendAppUpdateTransactionResult(SendAppTransactionResult[ABIReturnT]): + """Result of updating an application. + + Contains the compiled approval and clear programs. + """ + compiled_approval: Any | None = None compiled_clear: Any | None = None @dataclass(frozen=True, kw_only=True) class SendAppCreateTransactionResult(SendAppUpdateTransactionResult[ABIReturnT]): + """Result of creating a new application. + + Contains the app ID and address of the newly created application. + """ + app_id: int app_address: str class AlgorandClientTransactionSender: - """Orchestrates sending transactions for AlgorandClient.""" + """Orchestrates sending transactions for AlgorandClient. + + Provides methods to send various types of transactions including payments, + asset operations, and application calls. + """ def __init__( self, @@ -139,6 +168,10 @@ def __init__( self._algod = algod_client def new_group(self) -> TransactionComposer: + """Create a new transaction group. + + :return: A new TransactionComposer instance + """ return self._new_group() def _send( @@ -250,7 +283,11 @@ def _get_method_call_for_log(self, method: algosdk.abi.Method, args: list[Any]) return f"{method.name}({args_str})" def payment(self, params: PaymentParams) -> SendSingleTransactionResult: - """Send a payment transaction to transfer Algo between accounts.""" + """Send a payment transaction to transfer Algo between accounts. + + :param params: Payment transaction parameters + :return: Result of the payment transaction + """ return self._send( lambda c: c.add_payment, pre_log=lambda params, transaction: ( @@ -260,7 +297,11 @@ def payment(self, params: PaymentParams) -> SendSingleTransactionResult: )(params) def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransactionResult: - """Create a new Algorand Standard Asset.""" + """Create a new Algorand Standard Asset. + + :param params: Asset creation parameters + :return: Result containing the new asset ID + """ result = self._send( lambda c: c.add_asset_create, post_log=lambda params, result: ( @@ -278,7 +319,11 @@ def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransa ) def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: - """Configure an existing Algorand Standard Asset.""" + """Configure an existing Algorand Standard Asset. + + :param params: Asset configuration parameters + :return: Result of the configuration transaction + """ return self._send( lambda c: c.add_asset_config, pre_log=lambda params, transaction: ( @@ -287,7 +332,11 @@ def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult )(params) def asset_freeze(self, params: AssetFreezeParams) -> SendSingleTransactionResult: - """Freeze or unfreeze an Algorand Standard Asset for an account.""" + """Freeze or unfreeze an Algorand Standard Asset for an account. + + :param params: Asset freeze parameters + :return: Result of the freeze transaction + """ return self._send( lambda c: c.add_asset_freeze, pre_log=lambda params, transaction: ( @@ -296,7 +345,11 @@ def asset_freeze(self, params: AssetFreezeParams) -> SendSingleTransactionResult )(params) def asset_destroy(self, params: AssetDestroyParams) -> SendSingleTransactionResult: - """Destroys an Algorand Standard Asset.""" + """Destroys an Algorand Standard Asset. + + :param params: Asset destruction parameters + :return: Result of the destroy transaction + """ return self._send( lambda c: c.add_asset_destroy, pre_log=lambda params, transaction: ( @@ -305,7 +358,11 @@ def asset_destroy(self, params: AssetDestroyParams) -> SendSingleTransactionResu )(params) def asset_transfer(self, params: AssetTransferParams) -> SendSingleTransactionResult: - """Transfer an Algorand Standard Asset.""" + """Transfer an Algorand Standard Asset. + + :param params: Asset transfer parameters + :return: Result of the transfer transaction + """ return self._send( lambda c: c.add_asset_transfer, pre_log=lambda params, transaction: ( @@ -315,7 +372,11 @@ def asset_transfer(self, params: AssetTransferParams) -> SendSingleTransactionRe )(params) def asset_opt_in(self, params: AssetOptInParams) -> SendSingleTransactionResult: - """Opt an account into an Algorand Standard Asset.""" + """Opt an account into an Algorand Standard Asset. + + :param params: Asset opt-in parameters + :return: Result of the opt-in transaction + """ return self._send( lambda c: c.add_asset_opt_in, pre_log=lambda params, transaction: ( @@ -330,7 +391,13 @@ def asset_opt_out( params: AssetOptOutParams, ensure_zero_balance: bool = True, ) -> SendSingleTransactionResult: - """Opt an account out of an Algorand Standard Asset.""" + """Opt an account out of an Algorand Standard Asset. + + :param params: Asset opt-out parameters + :param ensure_zero_balance: Check if account has zero balance before opt-out, defaults to True + :raises ValueError: If account has non-zero balance or is not opted in + :return: Result of the opt-out transaction + """ if ensure_zero_balance: try: account_asset_info = self._asset_manager.get_account_information(params.sender, params.asset_id) @@ -362,39 +429,75 @@ def asset_opt_out( )(params) def app_create(self, params: AppCreateParams) -> SendAppCreateTransactionResult[ABIReturn]: - """Create a new application.""" + """Create a new application. + + :param params: Application creation parameters + :return: Result containing the new application ID and address + """ return self._send_app_create_call(lambda c: c.add_app_create)(params) def app_update(self, params: AppUpdateParams) -> SendAppUpdateTransactionResult[ABIReturn]: - """Update an application.""" + """Update an application. + + :param params: Application update parameters + :return: Result containing the compiled programs + """ return self._send_app_update_call(lambda c: c.add_app_update)(params) def app_delete(self, params: AppDeleteParams) -> SendAppTransactionResult[ABIReturn]: - """Delete an application.""" + """Delete an application. + + :param params: Application deletion parameters + :return: Result of the deletion transaction + """ return self._send_app_call(lambda c: c.add_app_delete)(params) def app_call(self, params: AppCallParams) -> SendAppTransactionResult[ABIReturn]: - """Call an application.""" + """Call an application. + + :param params: Application call parameters + :return: Result containing any ABI return value + """ return self._send_app_call(lambda c: c.add_app_call)(params) def app_create_method_call(self, params: AppCreateMethodCallParams) -> SendAppCreateTransactionResult[ABIReturn]: - """Call an application's create method.""" + """Call an application's create method. + + :param params: Method call parameters for application creation + :return: Result containing the new application ID and address + """ return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params) def app_update_method_call(self, params: AppUpdateMethodCallParams) -> SendAppUpdateTransactionResult[ABIReturn]: - """Call an application's update method.""" + """Call an application's update method. + + :param params: Method call parameters for application update + :return: Result containing the compiled programs + """ return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params) def app_delete_method_call(self, params: AppDeleteMethodCallParams) -> SendAppTransactionResult[ABIReturn]: - """Call an application's delete method.""" + """Call an application's delete method. + + :param params: Method call parameters for application deletion + :return: Result of the deletion transaction + """ return self._send_app_call(lambda c: c.add_app_delete_method_call)(params) def app_call_method_call(self, params: AppCallMethodCallParams) -> SendAppTransactionResult[ABIReturn]: - """Call an application's call method.""" + """Call an application's call method. + + :param params: Method call parameters + :return: Result containing any ABI return value + """ return self._send_app_call(lambda c: c.add_app_call_method_call)(params) def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSingleTransactionResult: - """Register an online key.""" + """Register an online key. + + :param params: Key registration parameters + :return: Result of the registration transaction + """ return self._send( lambda c: c.add_online_key_registration, pre_log=lambda params, transaction: ( @@ -403,7 +506,11 @@ def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSi )(params) def offline_key_registration(self, params: OfflineKeyRegistrationParams) -> SendSingleTransactionResult: - """Register an offline key.""" + """Register an offline key. + + :param params: Key registration parameters + :return: Result of the registration transaction + """ return self._send( lambda c: c.add_offline_key_registration, pre_log=lambda params, transaction: ( diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index ba5424f1..64a26abf 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -633,16 +633,6 @@ def test_box_methods_with_arc4_returns_parametrized( arg_value: Any, # noqa: ANN401 value_type: str, ) -> None: - """ - Test setting and retrieving box values with different data types and box prefixes. - - Args: - test_app_client_puya (AppClient): The AppClient instance for testing. - box_prefix_str (str): The string prefix for the box. - method (str): The method name to call for setting the box. - arg_value (Any): The value to set in the box. - value_type (str): The ABI type of the value. - """ # Encode the box prefix box_prefix = box_prefix_str.encode() From 5abbed30b62cc1b357d33ed360d6809b50e6290a Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Fri, 24 Jan 2025 15:30:14 +0100 Subject: [PATCH 16/31] feat: refactoring the resource population to support cover appcall itxns fees --- legacy_v2_tests/app_client_test.json | 379 +------- legacy_v2_tests/test_debug_utils.py | 52 +- src/algokit_utils/_debugging.py | 10 +- src/algokit_utils/accounts/account_manager.py | 7 + src/algokit_utils/algorand.py | 3 +- src/algokit_utils/applications/app_client.py | 4 +- .../applications/app_deployer.py | 1 + src/algokit_utils/applications/app_factory.py | 2 + src/algokit_utils/assets/asset_manager.py | 6 + src/algokit_utils/clients/client_manager.py | 2 +- src/algokit_utils/models/transaction.py | 1 + src/algokit_utils/protocols/typed_clients.py | 1 + src/algokit_utils/transactions/__init__.py | 1 - .../transactions/transaction_composer.py | 882 ++++++++++++++++-- .../transactions/transaction_sender.py | 1 + src/algokit_utils/transactions/utils.py | 390 -------- tests/artifacts/inner-fee/application.json | 202 ++++ tests/artifacts/inner-fee/contract.py | 49 + .../nested_contract/application.json} | 2 +- tests/transactions/test_resource_packing.py | 555 ++++++++++- 20 files changed, 1657 insertions(+), 893 deletions(-) delete mode 100644 src/algokit_utils/transactions/utils.py create mode 100644 tests/artifacts/inner-fee/application.json create mode 100644 tests/artifacts/inner-fee/contract.py rename tests/{app_algorand_client.json => artifacts/nested_contract/application.json} (99%) diff --git a/legacy_v2_tests/app_client_test.json b/legacy_v2_tests/app_client_test.json index c85999d5..a5bd4359 100644 --- a/legacy_v2_tests/app_client_test.json +++ b/legacy_v2_tests/app_client_test.json @@ -1,378 +1 @@ -{ - "hints": { - "version()uint64": { - "call_config": { - "no_op": "CALL" - } - }, - "readonly(uint64)void": { - "read_only": true, - "call_config": { - "no_op": "CALL" - } - }, - "set_box(byte[4],string)void": { - "call_config": { - "no_op": "CALL" - } - }, - "get_box(byte[4])string": { - "call_config": { - "no_op": "CALL" - } - }, - "get_box_readonly(byte[4])string": { - "read_only": true, - "call_config": { - "no_op": "CALL" - } - }, - "update()void": { - "call_config": { - "update_application": "CALL" - } - }, - "update_args(string)void": { - "call_config": { - "update_application": "CALL" - } - }, - "delete()void": { - "call_config": { - "delete_application": "CALL" - } - }, - "delete_args(string)void": { - "call_config": { - "delete_application": "CALL" - } - }, - "create_opt_in()void": { - "call_config": { - "opt_in": "CREATE" - } - }, - "update_greeting(string)void": { - "call_config": { - "no_op": "CALL" - } - }, - "create()void": { - "call_config": { - "no_op": "CREATE" - } - }, - "create_args(string)void": { - "call_config": { - "no_op": "CREATE" - } - }, - "hello(string)string": { - "read_only": true, - "call_config": { - "no_op": "CALL" - } - }, - "hello_remember(string)string": { - "call_config": { - "no_op": "CALL" - } - }, - "get_last()string": { - "read_only": true, - "call_config": { - "no_op": "CALL" - } - }, - "opt_in()void": { - "call_config": { - "opt_in": "CALL" - } - }, - "opt_in_args(string)void": { - "call_config": { - "opt_in": "CALL" - } - }, - "close_out()void": { - "call_config": { - "close_out": "CALL" - } - }, - "close_out_args(string)void": { - "call_config": { - "close_out": "CALL" - } - }, - "call_with_payment(pay)string": { - "call_config": { - "no_op": "CALL" - } - } - }, - "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", - "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4=" - }, - "state": { - "global": { - "num_byte_slices": 1, - "num_uints": 0 - }, - "local": { - "num_byte_slices": 1, - "num_uints": 0 - } - }, - "schema": { - "global": { - "declared": { - "greeting": { - "type": "bytes", - "key": "greeting", - "descr": "" - } - }, - "reserved": {} - }, - "local": { - "declared": { - "last": { - "type": "bytes", - "key": "last", - "descr": "" - } - }, - "reserved": {} - } - }, - "contract": { - "name": "HelloWorldApp", - "methods": [ - { - "name": "version", - "args": [], - "returns": { - "type": "uint64" - } - }, - { - "name": "readonly", - "args": [ - { - "type": "uint64", - "name": "error" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "set_box", - "args": [ - { - "type": "byte[4]", - "name": "name" - }, - { - "type": "string", - "name": "value" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "get_box", - "args": [ - { - "type": "byte[4]", - "name": "name" - } - ], - "returns": { - "type": "string" - } - }, - { - "name": "get_box_readonly", - "args": [ - { - "type": "byte[4]", - "name": "name" - } - ], - "returns": { - "type": "string" - } - }, - { - "name": "update", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "update_args", - "args": [ - { - "type": "string", - "name": "check" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "delete", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "delete_args", - "args": [ - { - "type": "string", - "name": "check" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "create_opt_in", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "update_greeting", - "args": [ - { - "type": "string", - "name": "greeting" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "create", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "create_args", - "args": [ - { - "type": "string", - "name": "greeting" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "hello", - "args": [ - { - "type": "string", - "name": "name" - } - ], - "returns": { - "type": "string" - } - }, - { - "name": "hello_remember", - "args": [ - { - "type": "string", - "name": "name" - } - ], - "returns": { - "type": "string" - } - }, - { - "name": "get_last", - "args": [], - "returns": { - "type": "string" - } - }, - { - "name": "opt_in", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "opt_in_args", - "args": [ - { - "type": "string", - "name": "check" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "close_out", - "args": [], - "returns": { - "type": "void" - } - }, - { - "name": "close_out_args", - "args": [ - { - "type": "string", - "name": "check" - } - ], - "returns": { - "type": "void" - } - }, - { - "name": "call_with_payment", - "args": [ - { - "type": "pay", - "name": "payment" - } - ], - "returns": { - "type": "string" - } - } - ], - "networks": {} - }, - "bare_call_config": { - "close_out": "CALL", - "delete_application": "CALL", - "no_op": "CREATE", - "opt_in": "CALL", - "update_application": "CALL" - } -} \ No newline at end of file +{"hints": {"version()uint64": {"call_config": {"no_op": "CALL"}}, "readonly(uint64)void": {"read_only": true, "call_config": {"no_op": "CALL"}}, "set_box(byte[4],string)void": {"call_config": {"no_op": "CALL"}}, "get_box(byte[4])string": {"call_config": {"no_op": "CALL"}}, "get_box_readonly(byte[4])string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "update()void": {"call_config": {"update_application": "CALL"}}, "update_args(string)void": {"call_config": {"update_application": "CALL"}}, "delete()void": {"call_config": {"delete_application": "CALL"}}, "delete_args(string)void": {"call_config": {"delete_application": "CALL"}}, "create_opt_in()void": {"call_config": {"opt_in": "CREATE"}}, "update_greeting(string)void": {"call_config": {"no_op": "CALL"}}, "create()void": {"call_config": {"no_op": "CREATE"}}, "create_args(string)void": {"call_config": {"no_op": "CREATE"}}, "hello(string)string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "hello_remember(string)string": {"call_config": {"no_op": "CALL"}}, "get_last()string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "opt_in()void": {"call_config": {"opt_in": "CALL"}}, "opt_in_args(string)void": {"call_config": {"opt_in": "CALL"}}, "close_out()void": {"call_config": {"close_out": "CALL"}}, "close_out_args(string)void": {"call_config": {"close_out": "CALL"}}, "call_with_payment(pay)string": {"call_config": {"no_op": "CALL"}}}, "source": {"approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4="}, "state": {"global": {"num_byte_slices": 1, "num_uints": 0}, "local": {"num_byte_slices": 1, "num_uints": 0}}, "schema": {"global": {"declared": {"greeting": {"type": "bytes", "key": "greeting", "descr": ""}}, "reserved": {}}, "local": {"declared": {"last": {"type": "bytes", "key": "last", "descr": ""}}, "reserved": {}}}, "contract": {"name": "HelloWorldApp", "methods": [{"name": "version", "args": [], "returns": {"type": "uint64"}}, {"name": "readonly", "args": [{"type": "uint64", "name": "error"}], "returns": {"type": "void"}}, {"name": "set_box", "args": [{"type": "byte[4]", "name": "name"}, {"type": "string", "name": "value"}], "returns": {"type": "void"}}, {"name": "get_box", "args": [{"type": "byte[4]", "name": "name"}], "returns": {"type": "string"}}, {"name": "get_box_readonly", "args": [{"type": "byte[4]", "name": "name"}], "returns": {"type": "string"}}, {"name": "update", "args": [], "returns": {"type": "void"}}, {"name": "update_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "delete", "args": [], "returns": {"type": "void"}}, {"name": "delete_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "create_opt_in", "args": [], "returns": {"type": "void"}}, {"name": "update_greeting", "args": [{"type": "string", "name": "greeting"}], "returns": {"type": "void"}}, {"name": "create", "args": [], "returns": {"type": "void"}}, {"name": "create_args", "args": [{"type": "string", "name": "greeting"}], "returns": {"type": "void"}}, {"name": "hello", "args": [{"type": "string", "name": "name"}], "returns": {"type": "string"}}, {"name": "hello_remember", "args": [{"type": "string", "name": "name"}], "returns": {"type": "string"}}, {"name": "get_last", "args": [], "returns": {"type": "string"}}, {"name": "opt_in", "args": [], "returns": {"type": "void"}}, {"name": "opt_in_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "close_out", "args": [], "returns": {"type": "void"}}, {"name": "close_out_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "call_with_payment", "args": [{"type": "pay", "name": "payment"}], "returns": {"type": "string"}}], "networks": {}}, "bare_call_config": {"close_out": "CALL", "delete_application": "CALL", "no_op": "CREATE", "opt_in": "CALL", "update_application": "CALL"}} \ No newline at end of file diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index 0514fb58..b0dbd47d 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -3,15 +3,7 @@ from unittest.mock import Mock import pytest -from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, - AtomicTransactionComposer, - TransactionWithSigner, -) -from algosdk.transaction import PaymentTxn - from algokit_utils._debugging import ( - AVMDebuggerSourceMap, PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response, @@ -21,13 +13,20 @@ from algokit_utils.application_specification import ApplicationSpecification from algokit_utils.common import Program from algokit_utils.models import Account -from legacy_v2_tests.conftest import check_output_stability, get_unique_name +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + +from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -@pytest.fixture +@pytest.fixture() def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: creator_name = get_unique_name() creator = get_account(algod_client, creator_name) @@ -59,23 +58,11 @@ def test_legacy_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_fact sourcemap_file_path = root_path / "sources.avm.json" app_output_path = root_path / "cool_app" - assert (sourcemap_file_path).exists() + assert not (sourcemap_file_path).exists() assert (app_output_path / "approval.teal").exists() - assert (app_output_path / "approval.teal.tok.map").exists() + assert (app_output_path / "approval.teal.map").exists() assert (app_output_path / "clear.teal").exists() - assert (app_output_path / "clear.teal.tok.map").exists() - - result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) - for item in result.txn_group_sources: - item.location = "dummy" - - check_output_stability(json.dumps(result.to_dict())) - - # check for updates in case of multiple runs - persist_sourcemaps(sources=sources, project_root=cwd, client=algod_client) - result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) - for item in result.txn_group_sources: - assert item.location != "dummy" + assert (app_output_path / "clear.teal.map").exists() def test_legacy_build_teal_sourcemaps_without_sources( @@ -104,18 +91,13 @@ def test_legacy_build_teal_sourcemaps_without_sources( sourcemap_file_path = root_path / "sources.avm.json" app_output_path = root_path / "cool_app" - assert (sourcemap_file_path).exists() + assert not (sourcemap_file_path).exists() assert not (app_output_path / "approval.teal").exists() - assert (app_output_path / "approval.teal.tok.map").exists() - assert json.loads((app_output_path / "approval.teal.tok.map").read_text())["sources"] == [] + assert (app_output_path / "approval.teal.map").exists() + assert json.loads((app_output_path / "approval.teal.map").read_text())["sources"] == [] assert not (app_output_path / "clear.teal").exists() - assert (app_output_path / "clear.teal.tok.map").exists() - assert json.loads((app_output_path / "clear.teal.tok.map").read_text())["sources"] == [] - - result = AVMDebuggerSourceMap.from_dict(json.loads(sourcemap_file_path.read_text())) - for item in result.txn_group_sources: - item.location = "dummy" - check_output_stability(json.dumps(result.to_dict())) + assert (app_output_path / "clear.teal.map").exists() + assert json.loads((app_output_path / "clear.teal.map").read_text())["sources"] == [] def test_legacy_simulate_and_persist_response_via_app_call( diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index b4e9c753..bda8be61 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -228,12 +228,12 @@ def simulate_response( simulate_request = SimulateRequest( txn_groups=txn_group, - allow_more_logs=allow_more_logs or True, + allow_more_logs=allow_more_logs if allow_more_logs is not None else True, round=simulation_round, - extra_opcode_budget=extra_opcode_budget or 0, - allow_unnamed_resources=allow_unnamed_resources or True, - allow_empty_signatures=allow_empty_signatures or True, - exec_trace_config=exec_trace_config or trace_config, + extra_opcode_budget=extra_opcode_budget if extra_opcode_budget is not None else 0, + allow_unnamed_resources=allow_unnamed_resources if allow_unnamed_resources is not None else True, + allow_empty_signatures=allow_empty_signatures if allow_empty_signatures is not None else True, + exec_trace_config=exec_trace_config if exec_trace_config is not None else trace_config, ) return atc.simulate(algod_client, simulate_request) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index d1a81e56..82ad7edd 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -593,6 +593,7 @@ def ensure_funded( # noqa: PLR0913 max_rounds_to_wait: int | None = None, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, # Common txn params signer: TransactionSigner | None = None, rekey_to: str | None = None, @@ -621,6 +622,7 @@ def ensure_funded( # noqa: PLR0913 :param max_rounds_to_wait: Optional maximum rounds to wait for transaction :param suppress_log: Optional flag to suppress logging :param populate_app_call_resources: Optional flag to populate app call resources + :param cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees :param signer: Optional transaction signer :param rekey_to: Optional rekey address :param note: Optional transaction note @@ -671,6 +673,7 @@ def ensure_funded( # noqa: PLR0913 validity_window=validity_window, first_valid_round=first_valid_round, last_valid_round=last_valid_round, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, ) ) .send( @@ -702,6 +705,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 max_rounds_to_wait: int | None = None, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, # Common transaction params (omitting sender) signer: TransactionSigner | None = None, rekey_to: str | None = None, @@ -730,6 +734,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 :param max_rounds_to_wait: Optional maximum rounds to wait for transaction :param suppress_log: Optional flag to suppress logging :param populate_app_call_resources: Optional flag to populate app call resources + :param cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees :param signer: Optional transaction signer :param rekey_to: Optional rekey address :param note: Optional transaction note @@ -785,6 +790,8 @@ def ensure_funded_from_environment( # noqa: PLR0913 validity_window=validity_window, first_valid_round=first_valid_round, last_valid_round=last_valid_round, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + populate_app_call_resources=populate_app_call_resources, ) ) .send( diff --git a/src/algokit_utils/algorand.py b/src/algokit_utils/algorand.py index 19f0d6fc..7c03c8f5 100644 --- a/src/algokit_utils/algorand.py +++ b/src/algokit_utils/algorand.py @@ -49,8 +49,7 @@ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): self._cached_suggested_params: SuggestedParams | None = None self._cached_suggested_params_expiry: float | None = None self._cached_suggested_params_timeout: int = 3_000 # three seconds - - self._default_validity_window: int = 10 + self._default_validity_window: int | None = None def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self: """ diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index b6515ef0..05c9b6c0 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -218,6 +218,7 @@ class FundAppAccountParams: :ivar max_rounds_to_wait: Optional maximum rounds to wait :ivar suppress_log: Optional flag to suppress logging :ivar populate_app_call_resources: Optional flag to populate app call resources + :ivar cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees :ivar on_complete: Optional on complete action """ @@ -237,6 +238,7 @@ class FundAppAccountParams: max_rounds_to_wait: int | None = None suppress_log: bool | None = None populate_app_call_resources: bool | None = None + cover_app_call_inner_txn_fees: bool | None = None on_complete: algosdk.transaction.OnComplete | None = None @@ -1892,7 +1894,7 @@ def _get_sender(self, sender: str | None) -> str: return sender or self._default_sender # type: ignore[return-value] def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: - return signer or self._default_signer if not sender or sender == self._default_sender else None + return signer or (self._default_signer if not sender or sender == self._default_sender else None) def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: sender = self._get_sender(params.get("sender")) diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 7357e556..50140be3 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -170,6 +170,7 @@ class AppDeployParams: max_rounds_to_wait: int | None = None suppress_log: bool = False populate_app_call_resources: bool = False + cover_app_call_inner_txn_fees: bool | None = None # Union type for all possible deploy results diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 4acb4075..f432998c 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -562,6 +562,7 @@ def deploy( # noqa: PLR0913 max_rounds_to_wait: int | None = None, suppress_log: bool = False, populate_app_call_resources: bool = False, + cover_app_call_inner_txn_fees: bool | None = None, ) -> tuple[AppClient, AppFactoryDeployResponse]: """Deploy the application with the specified parameters.""" # Resolve control parameters with factory defaults @@ -630,6 +631,7 @@ def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: suppress_log=suppress_log, max_rounds_to_wait=max_rounds_to_wait, populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, ) deploy_response = self._algorand.app_deployer.deploy(deploy_params) diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index c0b1e3e9..6173742a 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -157,6 +157,7 @@ def bulk_opt_in( # noqa: PLR0913 suppress_log: bool = False, max_rounds_to_wait: int | None = None, populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, signer: TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, @@ -176,6 +177,7 @@ def bulk_opt_in( # noqa: PLR0913 :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, defaults to None :param populate_app_call_resources: Whether to populate app call resources, defaults to None + :param cover_app_call_inner_txn_fees: Whether to cover app call inner transaction fees, defaults to None :param signer: The signer to use for the transaction, defaults to None :param rekey_to: The address to rekey the account to, defaults to None :param note: The note to include in the transaction, defaults to None @@ -201,6 +203,7 @@ def bulk_opt_in( # noqa: PLR0913 max_rounds_to_wait=max_rounds_to_wait, suppress_log=suppress_log, populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, signer=signer, rekey_to=rekey_to, note=note, @@ -230,6 +233,7 @@ def bulk_opt_out( # noqa: C901, PLR0913 suppress_log: bool = False, max_rounds_to_wait: int | None = None, populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, signer: TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, @@ -250,6 +254,7 @@ def bulk_opt_out( # noqa: C901, PLR0913 :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, defaults to None :param populate_app_call_resources: Whether to populate app call resources, defaults to None + :param cover_app_call_inner_txn_fees: Whether to cover app call inner transaction fees, defaults to None :param signer: The signer to use for the transaction, defaults to None :param rekey_to: The address to rekey the account to, defaults to None :param note: The note to include in the transaction, defaults to None @@ -301,6 +306,7 @@ def bulk_opt_out( # noqa: C901, PLR0913 max_rounds_to_wait=max_rounds_to_wait, suppress_log=suppress_log, populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, signer=signer, rekey_to=rekey_to, note=note, diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 0543cbcf..6d0b3a06 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -406,7 +406,7 @@ def get_indexer_client_from_environment() -> IndexerClient: return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) @staticmethod - def genesis_id_is_localnet(genesis_id: str) -> bool: + def genesis_id_is_localnet(genesis_id: str | None) -> bool: """Check if a genesis ID indicates a local network. :param genesis_id: Genesis ID to check diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 37a57fb4..51ad1d93 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -96,6 +96,7 @@ class SendParams: max_rounds_to_wait: int | None = None suppress_log: bool | None = None populate_app_call_resources: bool | None = None + cover_app_call_inner_txn_fees: bool | None = None @dataclass(kw_only=True, frozen=True) diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index bec75bc5..e3da7221 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -106,4 +106,5 @@ def deploy( # noqa: PLR0913 max_rounds_to_wait: int | None = None, suppress_log: bool = False, populate_app_call_resources: bool = False, + cover_app_call_inner_txn_fees: bool | None = None, ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... diff --git a/src/algokit_utils/transactions/__init__.py b/src/algokit_utils/transactions/__init__.py index 6a540c0c..4b59b06e 100644 --- a/src/algokit_utils/transactions/__init__.py +++ b/src/algokit_utils/transactions/__init__.py @@ -1,4 +1,3 @@ from algokit_utils.transactions.transaction_composer import * # noqa: F403 from algokit_utils.transactions.transaction_creator import * # noqa: F403 from algokit_utils.transactions.transaction_sender import * # noqa: F403 -from algokit_utils.transactions.utils import * # noqa: F403 diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index ff0b6920..7d997cd9 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -4,12 +4,14 @@ import json import math import re +from copy import deepcopy from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypedDict, Union +from typing import TYPE_CHECKING, Any, TypedDict, Union, cast import algosdk import algosdk.atomic_transaction_composer import algosdk.v2client.models +from algosdk import logic, transaction from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, SimulateAtomicTransactionResponse, @@ -18,6 +20,7 @@ ) from algosdk.transaction import OnComplete from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.models.simulate_request import SimulateRequest from typing_extensions import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response @@ -25,8 +28,8 @@ from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method from algokit_utils.config import config +from algokit_utils.models.state import BoxIdentifier, BoxReference from algokit_utils.models.transaction import SendParams, TransactionWrapper -from algokit_utils.transactions.utils import encode_lease, populate_app_call_resources if TYPE_CHECKING: from collections.abc import Callable @@ -37,7 +40,6 @@ from algokit_utils.applications.abi import ABIValue from algokit_utils.models.amount import AlgoAmount - from algokit_utils.models.state import BoxIdentifier, BoxReference from algokit_utils.models.transaction import Arc2TransactionNote @@ -74,6 +76,10 @@ logger = config.logger +MAX_TRANSACTION_GROUP_SIZE = 16 +MAX_APP_CALL_FOREIGN_REFERENCES = 8 +MAX_APP_CALL_ACCOUNT_REFERENCES = 4 + @dataclass(kw_only=True, frozen=True) class _CommonTxnParams: @@ -499,6 +505,42 @@ class AppDeleteMethodCallParams(_BaseAppMethodCall): ] +@dataclass(frozen=True, kw_only=True) +class TransactionContext: + """Contextual information for a transaction.""" + + max_fee: AlgoAmount | None = None + abi_method: Method | None = None + + @staticmethod + def empty() -> TransactionContext: + return TransactionContext(max_fee=None, abi_method=None) + + +class TransactionWithContext: + """Combines Transaction with additional context.""" + + def __init__(self, txn: algosdk.transaction.Transaction, context: TransactionContext): + self.txn = txn + self.context = context + + +class TransactionWithSignerAndContext(TransactionWithSigner): + """Combines TransactionWithSigner with additional context.""" + + def __init__(self, txn: algosdk.transaction.Transaction, signer: TransactionSigner, context: TransactionContext): + super().__init__(txn, signer) + self.context = context + + @staticmethod + def from_txn_with_context( + txn_with_context: TransactionWithContext, signer: TransactionSigner + ) -> TransactionWithSignerAndContext: + return TransactionWithSignerAndContext( + txn=txn_with_context.txn, signer=signer, context=txn_with_context.context + ) + + @dataclass(frozen=True) class BuiltTransactions: """Set of transactions built by TransactionComposer. @@ -547,6 +589,573 @@ class SendAtomicTransactionComposerResults: simulate_response: dict[str, Any] | None = None +@dataclass +class ExecutionInfoTxn: + unnamed_resources_accessed: dict | None = None + required_fee_delta: int = 0 + + +@dataclass +class ExecutionInfo: + """Information about transaction execution from simulation.""" + + group_unnamed_resources_accessed: dict[str, Any] | None = None + txns: list[ExecutionInfoTxn] | None = None + + +@dataclass +class _TransactionWithPriority: + txn: algosdk.transaction.Transaction + priority: int + fee_delta: int + index: int + + +MAX_LEASE_LENGTH = 32 +NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() + + +def _encode_lease(lease: str | bytes | None) -> bytes | None: + if lease is None: + return None + elif isinstance(lease, bytes): + if not (1 <= len(lease) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received bytes with length {len(lease)}" + ) + if len(lease) == MAX_LEASE_LENGTH: + return lease + lease32 = bytearray(32) + lease32[: len(lease)] = lease + return bytes(lease32) + elif isinstance(lease, str): + encoded = lease.encode("utf-8") + if not (1 <= len(encoded) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received '{lease}' with length {len(lease)}" + ) + lease32 = bytearray(MAX_LEASE_LENGTH) + lease32[: len(encoded)] = encoded + return bytes(lease32) + else: + raise TypeError(f"Unknown lease type received of {type(lease)}") + + +def _get_group_execution_info( # noqa: C901, PLR0912 + atc: AtomicTransactionComposer, + algod: AlgodClient, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, + max_fees: dict[int, AlgoAmount] | None = None, + suggested_params: algosdk.transaction.SuggestedParams | None = None, +) -> ExecutionInfo: + # Create simulation request + simulate_request = SimulateRequest( + txn_groups=[], + allow_unnamed_resources=True, + allow_empty_signatures=True, + ) + + # Clone ATC with null signers + empty_signer_atc = atc.clone() + + # Track app call indexes without max fees + app_call_indexes_without_max_fees = [] + + # Copy transactions with null signers + for i, txn in enumerate(empty_signer_atc.txn_list): + txn_with_signer = TransactionWithSigner(txn=txn.txn, signer=NULL_SIGNER) + + if cover_app_call_inner_txn_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn): + if not suggested_params: + raise ValueError("suggested_params required when cover_app_call_inner_txn_fees enabled") + + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + if max_fee is None: + app_call_indexes_without_max_fees.append(i) + else: + txn_with_signer.txn.fee = max_fee + + if cover_app_call_inner_txn_fees and app_call_indexes_without_max_fees: + raise ValueError( + f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_txn_fees` is enabled. " # noqa: E501 + f"Required for transactions: {', '.join(str(i) for i in app_call_indexes_without_max_fees)}" + ) + + # Get fee parameters + per_byte_txn_fee = suggested_params.fee if suggested_params else 0 + min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 + + # Simulate transactions + result = empty_signer_atc.simulate(algod, simulate_request) + + group_response = result.simulate_response["txn-groups"][0] + + if group_response.get("failure-message"): + msg = group_response["failure-message"] + if cover_app_call_inner_txn_fees and "fee too small" in msg: + raise ValueError( + "Fees were too small to resolve execution info via simulate. " + "You may need to increase an app call transaction maxFee." + ) + failed_at = group_response.get("failed-at", [0])[0] + raise ValueError( + f"Error during resource population simulation in transaction {failed_at}: " + f"{group_response['failure-message']}" + ) + + # Build execution info + txn_results = [] + for i, txn_result_raw in enumerate(group_response["txn-results"]): + txn_result = txn_result_raw.get("txn-result") + if not txn_result: + continue + + original_txn = atc.build_group()[i].txn + + required_fee_delta = 0 + if cover_app_call_inner_txn_fees: + # Calculate parent transaction fee + parent_per_byte_fee = per_byte_txn_fee * (original_txn.estimate_size() + 75) + parent_min_fee = max(parent_per_byte_fee, min_txn_fee) + parent_fee_delta = parent_min_fee - original_txn.fee + + if isinstance(original_txn, algosdk.transaction.ApplicationCallTxn): + # Calculate inner transaction fees recursively + def calculate_inner_fee_delta(inner_txns: list[dict], acc: int = 0) -> int: + for inner_txn in reversed(inner_txns): + current_fee_delta = ( + calculate_inner_fee_delta(inner_txn["inner-txns"], acc) + if inner_txn.get("inner-txns") + else acc + ) + (min_txn_fee - inner_txn["txn"]["txn"].get("fee", 0)) + acc = max(0, current_fee_delta) + return acc + + inner_fee_delta = calculate_inner_fee_delta(txn_result.get("inner-txns", [])) + required_fee_delta = inner_fee_delta + parent_fee_delta + else: + required_fee_delta = parent_fee_delta + + txn_results.append( + ExecutionInfoTxn( + unnamed_resources_accessed=txn_result_raw.get("unnamed-resources-accessed") + if populate_app_call_resources + else None, + required_fee_delta=required_fee_delta, + ) + ) + + return ExecutionInfo( + group_unnamed_resources_accessed=group_response.get("unnamed-resources-accessed") + if populate_app_call_resources + else None, + txns=txn_results, + ) + + +def _find_available_transaction_index( + txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int +) -> int: + """Find index of first transaction that can accommodate the new reference.""" + + def check_transaction(txn: TransactionWithSigner) -> bool: + # Skip if not an application call transaction + if txn.txn.type != "appl": + return False + + # Get current counts (using get() with default 0 for Pythonic null handling) + accounts = len(getattr(txn.txn, "accounts", []) or []) + assets = len(getattr(txn.txn, "foreign_assets", []) or []) + apps = len(getattr(txn.txn, "foreign_apps", []) or []) + boxes = len(getattr(txn.txn, "boxes", []) or []) + + # For account references, only check account limit + if reference_type == "account": + return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + + # For asset holdings or local state, need space for both account and other reference + if reference_type in ("asset_holding", "app_local"): + return ( + accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + ) + + # For boxes with non-zero app ID, need space for box and app reference + if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0: + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + + # Default case - just check total references + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Return first matching index or -1 if none found + return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1) + + +def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: + """Populate application call resources based on simulation results. + + :param atc: The AtomicTransactionComposer containing transactions + :param algod: Algod client for simulation + :return: Modified AtomicTransactionComposer with populated resources + """ + return prepare_group_for_sending(atc, algod, populate_app_call_resources=True) + + +def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 + atc: AtomicTransactionComposer, + algod: AlgodClient, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, + max_fees: dict[int, AlgoAmount] | None = None, + suggested_params: algosdk.transaction.SuggestedParams | None = None, +) -> AtomicTransactionComposer: + """Prepare a transaction group for sending by handling execution info and resources. + + :param atc: The AtomicTransactionComposer containing transactions + :param algod: Algod client for simulation + :param populate_app_call_resources: Whether to populate app call resources + :param cover_app_call_inner_txn_fees: Whether to cover inner txn fees + :param max_fees: Max fees allowed per transaction index + :param suggested_params: Suggested transaction parameters + :return: Modified AtomicTransactionComposer ready for sending + """ + # Get execution info via simulation + execution_info = _get_group_execution_info( + atc, algod, populate_app_call_resources, cover_app_call_inner_txn_fees, max_fees, suggested_params + ) + + group = atc.build_group() + + # Handle transaction fees if needed + if cover_app_call_inner_txn_fees: + # Sort transactions by fee priority + txns_with_priority: list[_TransactionWithPriority] = [] + for i, txn_info in enumerate(execution_info.txns or []): + if not txn_info: + continue + txn = group[i].txn + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + immutable_fee = max_fee is not None and max_fee == txn.fee + priority_multiplier = ( + 1000 + if ( + txn_info.required_fee_delta > 0 + and (immutable_fee or not isinstance(txn, algosdk.transaction.ApplicationCallTxn)) + ) + else 1 + ) + + txns_with_priority.append( + _TransactionWithPriority( + txn=txn, + index=i, + fee_delta=txn_info.required_fee_delta, + priority=txn_info.required_fee_delta * priority_multiplier + if txn_info.required_fee_delta > 0 + else -1, + ) + ) + + # Sort by priority descending + txns_with_priority.sort(key=lambda x: x.priority, reverse=True) + + # Calculate surplus fees and additional fees needed + surplus_fees = sum( + txn_info.required_fee_delta * -1 + for txn_info in execution_info.txns or [] + if txn_info is not None and txn_info.required_fee_delta < 0 + ) + + additional_fees = {} + + # Distribute surplus fees to cover deficits + for txn_obj in txns_with_priority: + if txn_obj.fee_delta > 0: + if surplus_fees >= txn_obj.fee_delta: + surplus_fees -= txn_obj.fee_delta + else: + additional_fees[txn_obj.index] = txn_obj.fee_delta - surplus_fees + surplus_fees = 0 + + def populate_group_resource( # noqa: PLR0915, PLR0912, C901 + txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str + ) -> None: + """Helper function to populate group-level resources.""" + + def is_appl_below_limit(t: TransactionWithSigner) -> bool: + if not isinstance(t.txn, transaction.ApplicationCallTxn): + return False + + accounts = len(getattr(t.txn, "accounts", []) or []) + assets = len(getattr(t.txn, "foreign_assets", []) or []) + apps = len(getattr(t.txn, "foreign_apps", []) or []) + boxes = len(getattr(t.txn, "boxes", []) or []) + + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Handle asset holding and app local references first + if ref_type in ("assetHolding", "appLocal"): + ref_dict = cast(dict[str, Any], reference) + account = ref_dict["account"] + + # First try to find transaction with account already available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + account in (getattr(t.txn, "accounts", []) or []) + or account + in ( + logic.get_application_address(app_id) + for app_id in (getattr(t.txn, "foreign_apps", []) or []) + ) + or any(str(account) in str(v) for v in t.txn.__dict__.values()) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + if ref_type == "assetHolding": + asset_id = ref_dict["asset"] + app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id] + else: + app_id = ref_dict["app"] + app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] + return + + # Try to find transaction that already has the app/asset available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES + and ( + ( + ref_type == "assetHolding" + and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or []) + ) + or ( + ref_type == "appLocal" + and ( + ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or []) + or t.txn.index == ref_dict["app"] + ) + ) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(account) + app_txn.accounts = accounts + return + + # Handle box references + if ref_type == "box": + box_ref = (reference["app"], base64.b64decode(reference["name"])) # type: ignore[index] + + # Try to find transaction that already has the app available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0]) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type] + app_txn.boxes = boxes + return + + # Find available transaction for the resource + txn_idx = _find_available_transaction_index(txns, ref_type, reference) + + if txn_idx == -1: + raise ValueError("No more transactions below reference limit. Add another app call to the group.") + + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + + if ref_type == "account": + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(cast(str, reference)) + app_txn.accounts = accounts + elif ref_type == "app": + app_id = int(cast(str | int, reference)) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(app_id) + app_txn.foreign_apps = foreign_apps + elif ref_type == "box": + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type] + app_txn.boxes = boxes + if box_ref[0] != 0: + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(box_ref[0]) + app_txn.foreign_apps = foreign_apps + elif ref_type == "asset": + asset_id = int(cast(str | int, reference)) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(asset_id) + app_txn.foreign_assets = foreign_assets + elif ref_type == "assetHolding": + ref_dict = cast(dict[str, Any], reference) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(ref_dict["asset"]) + app_txn.foreign_assets = foreign_assets + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + elif ref_type == "appLocal": + ref_dict = cast(dict[str, Any], reference) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(ref_dict["app"]) + app_txn.foreign_apps = foreign_apps + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + + # Process transaction-level resources + for i, txn_info in enumerate(execution_info.txns or []): + if not txn_info: + continue + + # Validate no unexpected resources + is_app_txn = isinstance(group[i].txn, algosdk.transaction.ApplicationCallTxn) + resources = txn_info.unnamed_resources_accessed + if resources and is_app_txn: + app_txn = group[i].txn + if resources.get("boxes") or resources.get("extra-box-refs"): + raise ValueError("Unexpected boxes at transaction level") + if resources.get("appLocals"): + raise ValueError("Unexpected app local at transaction level") + if resources.get("assetHoldings"): + raise ValueError("Unexpected asset holding at transaction level") + + # Update application call fields + accounts = list(getattr(app_txn, "accounts", []) or []) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + boxes = list(getattr(app_txn, "boxes", []) or []) + + # Add new resources + accounts.extend(resources.get("accounts", [])) + foreign_apps.extend(resources.get("apps", [])) + foreign_assets.extend(resources.get("assets", [])) + boxes.extend(resources.get("boxes", [])) + + # Validate limits + if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES: + raise ValueError( + f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}" + ) + + total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes) + if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES: + raise ValueError( + f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}" + ) + + # Update transaction + app_txn.accounts = accounts # type: ignore[attr-defined] + app_txn.foreign_apps = foreign_apps # type: ignore[attr-defined] + app_txn.foreign_assets = foreign_assets # type: ignore[attr-defined] + app_txn.boxes = boxes # type: ignore[attr-defined] + + # Update fees if needed + if cover_app_call_inner_txn_fees and i in additional_fees: + cur_txn = group[i].txn + additional_fee = additional_fees[i] + if not isinstance(cur_txn, algosdk.transaction.ApplicationCallTxn): + raise ValueError( + f"An additional fee of {additional_fee} µALGO is required for non app call transaction {i}" + ) + + transaction_fee = cur_txn.fee + additional_fee + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + + if max_fee is None or transaction_fee > max_fee: + raise ValueError( + f"Calculated transaction fee {transaction_fee} µALGO is greater " + f"than max of {max_fee or 'undefined'} " + f"for transaction {i}" + ) + cur_txn.fee = transaction_fee + + # Process group-level resources + group_resources = execution_info.group_unnamed_resources_accessed + if group_resources: + # Handle cross-reference resources first + for app_local in group_resources.get("appLocals", []): + populate_group_resource(group, app_local, "appLocal") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != app_local["account"] + ] + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])] + + for asset_holding in group_resources.get("assetHoldings", []): + populate_group_resource(group, asset_holding, "assetHolding") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != asset_holding["account"] + ] + if "assets" in group_resources: + group_resources["assets"] = [ + asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"]) + ] + + # Handle remaining resources + for account in group_resources.get("accounts", []): + populate_group_resource(group, account, "account") + + for box in group_resources.get("boxes", []): + populate_group_resource(group, box, "box") + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box["app"])] + + for asset in group_resources.get("assets", []): + populate_group_resource(group, asset, "asset") + + for app in group_resources.get("apps", []): + populate_group_resource(group, app, "app") + + # Handle extra box references + extra_box_refs = group_resources.get("extra-box-refs", 0) + for _ in range(extra_box_refs): + populate_group_resource(group, {"app": 0, "name": ""}, "box") + + # Create new ATC with updated transactions + new_atc = AtomicTransactionComposer() + for txn_with_signer in group: + txn_with_signer.txn.group = None + new_atc.add_transaction(txn_with_signer) + new_atc.method_dict = deepcopy(atc.method_dict) + + return new_atc + + def send_atomic_transaction_composer( # noqa: C901, PLR0912 atc: AtomicTransactionComposer, algod: AlgodClient, @@ -554,7 +1163,10 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, suppress_log: bool | None = None, - populate_resources: bool | None = None, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, + max_fees: dict[int, AlgoAmount] | None = None, + suggested_params: algosdk.transaction.SuggestedParams | None = None, ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group. @@ -565,7 +1177,10 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation, defaults to 5 :param skip_waiting: If True, don't wait for transaction confirmation, defaults to False :param suppress_log: If True, suppress logging, defaults to None - :param populate_resources: If True, populate app call resources, defaults to None + :param populate_app_call_resources: If True, populate app call resources, defaults to None + :param cover_app_call_inner_txn_fees: If True, cover app call inner transaction fees, defaults to None + :param max_fees: Optional max fees for each transaction, defaults to None + :param suggested_params: Optional suggested params for each transaction, defaults to None :return: Results from sending the transaction group :raises Exception: If there is an error sending the transactions :raises error: If there is an error from the Algorand node @@ -575,12 +1190,23 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 # Build transactions transactions_with_signer = atc.build_group() - if populate_resources or ( - populate_resources is None - and config.populate_app_call_resource - and any(isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer) + populate_app_call_resources = ( + populate_app_call_resources + if populate_app_call_resources is not None + else config.populate_app_call_resource + ) + + if (populate_app_call_resources or cover_app_call_inner_txn_fees) and any( + isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer ): - atc = populate_app_call_resources(atc, algod) + atc = prepare_group_for_sending( + atc, + algod, + populate_app_call_resources, + cover_app_call_inner_txn_fees, + max_fees, + suggested_params, + ) transactions_to_send = [t.txn for t in transactions_with_signer] @@ -592,8 +1218,14 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 ) if not suppress_log: - logger.info(f"Sending group of {len(transactions_to_send)} transactions ({group_id})") - logger.debug(f"Transaction IDs ({group_id}): {[t.get_txid() for t in transactions_to_send]}") + logger.info( + f"Sending group of {len(transactions_to_send)} transactions ({group_id})", + suppress_log=suppress_log or False, + ) + logger.debug( + f"Transaction IDs ({group_id}): {[t.get_txid() for t in transactions_to_send]}", + suppress_log=suppress_log or False, + ) # Simulate if debug enabled if config.debug and config.trace_all and config.project_root: @@ -610,9 +1242,15 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 # Log results if not suppress_log: if len(transactions_to_send) > 1: - logger.info(f"Group transaction ({group_id}) sent with {len(transactions_to_send)} transactions") + logger.info( + f"Group transaction ({group_id}) sent with {len(transactions_to_send)} transactions", + suppress_log=suppress_log or False, + ) else: - logger.info(f"Sent transaction ID {transactions_to_send[0].get_txid()}") + logger.info( + f"Sent transaction ID {transactions_to_send[0].get_txid()}", + suppress_log=suppress_log or False, + ) # Get confirmations if not skipping confirmations = None @@ -633,7 +1271,8 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 if config.debug: logger.error( "Received error executing Atomic Transaction Composer and debug flag enabled; " - "attempting simulation to get more information" + "attempting simulation to get more information", + suppress_log=suppress_log or False, ) simulate = None @@ -665,7 +1304,10 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 error.traces = traces # type: ignore[attr-defined] raise error from e - logger.error("Received error executing Atomic Transaction Composer, for more information enable the debug flag") + logger.error( + "Received error executing Atomic Transaction Composer, for more information enable the debug flag", + suppress_log=suppress_log or False, + ) raise e @@ -675,8 +1317,6 @@ class TransactionComposer: Provides a high-level interface for building and executing transaction groups using the Algosdk library. Supports various transaction types including payments, asset operations, application calls, and key registrations. - :cvar _NULL_SIGNER: A constant TransactionSigner representing an empty signer - :vartype _NULL_SIGNER: TransactionSigner :param algod: An instance of AlgodClient used to get suggested params and send transactions :param get_signer: A function that takes an address and returns a TransactionSigner for that address :param get_suggested_params: Optional function to get suggested transaction parameters, @@ -685,8 +1325,6 @@ class TransactionComposer: :param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None """ - _NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() - def __init__( self, algod: AlgodClient, @@ -695,7 +1333,9 @@ def __init__( default_validity_window: int | None = None, app_manager: AppManager | None = None, ): - self._txn_method_map: dict[str, algosdk.abi.Method] = {} + # Map of transaction index in the atc to a max logical fee. + # This is set using the value of either maxFee or staticFee. + self._txn_max_fees: dict[int, AlgoAmount] = {} self._txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] self._atc: AtomicTransactionComposer = AtomicTransactionComposer() self._algod: AlgodClient = algod @@ -703,6 +1343,7 @@ def __init__( self._get_suggested_params = get_suggested_params or self._default_get_send_params self._get_signer: Callable[[str], TransactionSigner] = get_signer self._default_validity_window: int = default_validity_window or 10 + self._default_validity_window_is_explicit: bool = default_validity_window is not None self._app_manager = app_manager or AppManager(algod) def add_transaction( @@ -902,16 +1543,17 @@ def build(self) -> TransactionComposerBuildResult: """ if self._atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: suggested_params = self._get_suggested_params() - txn_with_signers: list[TransactionWithSigner] = [] + txn_with_signers: list[TransactionWithSignerAndContext] = [] for txn in self._txns: txn_with_signers.extend(self._build_txn(txn, suggested_params)) for ts in txn_with_signers: self._atc.add_transaction(ts) - method = self._txn_method_map.get(ts.txn.get_txid()) - if method: - self._atc.method_dict[len(self._atc.txn_list) - 1] = method + if ts.context.abi_method: + self._atc.method_dict[len(self._atc.txn_list) - 1] = ts.context.abi_method + if ts.context.max_fee: + self._txn_max_fees[len(self._atc.txn_list) - 1] = ts.context.max_fee return TransactionComposerBuildResult( atc=self._atc, @@ -950,11 +1592,10 @@ def build_transactions(self) -> BuiltTransactions: for ts in txn_with_signers: transactions.append(ts.txn) - if ts.signer and ts.signer != self._NULL_SIGNER: + if ts.signer and ts.signer != NULL_SIGNER: signers[idx] = ts.signer - method = self._txn_method_map.get(ts.txn.get_txid()) - if method: - method_calls[idx] = method + if isinstance(ts, TransactionWithSignerAndContext) and ts.context.abi_method: + method_calls[idx] = ts.context.abi_method idx += 1 return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) @@ -975,21 +1616,24 @@ def send( max_rounds_to_wait: int | None = None, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, + cover_app_call_inner_txn_fees: bool | None = None, ) -> SendAtomicTransactionComposerResults: """Send the transaction group to the network. :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation :param suppress_log: Whether to suppress transaction logging :param populate_app_call_resources: Whether to populate app call resources + :param cover_app_call_inner_txn_fees: Whether to cover inner transaction fees for app calls :return: The transaction send results :raises Exception: If the transaction fails """ group = self.build().transactions - wait_rounds = max_rounds_to_wait + sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_txn_fees else None if wait_rounds is None: last_round = max(txn.txn.last_valid_round for txn in group) - first_round = self._get_suggested_params().first + assert sp is not None + first_round = sp.first wait_rounds = last_round - first_round + 1 try: @@ -997,8 +1641,11 @@ def send( self._atc, self._algod, max_rounds_to_wait=wait_rounds, + max_fees=self._txn_max_fees, suppress_log=suppress_log, - populate_resources=populate_app_call_resources, + populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + suggested_params=sp, ) except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e @@ -1043,7 +1690,7 @@ def simulate( allow_empty_signatures = True transactions = self.build_transactions() for txn in transactions.transactions: - atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer._NULL_SIGNER)) + atc.add_transaction(TransactionWithSigner(txn=txn, signer=NULL_SIGNER)) atc.method_dict = transactions.method_calls else: self.build() @@ -1125,36 +1772,68 @@ def arc2_note(note: Arc2TransactionNote) -> bytes: arc2_payload = f"{note['dapp_name']}:{note['format']}{data}" return arc2_payload.encode("utf-8") - def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSigner]: + def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSignerAndContext]: group = atc.build_group() - for ts in group: + txn_with_signers = [] + for idx, ts in enumerate(group): ts.txn.group = None + if atc.method_dict.get(idx): + txn_with_signers.append( + TransactionWithSignerAndContext( + txn=ts.txn, + signer=ts.signer, + context=TransactionContext(abi_method=atc.method_dict.get(idx)), + ) + ) + else: + txn_with_signers.append( + TransactionWithSignerAndContext( + txn=ts.txn, + signer=ts.signer, + context=TransactionContext(abi_method=None), + ) + ) - method = atc.method_dict.get(len(group) - 1) - if method: - self._txn_method_map[group[-1].txn.get_txid()] = method - - return group + return txn_with_signers - def _common_txn_build_step( + def _common_txn_build_step( # noqa: C901 self, build_txn: Callable[[dict], algosdk.transaction.Transaction], params: _CommonTxnWithSendParams, txn_params: dict, - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: # Clone suggested params txn_params["sp"] = ( algosdk.transaction.SuggestedParams(**txn_params["sp"].__dict__) if "sp" in txn_params else None ) if params.lease: - txn_params["lease"] = encode_lease(params.lease) + txn_params["lease"] = _encode_lease(params.lease) if params.rekey_to: txn_params["rekey_to"] = params.rekey_to if params.note: txn_params["note"] = params.note + if txn_params["sp"]: + if params.first_valid_round: + txn_params["sp"].first = params.first_valid_round + + if params.last_valid_round: + txn_params["sp"].last = params.last_valid_round + else: + # If the validity window isn't set in this transaction or by default and we are pointing at + # LocalNet set a bigger window to avoid dead transactions + from algokit_utils.clients import ClientManager + + is_localnet = ClientManager.genesis_id_is_localnet(txn_params["sp"].gen) + window = params.validity_window or ( + 1000 + if is_localnet and not self._default_validity_window_is_explicit + else self._default_validity_window + ) + txn_params["sp"].last = txn_params["sp"].first + window + if params.static_fee is not None and txn_params["sp"]: txn_params["sp"].fee = params.static_fee.micro_algos txn_params["sp"].flat_fee = True @@ -1169,17 +1848,24 @@ def _common_txn_build_step( if params.max_fee and txn.fee > params.max_fee.micro_algos: raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}") + use_max_fee = params.max_fee and params.max_fee.micro_algo > ( + params.static_fee.micro_algo if params.static_fee else 0 + ) + logical_max_fee = params.max_fee if use_max_fee else params.static_fee - return txn + return TransactionWithContext( + txn=txn, + context=TransactionContext(max_fee=logical_max_fee), + ) - def _build_method_call( # noqa: C901, PLR0912 + def _build_method_call( # noqa: C901, PLR0912, PLR0915 self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> list[TransactionWithSigner]: + ) -> list[TransactionWithSignerAndContext]: method_args: list[ABIValue | TransactionWithSigner] = [] - arg_offset = 0 + txns_for_group: list[TransactionWithSignerAndContext] = [] if params.args: - for _, arg in enumerate(params.args): + for _, arg in enumerate(reversed(params.args)): if self._is_abi_value(arg): method_args.append(arg) continue @@ -1191,7 +1877,11 @@ def _build_method_call( # noqa: C901, PLR0912 if isinstance(arg, algosdk.transaction.Transaction): # Wrap in TransactionWithSigner method_args.append( - TransactionWithSigner(txn=arg, signer=params.signer or self._get_signer(params.sender)) + TransactionWithSignerAndContext( + txn=arg, + signer=params.signer if params.signer is not None else self._get_signer(params.sender), + context=TransactionContext(abi_method=None), + ) ) continue match arg: @@ -1202,8 +1892,10 @@ def _build_method_call( # noqa: C901, PLR0912 | AppDeleteMethodCallParams() ): temp_txn_with_signers = self._build_method_call(arg, suggested_params) - method_args.extend(temp_txn_with_signers) - arg_offset += len(temp_txn_with_signers) - 1 + # Add all transactions except the last one in reverse order + txns_for_group.extend(temp_txn_with_signers[:-1]) + # Add the last transaction to method_args + method_args.append(temp_txn_with_signers[-1]) continue case AppCallParams(): txn = self._build_app_call(arg, suggested_params) @@ -1229,20 +1921,45 @@ def _build_method_call( # noqa: C901, PLR0912 raise ValueError(f"Unsupported method arg transaction type: {arg!s}") method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self._get_signer(params.sender)) + TransactionWithSignerAndContext( + txn=txn.txn, + signer=params.signer or self._get_signer(params.sender), + context=TransactionContext(abi_method=params.method), + ) ) continue method_atc = AtomicTransactionComposer() + max_fees: dict[int, AlgoAmount] = {} + + # Process in reverse order + for arg in reversed(txns_for_group): + atc_index = method_atc.get_tx_count() - 1 + + if isinstance(arg, TransactionWithSignerAndContext) and arg.context: + if arg.context.abi_method: + method_atc.method_dict[atc_index] = arg.context.abi_method + + if arg.context.max_fee is not None: + max_fees[atc_index] = arg.context.max_fee + + # Process method args that are transactions with ABI method info + for i, arg in enumerate(reversed([a for a in method_args if isinstance(a, TransactionWithSignerAndContext)])): + atc_index = method_atc.get_tx_count() + i + if arg.context: + if arg.context.abi_method: + method_atc.method_dict[atc_index] = arg.context.abi_method + if arg.context.max_fee is not None: + max_fees[atc_index] = arg.context.max_fee txn_params = { - "app_id": params.app_id or 0, + "app_id": params.app_id if params.app_id is not None else 0, "method": params.method, "sender": params.sender, "sp": suggested_params, - "signer": params.signer or self._get_signer(params.sender), - "method_args": method_args, + "signer": params.signer if params.signer is not None else self._get_signer(params.sender), + "method_args": list(reversed(method_args)), "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, "note": params.note, "lease": params.lease, @@ -1273,13 +1990,20 @@ def _add_method_call_and_return_txn(x: dict) -> algosdk.transaction.Transaction: method_atc.add_method_call(**x) return method_atc.build_group()[-1].txn - self._common_txn_build_step(lambda x: _add_method_call_and_return_txn(x), params, txn_params) + result = self._common_txn_build_step(lambda x: _add_method_call_and_return_txn(x), params, txn_params) + + build_atc_resp = self._build_atc(method_atc) + response = [] + for i, v in enumerate(build_atc_resp): + max_fee = result.context.max_fee if i == method_atc.get_tx_count() - 1 else max_fees.get(i) + context = TransactionContext(abi_method=v.context.abi_method, max_fee=max_fee) + response.append(TransactionWithSignerAndContext(txn=v.txn, signer=v.signer, context=context)) - return self._build_atc(method_atc) + return response def _build_payment( self, params: PaymentParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1292,7 +2016,7 @@ def _build_payment( def _build_asset_create( self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1315,7 +2039,7 @@ def _build_app_call( self, params: AppCallParams | AppUpdateParams | AppCreateParams | AppDeleteParams, suggested_params: algosdk.transaction.SuggestedParams, - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: app_id = getattr(params, "app_id", 0) approval_program = None @@ -1377,7 +2101,7 @@ def _build_app_call( def _build_asset_config( self, params: AssetConfigParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1393,7 +2117,7 @@ def _build_asset_config( def _build_asset_destroy( self, params: AssetDestroyParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1404,7 +2128,7 @@ def _build_asset_destroy( def _build_asset_freeze( self, params: AssetFreezeParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1417,7 +2141,7 @@ def _build_asset_freeze( def _build_asset_transfer( self, params: AssetTransferParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: txn_params = { "sender": params.sender, "sp": suggested_params, @@ -1434,7 +2158,7 @@ def _build_key_reg( self, params: OnlineKeyRegistrationParams | OfflineKeyRegistrationParams, suggested_params: algosdk.transaction.SuggestedParams, - ) -> algosdk.transaction.Transaction: + ) -> TransactionWithContext: if isinstance(params, OnlineKeyRegistrationParams): txn_params = { "sender": params.sender, @@ -1476,15 +2200,17 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 self, txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, suggested_params: algosdk.transaction.SuggestedParams, - ) -> list[TransactionWithSigner]: + ) -> list[TransactionWithSignerAndContext]: match txn: case TransactionWithSigner(): - return [txn] + return [ + TransactionWithSignerAndContext(txn=txn.txn, signer=txn.signer, context=TransactionContext.empty()) + ] case AtomicTransactionComposer(): return self._build_atc(txn) case algosdk.transaction.Transaction(): signer = self._get_signer(txn.sender) - return [TransactionWithSigner(txn=txn, signer=signer)] + return [TransactionWithSignerAndContext(txn=txn, signer=signer, context=TransactionContext.empty())] case ( AppCreateMethodCallParams() | AppCallMethodCallParams() @@ -1498,30 +2224,30 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 match txn: case PaymentParams(): payment = self._build_payment(txn, suggested_params) - return [TransactionWithSigner(txn=payment, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(payment, signer)] case AssetCreateParams(): asset_create = self._build_asset_create(txn, suggested_params) - return [TransactionWithSigner(txn=asset_create, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_create, signer)] case AppCallParams() | AppUpdateParams() | AppCreateParams() | AppDeleteParams(): app_call = self._build_app_call(txn, suggested_params) - return [TransactionWithSigner(txn=app_call, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(app_call, signer)] case AssetConfigParams(): asset_config = self._build_asset_config(txn, suggested_params) - return [TransactionWithSigner(txn=asset_config, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_config, signer)] case AssetDestroyParams(): asset_destroy = self._build_asset_destroy(txn, suggested_params) - return [TransactionWithSigner(txn=asset_destroy, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_destroy, signer)] case AssetFreezeParams(): asset_freeze = self._build_asset_freeze(txn, suggested_params) - return [TransactionWithSigner(txn=asset_freeze, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_freeze, signer)] case AssetTransferParams(): asset_transfer = self._build_asset_transfer(txn, suggested_params) - return [TransactionWithSigner(txn=asset_transfer, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] case AssetOptInParams(): asset_transfer = self._build_asset_transfer( AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params ) - return [TransactionWithSigner(txn=asset_transfer, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] case AssetOptOutParams(): txn_dict = txn.__dict__ creator = txn_dict.pop("creator") @@ -1529,9 +2255,9 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 AssetTransferParams(**txn_dict, receiver=txn.sender, amount=0, close_asset_to=creator), suggested_params, ) - return [TransactionWithSigner(txn=asset_transfer, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams(): key_reg = self._build_key_reg(txn, suggested_params) - return [TransactionWithSigner(txn=key_reg, signer=signer)] + return [TransactionWithSignerAndContext.from_txn_with_context(key_reg, signer)] case _: raise ValueError(f"Unsupported txn: {txn}") diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 94333c6a..eae6fa3f 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -190,6 +190,7 @@ def send_transaction(params: T) -> SendSingleTransactionResult: raw_result = composer.send( populate_app_call_resources=params.populate_app_call_resources, + cover_app_call_inner_txn_fees=params.cover_app_call_inner_txn_fees, max_rounds_to_wait=params.max_rounds_to_wait, suppress_log=params.suppress_log, ) diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py deleted file mode 100644 index 966d47c8..00000000 --- a/src/algokit_utils/transactions/utils.py +++ /dev/null @@ -1,390 +0,0 @@ -import base64 -from copy import deepcopy -from typing import Any, cast - -from algosdk import logic, transaction -from algosdk.atomic_transaction_composer import AtomicTransactionComposer, EmptySigner, TransactionWithSigner -from algosdk.error import AtomicTransactionComposerError -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup - -from algokit_utils.models.state import BoxReference - -__all__ = [ - "get_unnamed_app_call_resources_accessed", - "populate_app_call_resources", -] - -# Constants -MAX_APP_CALL_ACCOUNT_REFERENCES = 4 -MAX_APP_CALL_FOREIGN_REFERENCES = 8 - - -def _find_available_transaction_index( - txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int -) -> int: - """Find index of first transaction that can accommodate the new reference.""" - - def check_transaction(txn: TransactionWithSigner) -> bool: - # Skip if not an application call transaction - if txn.txn.type != "appl": - return False - - # Get current counts (using get() with default 0 for Pythonic null handling) - accounts = len(getattr(txn.txn, "accounts", []) or []) - assets = len(getattr(txn.txn, "foreign_assets", []) or []) - apps = len(getattr(txn.txn, "foreign_apps", []) or []) - boxes = len(getattr(txn.txn, "boxes", []) or []) - - # For account references, only check account limit - if reference_type == "account": - return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES - - # For asset holdings or local state, need space for both account and other reference - if reference_type in ("asset_holding", "app_local"): - return ( - accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 - and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES - ) - - # For boxes with non-zero app ID, need space for box and app reference - if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0: - return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 - - # Default case - just check total references - return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - - # Return first matching index or -1 if none found - return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1) - - -def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: # noqa: C901, PLR0915, PLR0912 - """ - Populate application call resources based on simulation results. - """ - # Get unnamed resources from simulation - unnamed_resources = get_unnamed_app_call_resources_accessed(atc, algod) - group = atc.build_group() - - # Process transaction-level resources - for i, txn_resources in enumerate(unnamed_resources["txns"]): - if not txn_resources or not isinstance(group[i].txn, transaction.ApplicationCallTxn): - continue - - # Validate no unexpected resources - if txn_resources.get("boxes") or txn_resources.get("extra-box-refs"): - raise ValueError("Unexpected boxes at the transaction level") - if txn_resources.get("appLocals"): - raise ValueError("Unexpected app local at the transaction level") - if txn_resources.get("assetHoldings"): - raise ValueError("Unexpected asset holding at the transaction level") - - # Update application call fields - app_txn = cast(transaction.ApplicationCallTxn, group[i].txn) - accounts = list(getattr(app_txn, "accounts", []) or []) - foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) - boxes = list(getattr(app_txn, "boxes", []) or []) - - # Add new resources - accounts.extend(txn_resources.get("accounts", [])) - foreign_apps.extend(txn_resources.get("apps", [])) - foreign_assets.extend(txn_resources.get("assets", [])) - boxes.extend(txn_resources.get("boxes", [])) - - # Validate limits - if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES: - raise ValueError( - f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}" - ) - - total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes) - if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES: - raise ValueError( - f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}" - ) - - # Update transaction - app_txn.accounts = accounts - app_txn.foreign_apps = foreign_apps - app_txn.foreign_assets = foreign_assets - app_txn.boxes = boxes - - def populate_group_resource( # noqa: C901, PLR0912, PLR0915 - txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str - ) -> None: - """Helper function to populate group-level resources matching TypeScript implementation""" - - def is_appl_below_limit(t: TransactionWithSigner) -> bool: - if not isinstance(t.txn, transaction.ApplicationCallTxn): - return False - - accounts = len(getattr(t.txn, "accounts", []) or []) - assets = len(getattr(t.txn, "foreign_assets", []) or []) - apps = len(getattr(t.txn, "foreign_apps", []) or []) - boxes = len(getattr(t.txn, "boxes", []) or []) - - return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - - # Handle asset holding and app local references first - if ref_type in ("assetHolding", "appLocal"): - ref_dict = cast(dict[str, Any], reference) - account = ref_dict["account"] - - # First try to find transaction with account already available - txn_idx = next( - ( - i - for i, t in enumerate(txns) - if is_appl_below_limit(t) - and isinstance(t.txn, transaction.ApplicationCallTxn) - and ( - account in (getattr(t.txn, "accounts", []) or []) - or account - in ( - logic.get_application_address(app_id) - for app_id in (getattr(t.txn, "foreign_apps", []) or []) - ) - or any(str(account) in str(v) for v in t.txn.__dict__.values()) - ) - ), - -1, - ) - - if txn_idx >= 0: - app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) - if ref_type == "assetHolding": - asset_id = ref_dict["asset"] - app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id] - else: - app_id = ref_dict["app"] - app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] - return - - # Try to find transaction that already has the app/asset available - txn_idx = next( - ( - i - for i, t in enumerate(txns) - if is_appl_below_limit(t) - and isinstance(t.txn, transaction.ApplicationCallTxn) - and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES - and ( - ( - ref_type == "assetHolding" - and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or []) - ) - or ( - ref_type == "appLocal" - and ( - ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or []) - or t.txn.index == ref_dict["app"] - ) - ) - ) - ), - -1, - ) - - if txn_idx >= 0: - app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) - accounts = list(getattr(app_txn, "accounts", []) or []) - accounts.append(account) - app_txn.accounts = accounts - return - - # Handle box references - if ref_type == "box": - box_ref: tuple[int, bytes] = (reference["app"], base64.b64decode(reference["name"])) # type: ignore # noqa: PGH003 - - # Try to find transaction that already has the app available - txn_idx = next( - ( - i - for i, t in enumerate(txns) - if is_appl_below_limit(t) - and isinstance(t.txn, transaction.ApplicationCallTxn) - and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0]) - ), - -1, - ) - - if txn_idx >= 0: - app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) - boxes = list(getattr(app_txn, "boxes", []) or []) - boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) - app_txn.boxes = boxes - return - - # Find available transaction for the resource - txn_idx = _find_available_transaction_index(txns, ref_type, reference) - - if txn_idx == -1: - raise ValueError("No more transactions below reference limit. Add another app call to the group.") - - app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) - - if ref_type == "account": - accounts = list(getattr(app_txn, "accounts", []) or []) - accounts.append(cast(str, reference)) - app_txn.accounts = accounts - elif ref_type == "app": - app_id = int(cast(str | int, reference)) - foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_apps.append(app_id) - app_txn.foreign_apps = foreign_apps - elif ref_type == "box": - boxes = list(getattr(app_txn, "boxes", []) or []) - boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) - app_txn.boxes = boxes - if box_ref[0] != 0: - foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_apps.append(box_ref[0]) - app_txn.foreign_apps = foreign_apps - elif ref_type == "asset": - asset_id = int(cast(str | int, reference)) - foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) - foreign_assets.append(asset_id) - app_txn.foreign_assets = foreign_assets - elif ref_type == "assetHolding": - ref_dict = cast(dict[str, Any], reference) - foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) - foreign_assets.append(ref_dict["asset"]) - app_txn.foreign_assets = foreign_assets - accounts = list(getattr(app_txn, "accounts", []) or []) - accounts.append(ref_dict["account"]) - app_txn.accounts = accounts - elif ref_type == "appLocal": - ref_dict = cast(dict[str, Any], reference) - foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) - foreign_apps.append(ref_dict["app"]) - app_txn.foreign_apps = foreign_apps - accounts = list(getattr(app_txn, "accounts", []) or []) - accounts.append(ref_dict["account"]) - app_txn.accounts = accounts - - # Process group-level resources - group_resources = unnamed_resources["group"] - if group_resources: - # Handle cross-reference resources first - for app_local in group_resources.get("appLocals", []): - populate_group_resource(group, app_local, "appLocal") - # Remove processed resources - if "accounts" in group_resources: - group_resources["accounts"] = [ - acc for acc in group_resources["accounts"] if acc != app_local["account"] - ] - if "apps" in group_resources: - group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])] - - for asset_holding in group_resources.get("assetHoldings", []): - populate_group_resource(group, asset_holding, "assetHolding") - # Remove processed resources - if "accounts" in group_resources: - group_resources["accounts"] = [ - acc for acc in group_resources["accounts"] if acc != asset_holding["account"] - ] - if "assets" in group_resources: - group_resources["assets"] = [ - asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"]) - ] - - # Handle remaining resources - for account in group_resources.get("accounts", []): - populate_group_resource(group, account, "account") - - for box in group_resources.get("boxes", []): - populate_group_resource(group, box, "box") - if "apps" in group_resources: - group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box.app_index)] - - for asset in group_resources.get("assets", []): - populate_group_resource(group, asset, "asset") - - for app in group_resources.get("apps", []): - populate_group_resource(group, app, "app") - - # Handle extra box references - extra_box_refs = group_resources.get("extra-box-refs", 0) - for _ in range(extra_box_refs): - populate_group_resource(group, {"app": 0, "name": ""}, "box") - - # Create new ATC with updated transactions - new_atc = AtomicTransactionComposer() - for txn_with_signer in group: - txn_with_signer.txn.group = None - new_atc.add_transaction(txn_with_signer) - - # Copy method calls - new_atc.method_dict = deepcopy(atc.method_dict) - - return new_atc - - -def get_unnamed_app_call_resources_accessed(atc: AtomicTransactionComposer, algod: AlgodClient) -> dict[str, Any]: - """Get unnamed resources accessed by application calls in an atomic transaction group.""" - # Create simulation request with required flags - simulate_request = SimulateRequest(txn_groups=[], allow_unnamed_resources=True, allow_empty_signatures=True) - - # Create empty signer - null_signer = EmptySigner() - - # Clone the ATC and replace signers - unsigned_txn_groups = atc.build_group() - txn_group = [ - SimulateRequestTransactionGroup( - txns=null_signer.sign_transactions([txn_group.txn for txn_group in unsigned_txn_groups], []) - ) - ] - simulate_request = SimulateRequest(txn_groups=txn_group, allow_unnamed_resources=True, allow_empty_signatures=True) - - # Run simulation - result = atc.simulate(algod, simulate_request) - - # Get first group response - group_response = result.simulate_response["txn-groups"][0] - - # Check for simulation failure - if group_response.get("failure-message"): - failed_at = group_response.get("failed-at", [0])[0] - raise AtomicTransactionComposerError( - f"Error during resource population simulation in transaction {failed_at}: " - f"{group_response['failure-message']}" - ) - - # Return resources accessed at group and transaction level - return { - "group": group_response.get("unnamed-resources-accessed", {}), - "txns": [txn.get("unnamed-resources-accessed", {}) for txn in group_response.get("txn-results", [])], - } - - -MAX_LEASE_LENGTH = 32 - - -def encode_lease(lease: str | bytes | None) -> bytes | None: - if lease is None: - return None - elif isinstance(lease, bytes): - if not (1 <= len(lease) <= MAX_LEASE_LENGTH): - raise ValueError( - f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " - f"but received bytes with length {len(lease)}" - ) - if len(lease) == MAX_LEASE_LENGTH: - return lease - lease32 = bytearray(32) - lease32[: len(lease)] = lease - return bytes(lease32) - elif isinstance(lease, str): - encoded = lease.encode("utf-8") - if not (1 <= len(encoded) <= MAX_LEASE_LENGTH): - raise ValueError( - f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " - f"but received '{lease}' with length {len(lease)}" - ) - lease32 = bytearray(MAX_LEASE_LENGTH) - lease32[: len(encoded)] = encoded - return bytes(lease32) - else: - raise TypeError(f"Unknown lease type received of {type(lease)}") diff --git a/tests/artifacts/inner-fee/application.json b/tests/artifacts/inner-fee/application.json new file mode 100644 index 00000000..e124cbd4 --- /dev/null +++ b/tests/artifacts/inner-fee/application.json @@ -0,0 +1,202 @@ +{ + "name": "InnerFeeContract", + "structs": {}, + "methods": [ + { + "name": "burn_ops", + "args": [ + { + "type": "uint64", + "name": "op_budget" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "no_op", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_x_inners_with_fees", + "args": [ + { + "type": "uint64", + "name": "app_id" + }, + { + "type": "uint64[]", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_inners_with_fees", + "args": [ + { + "type": "uint64", + "name": "app_id_1" + }, + { + "type": "uint64", + "name": "app_id_2" + }, + { + "type": "(uint64,uint64,uint64,uint64,uint64[])", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_inners_with_fees_2", + "args": [ + { + "type": "uint64", + "name": "app_id_1" + }, + { + "type": "uint64", + "name": "app_id_2" + }, + { + "type": "(uint64,uint64,uint64[],uint64,uint64,uint64[])", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 0, + "bytes": 0 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 75, + 91, + 100, + 119, + 142 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 170 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 78, + 94, + 103, + 122, + 145 + ], + "errorMessage": "can only call when not creating" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmFwcHJvdmFsX3Byb2dyYW06CiAgICBpbnRjYmxvY2sgMSA2IDAgOAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHgwNjgxMDEKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgcHJvdG8gMCAxCiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDkKICAgIHB1c2hieXRlcyAweGRkMzc4MjQ3IC8vIG1ldGhvZCAiYnVybl9vcHModWludDY0KXZvaWQiCiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBwdXNoYnl0ZXNzIDB4MzQzNjgyY2QgMHgxY2YyZjU5MCAvLyBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0LCh1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjQsdWludDY0W10pKXZvaWQiLCBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0W10sdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIF9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDIgX19wdXlhX2FyYzRfcm91dGVyX19fbm9fb3Bfcm91dGVAMyBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDYKICAgIGludGNfMiAvLyAwCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIGJ1cm5fb3BzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19ub19vcF9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxOAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfcm91dGVANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX2lubmVyc193aXRoX2ZlZXMKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDY6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A5OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTMKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDEzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgaW50Y18yIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHMob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NS02CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBidXJuX29wcyhzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6Ny04CiAgICAvLyAjIFVzZXMgYXBwcm94IDYwIG9wIGJ1ZGdldCBwZXIgaXRlcmF0aW9uCiAgICAvLyBjb3VudCA9IG9wX2J1ZGdldCAvLyA2MAogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDYwIC8vIDYwCiAgICAvCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo5CiAgICAvLyBlbnN1cmVfYnVkZ2V0KG9wX2J1ZGdldCkKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18yIC8vIDAKICAgIGNhbGxzdWIgZW5zdXJlX2J1ZGdldAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBpbnRjXzIgLy8gMAoKYnVybl9vcHNfZm9yX2hlYWRlckAxOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IGJ1cm5fb3BzX2FmdGVyX2ZvckA0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMQogICAgLy8gc3FydCA9IG9wLmJzcXJ0KEJpZ1VJbnQoaSkpCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBpdG9iCiAgICBic3FydAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTIKICAgIC8vIGFzc2VydChzcXJ0ID49IDApICMgUHJldmVudCBvcHRpbWlzZXIgcmVtb3ZpbmcgdGhlIHNxcnQKICAgIHB1c2hieXRlcyAweAogICAgYj49CiAgICBhc3NlcnQKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjEwCiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBidXJuX29wc19mb3JfaGVhZGVyQDEKCmJ1cm5fb3BzX2FmdGVyX2ZvckA0OgogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLnRlc3RfY29udHJhY3QuY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWQ6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgtMTkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZDogVUludDY0LCBmZWVzOiBhcmM0LkR5bmFtaWNBcnJheVthcmM0LlVJbnQ2NF0pIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMiAvLyAwCiAgICBleHRyYWN0X3VpbnQxNgogICAgaW50Y18yIC8vIDAKCnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2Zvcl9oZWFkZXJAMToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1CiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMiAwCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBjb3ZlciAyCiAgICBpbnRjXzMgLy8gOAogICAgKgogICAgaW50Y18zIC8vIDgKICAgIGV4dHJhY3QzIC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjEKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZCwgZmVlPWZlZS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIGludGNfMCAvLyAxCiAgICArCiAgICBmcmFtZV9idXJ5IDEKICAgIGIgc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxCgpzZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19hZnRlcl9mb3JANToKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyMy0yNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2VuZF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZF8xOiBVSW50NjQsIGFwcF9pZF8yOiBVSW50NjQsIGZlZXM6IGFyYzQuVHVwbGVbYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI1CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjYKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozMAogICAgLy8gZmVlPWZlZXNbMl0ubmF0aXZlCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI5CiAgICAvLyByZWNlaXZlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGl0eG5fZmllbGQgUmVjZWl2ZXIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI4CiAgICAvLyBhbW91bnQ9MCwKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEFtb3VudAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjcKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzIKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNF0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbM10ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI0IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0b2IKICAgIGZyYW1lX2RpZyAtMQogICAgcHVzaGludCAzMiAvLyAzMgogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgc3dhcAogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXNfMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0LTM1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXNfMihzZWxmLCBhcHBfaWRfMTogVUludDY0LCBhcHBfaWRfMjogVUludDY0LCBmZWVzOiBhcmM0LlR1cGxlW2FyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdLCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM2CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzcKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbMl0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMV0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDE2IC8vIDE2CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDM0IC8vIDM0CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDIKICAgIGRpZyAyCiAgICBzdWJzdHJpbmczCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMSAvLyBtZXRob2QgInNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjRbXSl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGRpZyAyCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzgKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzkKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNV0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbNF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI2IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTEKICAgIGxlbgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDMKICAgIHVuY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgcmV0c3ViCgoKLy8gX3B1eWFfbGliLnV0aWwuZW5zdXJlX2J1ZGdldChyZXF1aXJlZF9idWRnZXQ6IHVpbnQ2NCwgZmVlX3NvdXJjZTogdWludDY0KSAtPiB2b2lkOgplbnN1cmVfYnVkZ2V0OgogICAgcHJvdG8gMiAwCiAgICBmcmFtZV9kaWcgLTIKICAgIHB1c2hpbnQgMTAgLy8gMTAKICAgICsKCmVuc3VyZV9idWRnZXRfd2hpbGVfdG9wQDE6CiAgICBmcmFtZV9kaWcgMAogICAgZ2xvYmFsIE9wY29kZUJ1ZGdldAogICAgPgogICAgYnogZW5zdXJlX2J1ZGdldF9hZnRlcl93aGlsZUA3CiAgICBpdHhuX2JlZ2luCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgcHVzaGludCA1IC8vIERlbGV0ZUFwcGxpY2F0aW9uCiAgICBpdHhuX2ZpZWxkIE9uQ29tcGxldGlvbgogICAgYnl0ZWNfMiAvLyAweDA2ODEwMQogICAgaXR4bl9maWVsZCBBcHByb3ZhbFByb2dyYW0KICAgIGJ5dGVjXzIgLy8gMHgwNjgxMDEKICAgIGl0eG5fZmllbGQgQ2xlYXJTdGF0ZVByb2dyYW0KICAgIGZyYW1lX2RpZyAtMQogICAgc3dpdGNoIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMEAzIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMUA0CiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMzoKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgYiBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANgoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV8xQDQ6CiAgICBnbG9iYWwgTWluVHhuRmVlCiAgICBpdHhuX2ZpZWxkIEZlZQoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDY6CiAgICBpdHhuX3N1Ym1pdAogICAgYiBlbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxCgplbnN1cmVfYnVkZ2V0X2FmdGVyX3doaWxlQDc6CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmNsZWFyX3N0YXRlX3Byb2dyYW06CiAgICBwdXNoaW50IDEgLy8gMQogICAgcmV0dXJuCg==" + }, + "events": [], + "templateVariables": {} +} diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py new file mode 100644 index 00000000..c57cbeda --- /dev/null +++ b/tests/artifacts/inner-fee/contract.py @@ -0,0 +1,49 @@ +from algopy import ( + ARC4Contract, + BigUInt, + Global, + UInt64, + arc4, + ensure_budget, + itxn, + op, + urange, +) + + +class InnerFeeContract(ARC4Contract): + @arc4.abimethod + def burn_ops(self, op_budget: UInt64) -> None: + # Uses approx 60 op budget per iteration + count = op_budget // 60 + ensure_budget(op_budget) + for i in urange(count): + sqrt = op.bsqrt(BigUInt(i)) + assert(sqrt >= 0) # Prevent optimiser removing the sqrt + + @arc4.abimethod + def no_op(self) -> None: + pass + + @arc4.abimethod + def send_x_inners_with_fees(self, app_id: UInt64, fees: arc4.DynamicArray[arc4.UInt64]) -> None: + for fee in fees: + arc4.abi_call('no_op', app_id=app_id, fee=fee.native) + + @arc4.abimethod + def send_inners_with_fees(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[1].native) + itxn.Payment( + amount=0, + receiver=Global.current_application_address, + fee=fees[2].native + ).submit() + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) + + @arc4.abimethod + def send_inners_with_fees_2(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64], arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[3].native) + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) diff --git a/tests/app_algorand_client.json b/tests/artifacts/nested_contract/application.json similarity index 99% rename from tests/app_algorand_client.json rename to tests/artifacts/nested_contract/application.json index de1411cc..1d96d437 100644 --- a/tests/app_algorand_client.json +++ b/tests/artifacts/nested_contract/application.json @@ -176,4 +176,4 @@ } ] } -} \ No newline at end of file +} diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index aa5fed4d..061ff6ce 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -1,3 +1,5 @@ +import dataclasses +import json from collections.abc import Generator from pathlib import Path @@ -13,10 +15,11 @@ AppClientMethodCallWithSendParams, FundAppAccountParams, ) -from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateWithSendParams from algokit_utils.config import config from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams @pytest.fixture @@ -420,3 +423,553 @@ def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: Account result = self.external_client.send.call(AppClientMethodCallWithSendParams(method="senderAssetBalance")) assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 + + +class TestCoverAppCallInnerFees: + """Test covering app call inner transaction fees""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Load inner fee contract spec + spec_path = Path(__file__).parent.parent / "artifacts" / "inner-fee" / "application.json" + inner_fee_spec = json.loads(spec_path.read_text()) + + # Create app factory + factory = algorand.client.get_app_factory(app_spec=inner_fee_spec, default_sender=funded_account.address) + + # Create 3 app instances + self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app1")) + self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app2")) + self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app3")) + + # Fund app accounts + for client in [self.app_client1, self.app_client2, self.app_client3]: + client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algos(2))) + + yield + + config.configure(populate_app_call_resources=False) + + def test_throws_when_no_max_fee(self) -> None: + """Test that error is thrown when no max fee is supplied""" + + params = AppClientMethodCallWithSendParams(method="no_op", cover_app_call_inner_txn_fees=True) + + with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): + self.app_client1.send.call(params) + + def test_throws_when_inner_fees_not_covered(self) -> None: + """Test that error is thrown when inner transaction fees are not covered""" + + expected_fee = 7000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=False, + ) + + with pytest.raises(Exception, match="fee too small"): + self.app_client1.send.call(params) + + def test_does_not_alter_fee_without_inners(self) -> None: + """Test that fee is not altered when app call has no inner transactions""" + + expected_fee = 1000 + params = AppClientMethodCallWithSendParams( + method="no_op", cover_app_call_inner_txn_fees=True, max_fee=AlgoAmount.from_micro_algos(2000) + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_throws_when_max_fee_too_small(self) -> None: + """Test that error is thrown when max fee is too small to cover inner fees""" + + expected_fee = 7000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + cover_app_call_inner_txn_fees=True, + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call(params) + + def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: + """Test that error is thrown when static fee is too small for inner transaction fees""" + + expected_fee = 7000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + cover_app_call_inner_txn_fees=True, + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call(params) + + def test_alters_fee_handling_when_no_itxns_covered(self) -> None: + """Test that fee handling is altered when no inner transaction fees are covered""" + + expected_fee = 7000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_all_inners_covered(self) -> None: + """Test that fee handling is altered when all inner transaction fees are covered""" + + expected_fee = 1000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_some_inners_covered(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 5300 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_when_some_inners_have_surplus(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 2000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fees(self) -> None: + """Test that fee handling is altered when multiple app calls are in a group with inners with varying fees""" + txn_1_expected_fee = 5800 + txn_2_expected_fee = 6000 + + txn_1_params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], + static_fee=AlgoAmount.from_micro_algos(txn_1_expected_fee), + note=b"txn_1", + ) + + txn_2_params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(txn_2_expected_fee), + note=b"txn_2", + ) + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) + .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) + .send(cover_app_call_inner_txn_fees=True) + ) + + assert result.transactions[0].raw.fee == txn_1_expected_fee + self._assert_min_fee(self.app_client1, txn_1_params, txn_1_expected_fee) + assert result.transactions[1].raw.fee == txn_2_expected_fee + self._assert_min_fee(self.app_client1, txn_2_params, txn_2_expected_fee) + + def test_does_not_alter_static_fee_with_surplus(self) -> None: + """Test that a static fee with surplus is not altered""" + + expected_fee = 6000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + static_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + + def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: + """Test fee handling with large inner fee surplus pooling to lower siblings""" + + expected_fee = 7000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: + """Test fee handling with inner fee surplus pooling to some lower siblings""" + + expected_fee = 6300 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: + """Test fee handling with large inner fee surplus but no pooling""" + + expected_fee = 10_000 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) -> None: + """Test fee handling with multiple inner fee surplus poolings to lower siblings""" + + expected_fee = 7100 + params = AppClientMethodCallWithSendParams( + method="send_inners_with_fees_2", + args=[ + self.app_client2.app_id, + self.app_client3.app_id, + [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], + ], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: Account) -> None: + """Test that fee is not altered when another transaction in group covers inner fees""" + + expected_fee = 8000 + + result = ( + self.app_client1.algorand.new_group() + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + ) + ) + .send(cover_app_call_inner_txn_fees=True) + ) + + assert result.transactions[0].raw.fee == expected_fee + # We could technically reduce the below to 0, however it adds more complexity + # and is probably unlikely to be a common use case + assert result.transactions[1].raw.fee == 1000 + + def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: Account) -> None: + """Test that surplus fees are allocated to the most fee constrained transaction first""" + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(2000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(7500), + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(0), + ) + ) + .send(cover_app_call_inner_txn_fees=True) + ) + + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 7500 + assert result.transactions[2].raw.fee == 0 + + def test_handles_nested_abi_method_calls(self, funded_account: Account) -> None: + """Test fee handling with nested ABI method calls""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + # Setup transaction parameters + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(4000), + ) + ) + + payment_params = PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(1500), + ) + + expected_fee = 2000 + params = AppClientMethodCallWithSendParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment(payment_params), + txn_arg_call, + ], + static_fee=AlgoAmount.from_micro_algos(expected_fee), + cover_app_call_inner_txn_fees=True, + ) + result = nested_client.send.call(params) + + assert len(result.transactions) == 3 + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 3500 + assert result.transactions[2].raw.fee == expected_fee + + self._assert_min_fee( + nested_client, + dataclasses.replace( + params, + args=[self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call], + ), + expected_fee, + ) + + def test_throws_when_max_fee_below_calculated(self) -> None: + """Test that error is thrown when max fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 1200 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(1200), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(10_000), + ) + ) + ) + .send(cover_app_call_inner_txn_fees=True) + ) + + def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Account) -> None: + """Test that error is thrown when nested max fee is below calculated fee""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(2000), + ) + ) + + with pytest.raises( + ValueError, match="Calculated transaction fee 5000 µALGO is greater than max of 2000 for transaction 1" + ): + nested_client.send.call( + AppClientMethodCallWithSendParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + ) + ), + txn_arg_call, + ], + cover_app_call_inner_txn_fees=True, + max_fee=AlgoAmount.from_micro_algos(10_000), + ) + ) + + def test_throws_when_static_fee_below_calculated(self) -> None: + """Test that error is thrown when static fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 5000 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(5000), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(10_000), + ) + ) + ) + .send(cover_app_call_inner_txn_fees=True) + ) + + def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Account) -> None: + """Test that error is thrown when static fee for non-app-call transaction is too low""" + + with pytest.raises( + ValueError, match="An additional fee of 500 µALGO is required for non app call transaction 2" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(13_000), + max_fee=AlgoAmount.from_micro_algos(14_000), + ) + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallWithSendParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(1000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(500), + ) + ) + .send(cover_app_call_inner_txn_fees=True) + ) + + def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: + """Test fee handling with expensive ABI method calls that use ensure_budget to op-up""" + + expected_fee = 10_000 + params = AppClientMethodCallWithSendParams( + method="burn_ops", + args=[6200], + cover_app_call_inner_txn_fees=True, + max_fee=AlgoAmount.from_micro_algos(12_000), + ) + result = self.app_client1.send.call(params) + + assert result.transaction.raw.fee == expected_fee + assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] + self._assert_min_fee(self.app_client1, params, expected_fee) + + def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallWithSendParams, fee: int) -> None: + """Helper to assert minimum required fee""" + if fee == 1000: + return + + params_copy = dataclasses.replace( + params, cover_app_call_inner_txn_fees=False, static_fee=None, extra_fee=None, suppress_log=True + ) + + with pytest.raises(Exception, match="fee too small"): + app_client.send.call(params_copy) From 42007ee11dd605669a009e913c727dc6d90f4b71 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sat, 25 Jan 2025 01:39:12 +0100 Subject: [PATCH 17/31] fix: further fixes revealed after refreshing generator against app call inner fees handling logic --- legacy_v2_tests/test_debug_utils.py | 16 +++---- src/algokit_utils/accounts/account_manager.py | 1 + src/algokit_utils/applications/app_client.py | 5 ++- src/algokit_utils/assets/asset_manager.py | 14 +++++- .../transactions/transaction_composer.py | 9 +++- tests/artifacts/inner-fee/contract.py | 45 ++++++++++++------- 6 files changed, 61 insertions(+), 29 deletions(-) diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index b0dbd47d..e75dd1b5 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -3,6 +3,13 @@ from unittest.mock import Mock import pytest +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + from algokit_utils._debugging import ( PersistSourceMapInput, persist_sourcemaps, @@ -13,20 +20,13 @@ from algokit_utils.application_specification import ApplicationSpecification from algokit_utils.common import Program from algokit_utils.models import Account -from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, - AtomicTransactionComposer, - TransactionWithSigner, -) -from algosdk.transaction import PaymentTxn - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -@pytest.fixture() +@pytest.fixture def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: creator_name = get_unique_name() creator = get_account(algod_client, creator_name) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 82ad7edd..4c3479fa 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -680,6 +680,7 @@ def ensure_funded( # noqa: PLR0913 max_rounds_to_wait=max_rounds_to_wait, suppress_log=suppress_log, populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, ) ) diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 05c9b6c0..e375930e 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -1914,7 +1914,7 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 sender: str, ) -> list[Any]: method = self._app_spec.get_arc56_method(method_name_or_signature) - result = [] + result: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] = [] for i, method_arg in enumerate(method.args): arg_value = args[i] if args and i < len(args) else None @@ -1990,6 +1990,9 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 f"No value provided for required argument " f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}" ) + elif arg_value is None and default_value is None: + # At this point only allow explicit None values if no default value was identified + result.append(None) return result diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 6173742a..619aebec 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -217,7 +217,12 @@ def bulk_opt_in( # noqa: PLR0913 ) composer.add_asset_opt_in(params) - result = composer.send(suppress_log=suppress_log) + result = composer.send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + ) for i, asset_id in enumerate(asset_group): results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) @@ -320,7 +325,12 @@ def bulk_opt_out( # noqa: C901, PLR0913 ) composer.add_asset_opt_out(params) - result = composer.send(suppress_log=suppress_log) + result = composer.send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + ) for i, asset_id in enumerate(asset_group): results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 7d997cd9..94f783dd 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -686,7 +686,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912 # Get fee parameters per_byte_txn_fee = suggested_params.fee if suggested_params else 0 - min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 + min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 # type: ignore[unused-ignore] # Simulate transactions result = empty_signer_atc.simulate(algod, simulate_request) @@ -1865,7 +1865,12 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 txns_for_group: list[TransactionWithSignerAndContext] = [] if params.args: - for _, arg in enumerate(reversed(params.args)): + for arg in reversed(params.args): + if arg is None and len(txns_for_group) > 0: + # Pull last transaction from group as placeholder + placeholder_transaction = txns_for_group.pop() + method_args.append(placeholder_transaction) + continue if self._is_abi_value(arg): method_args.append(arg) continue diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py index c57cbeda..bdc1bf4f 100644 --- a/tests/artifacts/inner-fee/contract.py +++ b/tests/artifacts/inner-fee/contract.py @@ -19,7 +19,7 @@ def burn_ops(self, op_budget: UInt64) -> None: ensure_budget(op_budget) for i in urange(count): sqrt = op.bsqrt(BigUInt(i)) - assert(sqrt >= 0) # Prevent optimiser removing the sqrt + assert sqrt >= 0 # Prevent optimiser removing the sqrt @arc4.abimethod def no_op(self) -> None: @@ -28,22 +28,35 @@ def no_op(self) -> None: @arc4.abimethod def send_x_inners_with_fees(self, app_id: UInt64, fees: arc4.DynamicArray[arc4.UInt64]) -> None: for fee in fees: - arc4.abi_call('no_op', app_id=app_id, fee=fee.native) + arc4.abi_call("no_op", app_id=app_id, fee=fee.native) @arc4.abimethod - def send_inners_with_fees(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[1].native) - itxn.Payment( - amount=0, - receiver=Global.current_application_address, - fee=fees[2].native - ).submit() - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) + def send_inners_with_fees( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[1].native) + itxn.Payment(amount=0, receiver=Global.current_application_address, fee=fees[2].native).submit() + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) @arc4.abimethod - def send_inners_with_fees_2(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64], arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[3].native) - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) + def send_inners_with_fees_2( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[ + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + ], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[3].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) From 8da2dfeb04a1138763f0e0042993f0842ea32bfe Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sat, 25 Jan 2025 02:32:52 +0100 Subject: [PATCH 18/31] fix: add missing extra_program_pages calculation in method txn creation --- .../transactions/transaction_composer.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 94f783dd..8975b4d4 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1958,12 +1958,29 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 if arg.context.max_fee is not None: max_fees[atc_index] = arg.context.max_fee + app_id = params.app_id or 0 + approval_program = getattr(params, "approval_program", None) + clear_program = getattr(params, "clear_state_program", None) + extra_pages = None + + if app_id == 0: + extra_pages = getattr(params, "extra_program_pages", None) + if extra_pages is None and approval_program is not None: + approval_len, clear_len = len(approval_program), len(clear_program or b"") + extra_pages = ( + int(math.floor((approval_len + clear_len) / algosdk.constants.APP_PAGE_MAX_SIZE)) + if approval_len + else 0 + ) + txn_params = { - "app_id": params.app_id if params.app_id is not None else 0, + "app_id": app_id, "method": params.method, "sender": params.sender, "sp": suggested_params, - "signer": params.signer if params.signer is not None else self._get_signer(params.sender), + "signer": params.signer + if params.signer is not None + else self._get_signer(params.sender) or algosdk.atomic_transaction_composer.EmptySigner(), "method_args": list(reversed(method_args)), "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, "note": params.note, @@ -1986,9 +2003,10 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 ) if params.schema else None, - "approval_program": getattr(params, "approval_program", None), - "clear_program": getattr(params, "clear_state_program", None), + "approval_program": approval_program, + "clear_program": clear_program, "rekey_to": params.rekey_to, + "extra_pages": extra_pages, } def _add_method_call_and_return_txn(x: dict) -> algosdk.transaction.Transaction: From 1df5083b0e206948b5302bd8c81e33eb2a322de5 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sun, 26 Jan 2025 02:51:52 +0100 Subject: [PATCH 19/31] refactor: reusing OnSchemaBreak, OnUpdate, OperationPerformed --- src/algokit_utils/__init__.py | 4 +- src/algokit_utils/_legacy_v2/deploy.py | 39 +------------------ .../applications/app_deployer.py | 28 ++++++------- .../transactions/transaction_composer.py | 3 +- 4 files changed, 18 insertions(+), 56 deletions(-) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 0bbf868d..a621005d 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -11,6 +11,7 @@ # Core types and utilities that are commonly used from algokit_utils.models.account import Account +from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.errors.logic_error import LogicError from algokit_utils.algorand import AlgorandClient @@ -87,9 +88,6 @@ DeployCreateCallArgsDict, DeploymentFailedError, DeployResponse, - OnSchemaBreak, - OnUpdate, - OperationPerformed, TemplateValueDict, TemplateValueMapping, get_app_id_from_tx_id, diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index 3ec196f1..fc73f84b 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -4,7 +4,6 @@ import logging import re from collections.abc import Iterable, Mapping, Sequence -from enum import Enum from typing import TYPE_CHECKING, TypeAlias, TypedDict import algosdk @@ -25,6 +24,7 @@ CreateCallParameters, TransactionResponse, ) +from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_manager import AppManager from algokit_utils.models.account import Account @@ -458,43 +458,6 @@ def get(key: OnCompleteActionName) -> CallConfig: return get("clear_state") -class OnUpdate(Enum): - """Action to take if an Application has been updated""" - - Fail = 0 - """Fail the deployment""" - UpdateApp = 1 - """Update the Application with the new approval and clear programs""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new application""" - - -class OnSchemaBreak(Enum): - """Action to take if an Application's schema has breaking changes""" - - Fail = 0 - """Fail the deployment""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new Application""" - - -class OperationPerformed(Enum): - """Describes the actions taken during deployment""" - - Nothing = 0 - """An existing Application was found""" - Create = 1 - """No existing Application was found, created a new Application""" - Update = 2 - """An existing Application was found, but was out of date, updated to latest version""" - Replace = 3 - """An existing Application was found, but was out of date, created a new Application and deleted the original""" - - @dataclasses.dataclass(kw_only=True) class DeployResponse: """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`""" diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 50140be3..536765d9 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -116,40 +116,40 @@ class AppLookup: apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) -class OnSchemaBreak(str, Enum): +class OnSchemaBreak(Enum): """Action to take if an Application's schema has breaking changes""" - Fail = "fail" + Fail = 0 """Fail the deployment""" - ReplaceApp = "replace_app" + ReplaceApp = 2 """Create a new Application and delete the old Application in a single transaction""" - AppendApp = "append_app" + AppendApp = 3 """Create a new Application""" -class OnUpdate(str, Enum): +class OnUpdate(Enum): """Action to take if an Application has been updated""" - Fail = "fail" + Fail = 0 """Fail the deployment""" - UpdateApp = "update_app" + UpdateApp = 1 """Update the Application with the new approval and clear programs""" - ReplaceApp = "replace_app" + ReplaceApp = 2 """Create a new Application and delete the old Application in a single transaction""" - AppendApp = "append_app" + AppendApp = 3 """Create a new application""" -class OperationPerformed(str, Enum): +class OperationPerformed(Enum): """Describes the actions taken during deployment""" - Nothing = "nothing" + Nothing = 0 """An existing Application was found""" - Create = "create" + Create = 1 """No existing Application was found, created a new Application""" - Update = "update" + Update = 2 """An existing Application was found, but was out of date, updated to latest version""" - Replace = "replace" + Replace = 3 """An existing Application was found, but was out of date, created a new Application and deleted the original""" diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 8975b4d4..684bfde1 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -23,7 +23,6 @@ from algosdk.v2client.models.simulate_request import SimulateRequest from typing_extensions import deprecated -from algokit_utils._debugging import simulate_and_persist_response, simulate_response from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method @@ -1185,6 +1184,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 :raises Exception: If there is an error sending the transactions :raises error: If there is an error from the Algorand node """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response try: # Build transactions @@ -1683,6 +1683,7 @@ def simulate( :param skip_signatures: Whether to skip signature validation :return: The simulation results """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response atc = AtomicTransactionComposer() if skip_signatures else self._atc From d01d90e08c30bc498938d36c482a0a584f262958 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sun, 26 Jan 2025 06:08:53 +0100 Subject: [PATCH 20/31] refactor: refine dataclasses; moving send and compilation params into explicit args --- src/algokit_utils/accounts/account_manager.py | 38 +- .../accounts/kmd_account_manager.py | 3 +- src/algokit_utils/applications/app_client.py | 269 ++++++----- .../applications/app_deployer.py | 2 +- src/algokit_utils/applications/app_factory.py | 160 ++++--- src/algokit_utils/assets/asset_manager.py | 48 +- src/algokit_utils/models/transaction.py | 14 +- src/algokit_utils/protocols/typed_clients.py | 2 +- .../transactions/transaction_composer.py | 100 ++--- .../transactions/transaction_sender.py | 184 +++++--- tests/applications/test_app_client.py | 44 +- tests/applications/test_app_factory.py | 85 ++-- tests/assets/test_asset_manager.py | 2 +- tests/test_debug_utils.py | 16 +- tests/transactions/test_resource_packing.py | 416 +++++++++++------- .../transactions/test_transaction_composer.py | 8 +- 16 files changed, 763 insertions(+), 628 deletions(-) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 4c3479fa..9443ce17 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -15,6 +15,7 @@ from algokit_utils.config import config from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account, MultiSigAccount, MultisigMetadata from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( PaymentParams, SendAtomicTransactionComposerResults, @@ -568,7 +569,6 @@ def rekey_account( # noqa: PLR0913 validity_window=validity_window, first_valid_round=first_valid_round, last_valid_round=last_valid_round, - suppress_log=suppress_log, ) ) .send() @@ -590,10 +590,7 @@ def ensure_funded( # noqa: PLR0913 min_spending_balance: AlgoAmount, min_funding_increment: AlgoAmount | None = None, # Sender params - max_rounds_to_wait: int | None = None, - suppress_log: bool | None = None, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + send_params: SendParams | None = None, # Common txn params signer: TransactionSigner | None = None, rekey_to: str | None = None, @@ -619,10 +616,7 @@ def ensure_funded( # noqa: PLR0913 :param min_spending_balance: The minimum balance of Algo that the account should have available to spend :param min_funding_increment: Optional minimum funding increment - :param max_rounds_to_wait: Optional maximum rounds to wait for transaction - :param suppress_log: Optional flag to suppress logging - :param populate_app_call_resources: Optional flag to populate app call resources - :param cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees + :param send_params: Parameters for the send operation, defaults to None :param signer: Optional transaction signer :param rekey_to: Optional rekey address :param note: Optional transaction note @@ -673,15 +667,9 @@ def ensure_funded( # noqa: PLR0913 validity_window=validity_window, first_valid_round=first_valid_round, last_valid_round=last_valid_round, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, ) ) - .send( - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, - ) + .send(send_params) ) return EnsureFundedResponse( @@ -703,10 +691,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 *, # Force remaining params to be keyword-only min_funding_increment: AlgoAmount | None = None, # SendParams - max_rounds_to_wait: int | None = None, - suppress_log: bool | None = None, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + send_params: SendParams | None = None, # Common transaction params (omitting sender) signer: TransactionSigner | None = None, rekey_to: str | None = None, @@ -732,10 +717,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 :param min_spending_balance: The minimum balance of Algo that the account should have available to spend :param min_funding_increment: Optional minimum funding increment - :param max_rounds_to_wait: Optional maximum rounds to wait for transaction - :param suppress_log: Optional flag to suppress logging - :param populate_app_call_resources: Optional flag to populate app call resources - :param cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees + :param send_params: Parameters for the send operation, defaults to None :param signer: Optional transaction signer :param rekey_to: Optional rekey address :param note: Optional transaction note @@ -791,15 +773,9 @@ def ensure_funded_from_environment( # noqa: PLR0913 validity_window=validity_window, first_valid_round=first_valid_round, last_valid_round=last_valid_round, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, - populate_app_call_resources=populate_app_call_resources, ) ) - .send( - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - ) + .send(send_params) ) return EnsureFundedResponse( diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index a7dc6636..a7f8d142 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -105,6 +105,7 @@ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = :param fund_with: The number of Algos to fund the account with when created :return: An Algorand account with private key loaded """ + fund_with = fund_with or AlgoAmount.from_algo(1000) existing = self.get_wallet_account(name) if existing: @@ -132,7 +133,7 @@ def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = PaymentParams( sender=dispenser.address, receiver=account.address, - amount=fund_with or AlgoAmount.from_algo(1000), + amount=fund_with, ) ).send() return account diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index e375930e..79c3bf74 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -6,7 +6,7 @@ import os from collections.abc import Sequence from dataclasses import dataclass, fields -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar import algosdk from algosdk.source_map import SourceMap @@ -42,7 +42,7 @@ CompiledTeal, ) from algokit_utils.models.state import BoxName, BoxValue -from algokit_utils.models.transaction import SendParams +from algokit_utils.models.transaction import AppCallSendParams, SendParams from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, @@ -75,22 +75,16 @@ "AppClient", "AppClientBareCallCreateParams", "AppClientBareCallParams", - "AppClientBareCallWithCallOnCompleteParams", - "AppClientBareCallWithCompilationAndSendParams", - "AppClientBareCallWithCompilationParams", - "AppClientBareCallWithSendParams", "AppClientCallParams", "AppClientCompilationParams", "AppClientCompilationResult", "AppClientCreateSchema", "AppClientMethodCallCreateParams", "AppClientMethodCallParams", - "AppClientMethodCallWithCompilationAndSendParams", - "AppClientMethodCallWithCompilationParams", - "AppClientMethodCallWithSendParams", "AppClientParams", "AppSourceMaps", "BaseAppClientMethodCallParams", + "CreateOnComplete", "FundAppAccountParams", "get_constant_block_offset", ] @@ -166,6 +160,15 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 return max(bytecblock_offset or 0, intcblock_offset or 0) +CreateOnComplete = Literal[ + OnComplete.NoOpOC, + OnComplete.UpdateApplicationOC, + OnComplete.DeleteApplicationOC, + OnComplete.OptInOC, + OnComplete.CloseOutOC, +] + + @dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: """Result of compiling an application's TEAL code. @@ -184,8 +187,7 @@ class AppClientCompilationResult: compiled_clear: CompiledTeal | None = None -@dataclass(kw_only=True, frozen=True) -class AppClientCompilationParams: +class AppClientCompilationParams(TypedDict, total=False): """Parameters for compiling an application's TEAL code. :ivar deploy_time_params: Optional template parameters to use during compilation @@ -193,9 +195,9 @@ class AppClientCompilationParams: :ivar deletable: Optional flag indicating if app should be deletable """ - deploy_time_params: TealTemplateParams | None = None - updatable: bool | None = None - deletable: bool | None = None + deploy_time_params: TealTemplateParams | None + updatable: bool | None + deletable: bool | None @dataclass(kw_only=True) @@ -328,23 +330,6 @@ class AppClientMethodCallParams( """Parameters for application method calls.""" -@dataclass(kw_only=True, frozen=True) -class AppClientMethodCallWithCompilationParams(AppClientMethodCallParams, AppClientCompilationParams): - """Combined parameters for method calls with compilation.""" - - -@dataclass(kw_only=True, frozen=True) -class AppClientMethodCallWithSendParams(AppClientMethodCallParams, SendParams): - """Combined parameters for method calls with send options.""" - - -@dataclass(kw_only=True, frozen=True) -class AppClientMethodCallWithCompilationAndSendParams( - AppClientMethodCallParams, AppClientCompilationParams, SendParams -): - """Combined parameters for method calls with compilation and send options.""" - - @dataclass(kw_only=True, frozen=True) class AppClientBareCallParams: """Parameters for bare application calls. @@ -397,40 +382,19 @@ class AppClientCreateSchema: schema: AppCreateSchema | None = None -@dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCompilationParams(AppClientBareCallParams, AppClientCompilationParams): - """Combined parameters for bare calls with compilation.""" - - -@dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithSendParams(AppClientBareCallParams, SendParams): - """Combined parameters for bare calls with send options.""" - - -@dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, AppClientCompilationParams, SendParams): - """Combined parameters for bare calls with compilation and send options.""" - - -@dataclass(kw_only=True, frozen=True) -class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams): - """Parameters for bare calls with on complete action. - - :ivar on_complete: Optional on complete action - """ - - on_complete: algosdk.transaction.OnComplete | None = None - - @dataclass(frozen=True) -class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallWithCallOnCompleteParams): +class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallParams): """Parameters for creating application with bare call.""" + on_complete: OnComplete | None = None + @dataclass(frozen=True) class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams): """Parameters for creating application with method call.""" + on_complete: CreateOnComplete | None = None + class _AppClientStateMethods: def __init__( @@ -676,7 +640,7 @@ def __init__(self, client: AppClient) -> None: self._app_spec = client._app_spec def _get_bare_params( - self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete + self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete | None = None ) -> dict[str, Any]: params = params or {} sender = self._client._get_sender(params.get("sender")) @@ -685,10 +649,13 @@ def _get_bare_params( "app_id": self._app_id, "sender": sender, "signer": self._client._get_signer(params.get("sender"), params.get("signer")), - "on_complete": on_complete, + "on_complete": on_complete or OnComplete.NoOpOC, } - def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> AppUpdateParams: + def update( + self, + params: AppClientBareCallParams | None = None, + ) -> AppUpdateParams: """Create parameters for updating an application. :param params: Optional compilation and send parameters, defaults to None @@ -699,7 +666,7 @@ def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = ) return call_params - def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + def opt_in(self, params: AppClientBareCallParams | None = None) -> AppCallParams: """Create parameters for opting into an application. :param params: Optional send parameters, defaults to None @@ -710,7 +677,7 @@ def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCa ) return call_params - def delete(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + def delete(self, params: AppClientBareCallParams | None = None) -> AppCallParams: """Create parameters for deleting an application. :param params: Optional send parameters, defaults to None @@ -721,7 +688,7 @@ def delete(self, params: AppClientBareCallWithSendParams | None = None) -> AppCa ) return call_params - def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + def clear_state(self, params: AppClientBareCallParams | None = None) -> AppCallParams: """Create parameters for clearing application state. :param params: Optional send parameters, defaults to None @@ -732,7 +699,7 @@ def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> ) return call_params - def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + def close_out(self, params: AppClientBareCallParams | None = None) -> AppCallParams: """Create parameters for closing out of an application. :param params: Optional send parameters, defaults to None @@ -743,14 +710,17 @@ def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Ap ) return call_params - def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> AppCallParams: + def call( + self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC + ) -> AppCallParams: """Create parameters for calling an application. :param params: Optional call parameters with on complete action, defaults to None + :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC :return: Parameters for calling the application """ call_params: AppCallParams = AppCallParams( - **self._get_bare_params(params.__dict__ if params else {}, OnComplete.NoOpOC) + **self._get_bare_params(params.__dict__ if params else {}, on_complete or OnComplete.NoOpOC) ) return call_params @@ -800,7 +770,9 @@ def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: :param params: Parameters for the opt-in call :return: Parameters for opting into the application """ - input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.OptInOC + ) return AppCallMethodCallParams(**input_params) def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: @@ -809,7 +781,9 @@ def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: :param params: Parameters for the method call :return: Parameters for calling the application method """ - input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC + ) return AppCallMethodCallParams(**input_params) def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: @@ -819,32 +793,35 @@ def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams :return: Parameters for deleting the application """ input_params = self._get_abi_params( - params.__dict__, on_complete=algosdk.transaction.OnComplete.DeleteApplicationOC + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.DeleteApplicationOC ) return AppDeleteMethodCallParams(**input_params) def update( - self, params: AppClientMethodCallParams | AppClientMethodCallWithCompilationAndSendParams + self, params: AppClientMethodCallParams, compilation_params: AppClientCompilationParams | None = None ) -> AppUpdateMethodCallParams: """Create parameters for updating an application. :param params: Parameters for the update call, optionally including compilation parameters + :param compilation_params: Parameters for the compilation, defaults to None :return: Parameters for updating the application """ compile_params = ( self._client.compile( app_spec=self._client.app_spec, app_manager=self._algorand.app, - deploy_time_params=params.deploy_time_params, - updatable=params.updatable, - deletable=params.deletable, + deploy_time_params=compilation_params.get("deploy_time_params"), + updatable=compilation_params.get("updatable"), + deletable=compilation_params.get("deletable"), ).__dict__ - if isinstance(params, AppClientMethodCallWithCompilationAndSendParams) + if compilation_params else {} ) input_params = { - **self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.UpdateApplicationOC), + **self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.UpdateApplicationOC + ), **compile_params, } # Filter input_params to include only fields valid for AppUpdateMethodCallParams @@ -858,7 +835,9 @@ def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParam :param params: Parameters for the close-out call :return: Parameters for closing out of the application """ - input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.CloseOutOC) + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.CloseOutOC + ) return AppCallMethodCallParams(**input_params) def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: @@ -885,7 +864,7 @@ def __init__(self, client: AppClient) -> None: self._client = client self._algorand = client._algorand - def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> Transaction: + def update(self, params: AppClientBareCallParams | None = None) -> Transaction: """Create a transaction to update an application. Creates a transaction that will update an existing application with new approval and clear state programs. @@ -894,10 +873,10 @@ def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = :return: The constructed application update transaction """ return self._algorand.create_transaction.app_update( - self._client.params.bare.update(params or AppClientBareCallWithCompilationAndSendParams()) + self._client.params.bare.update(params or AppClientBareCallParams()) ) - def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + def opt_in(self, params: AppClientBareCallParams | None = None) -> Transaction: """Create a transaction to opt into an application. Creates a transaction that will opt the sender account into using this application. @@ -906,10 +885,10 @@ def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> Trans :return: The constructed opt-in transaction """ return self._algorand.create_transaction.app_call( - self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) + self._client.params.bare.opt_in(params or AppClientBareCallParams()) ) - def delete(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + def delete(self, params: AppClientBareCallParams | None = None) -> Transaction: """Create a transaction to delete an application. Creates a transaction that will delete this application from the blockchain. @@ -918,10 +897,10 @@ def delete(self, params: AppClientBareCallWithSendParams | None = None) -> Trans :return: The constructed delete transaction """ return self._algorand.create_transaction.app_call( - self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) + self._client.params.bare.delete(params or AppClientBareCallParams()) ) - def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + def clear_state(self, params: AppClientBareCallParams | None = None) -> Transaction: """Create a transaction to clear application state. Creates a transaction that will clear the sender's local state for this application. @@ -930,10 +909,10 @@ def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> :return: The constructed clear state transaction """ return self._algorand.create_transaction.app_call( - self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) + self._client.params.bare.clear_state(params or AppClientBareCallParams()) ) - def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Transaction: + def close_out(self, params: AppClientBareCallParams | None = None) -> Transaction: """Create a transaction to close out of an application. Creates a transaction that will close out the sender's participation in this application. @@ -942,19 +921,22 @@ def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> Tr :return: The constructed close out transaction """ return self._algorand.create_transaction.app_call( - self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) + self._client.params.bare.close_out(params or AppClientBareCallParams()) ) - def call(self, params: AppClientBareCallWithCallOnCompleteParams | None = None) -> Transaction: + def call( + self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC + ) -> Transaction: """Create a transaction to call an application. Creates a transaction that will call this application with the specified parameters. :param params: Parameters for the application call including on complete action, defaults to None + :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC :return: The constructed application call transaction """ return self._algorand.create_transaction.app_call( - self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) + self._client.params.bare.call(params or AppClientBareCallParams(), on_complete or OnComplete.NoOpOC) ) @@ -1040,7 +1022,9 @@ def __init__(self, client: AppClient) -> None: def update( self, - params: AppClientBareCallWithCompilationAndSendParams | None = None, + params: AppClientBareCallParams | None = None, + send_params: AppCallSendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> SendAppTransactionResult[ABIReturn]: """Send an application update transaction. @@ -1048,89 +1032,111 @@ def update( :param params: The parameters for the update call, including optional compilation parameters, deploy time parameters, and transaction configuration + :param send_params: Send parameters, defaults to None + :param compilation_params: Parameters for the compilation, defaults to None :return: The result of sending the transaction, including compilation artifacts and ABI return value if applicable """ - params = params or AppClientBareCallWithCompilationAndSendParams() - compiled = self._client.compile_app(params.deploy_time_params, params.updatable, params.deletable) + params = params or AppClientBareCallParams() + compilation = compilation_params or AppClientCompilationParams() + compiled = self._client.compile_app( + compilation.get("deploy_time_params"), compilation.get("updatable"), compilation.get("deletable") + ) bare_params = self._client.params.bare.update(params) bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) - call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params)) + call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params, send_params)) return SendAppTransactionResult[ABIReturn]( **{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}, abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)), ) - def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + def opt_in( + self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Send an application opt-in transaction. Creates and sends a transaction that will opt the sender's account into this application. :param params: Parameters for the opt-in call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( - self._client.params.bare.opt_in(params or AppClientBareCallWithSendParams()) + self._client.params.bare.opt_in(params or AppClientBareCallParams()), send_params ) ) - def delete(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + def delete( + self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Send an application delete transaction. Creates and sends a transaction that will delete this application. :param params: Parameters for the delete call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( - self._client.params.bare.delete(params or AppClientBareCallWithSendParams()) + self._client.params.bare.delete(params or AppClientBareCallParams()), send_params ) ) - def clear_state(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + def clear_state( + self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Send an application clear state transaction. Creates and sends a transaction that will clear the sender's local state for this application. :param params: Parameters for the clear state call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( - self._client.params.bare.clear_state(params or AppClientBareCallWithSendParams()) + self._client.params.bare.clear_state(params or AppClientBareCallParams()), send_params ) ) - def close_out(self, params: AppClientBareCallWithSendParams | None = None) -> SendAppTransactionResult[ABIReturn]: + def close_out( + self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Send an application close out transaction. Creates and sends a transaction that will close out the sender's participation in this application. :param params: Parameters for the close out call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( - self._client.params.bare.close_out(params or AppClientBareCallWithSendParams()) + self._client.params.bare.close_out(params or AppClientBareCallParams()), send_params ) ) def call( - self, params: AppClientBareCallWithCallOnCompleteParams | None = None + self, + params: AppClientBareCallParams | None = None, + on_complete: OnComplete | None = None, + send_params: AppCallSendParams | None = None, ) -> SendAppTransactionResult[ABIReturn]: """Send an application call transaction. Creates and sends a transaction that will call this application with the specified parameters. :param params: Parameters for the application call including transaction options, defaults to None + :param on_complete: The OnComplete action, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._algorand.send.app_call( - self._client.params.bare.call(params or AppClientBareCallWithCallOnCompleteParams()) + self._client.params.bare.call(params or AppClientBareCallParams(), on_complete), send_params ) ) @@ -1151,89 +1157,111 @@ def bare(self) -> _AppClientBareSendAccessor: """ return self._bare_send_accessor - def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + def fund_app_account( + self, params: FundAppAccountParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Send funds to the application account. Creates and sends a payment transaction to fund the application account. :param params: Parameters for funding the app account including amount and transaction options + :param send_params: Send parameters, defaults to None :return: The result of sending the payment transaction """ return self._client._handle_call_errors( # type: ignore[no-any-return] - lambda: self._algorand.send.payment(self._client.params.fund_app_account(params)) + lambda: self._algorand.send.payment(self._client.params.fund_app_account(params), send_params) ) - def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + def opt_in( + self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application opt-in transaction. Creates and sends a transaction that will opt the sender into this application. :param params: Parameters for the opt-in call including method and transaction options + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( - lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)), + lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params), send_params), self._app_spec.get_arc56_method(params.method), ) ) - def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + def delete( + self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application delete transaction. Creates and sends a transaction that will delete this application. :param params: Parameters for the delete call including method and transaction options + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( - lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)), + lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params), send_params), self._app_spec.get_arc56_method(params.method), ) ) def update( - self, params: AppClientMethodCallWithCompilationAndSendParams + self, + params: AppClientMethodCallParams, + compilation_params: AppClientCompilationParams | None = None, + send_params: AppCallSendParams | None = None, ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]: """Send an application update transaction. Creates and sends a transaction that will update this application's program. :param params: Parameters for the update call including method, compilation and transaction options + :param compilation_params: Parameters for the compilation, defaults to None + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ result = self._client._handle_call_errors( lambda: self._client._process_method_call_return( - lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)), + lambda: self._algorand.send.app_update_method_call( + self._client.params.update(params, compilation_params), send_params + ), self._app_spec.get_arc56_method(params.method), ) ) assert isinstance(result, SendAppUpdateTransactionResult) return result - def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + def close_out( + self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application close out transaction. Creates and sends a transaction that will close out the sender's participation in this application. :param params: Parameters for the close out call including method and transaction options + :param send_params: Send parameters, defaults to None :return: The result of sending the transaction, including ABI return value if applicable """ return self._client._handle_call_errors( lambda: self._client._process_method_call_return( - lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)), + lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params), send_params), self._app_spec.get_arc56_method(params.method), ) ) - def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult[Arc56ReturnValueType]: + def call( + self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application call transaction. Creates and sends a transaction that will call this application with the specified parameters. For read-only calls, simulates the transaction instead of sending it. :param params: Parameters for the application call including method and transaction options + :param send_params: Send parameters :return: The result of sending or simulating the transaction, including ABI return value if applicable """ is_read_only_call = ( @@ -1244,10 +1272,10 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( self._client.params.call(params) ) - + send_params = send_params or AppCallSendParams() simulate_response = self._client._handle_call_errors( lambda: method_call_to_simulate.simulate( - allow_unnamed_resources=params.populate_app_call_resources or True, + allow_unnamed_resources=send_params.get("populate_app_call_resources") or True, skip_signatures=True, allow_more_logs=True, allow_empty_signatures=True, @@ -1272,7 +1300,7 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR return self._client._handle_call_errors( lambda: self._client._process_method_call_return( - lambda: self._algorand.send.app_call_method_call(self._client.params.call(params)), + lambda: self._algorand.send.app_call_method_call(self._client.params.call(params), send_params), self._app_spec.get_arc56_method(params.method), ) ) @@ -1847,13 +1875,16 @@ def get_box_values_from_abi_type( return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] - def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + def fund_app_account( + self, params: FundAppAccountParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Fund the application's account. :param params: The funding parameters + :param send_params: Send parameters, defaults to None :return: The transaction result """ - return self.send.fund_app_account(params) + return self.send.fund_app_account(params, send_params) def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception: source_info = None @@ -1939,7 +1970,7 @@ def _get_abi_args_with_default_values( # noqa: C901, PLR0912 default_method = self._app_spec.get_arc56_method(default_value.data) empty_args = [None] * len(default_method.args) call_result = self.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method=default_value.data, args=empty_args, sender=sender, diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 536765d9..3c680d09 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -169,7 +169,7 @@ class AppDeployParams: max_fee: int | None = None max_rounds_to_wait: int | None = None suppress_log: bool = False - populate_app_call_resources: bool = False + populate_app_call_resources: bool | None = None cover_app_call_inner_txn_fees: bool | None = None diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index f432998c..4ba282e9 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -1,8 +1,8 @@ import base64 import dataclasses from collections.abc import Callable, Sequence -from dataclasses import asdict, dataclass, replace -from typing import Any, Generic, Literal, TypeVar +from dataclasses import asdict, dataclass +from typing import Any, Generic, TypeVar from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.source_map import SourceMap @@ -27,6 +27,7 @@ AppClientMethodCallCreateParams, AppClientMethodCallParams, AppClientParams, + CreateOnComplete, ) from algokit_utils.applications.app_deployer import ( AppDeployMetaData, @@ -44,7 +45,7 @@ AppSourceMaps, ) from algokit_utils.models.state import TealTemplateParams -from algokit_utils.models.transaction import SendParams +from algokit_utils.models.transaction import AppCallSendParams from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCallParams, AppCreateParams, @@ -67,9 +68,7 @@ "AppFactory", "AppFactoryCreateMethodCallParams", "AppFactoryCreateMethodCallResult", - "AppFactoryCreateMethodCallWithSendParams", "AppFactoryCreateParams", - "AppFactoryCreateWithSendParams", "AppFactoryDeployResponse", "AppFactoryParams", "SendAppCreateFactoryTransactionResult", @@ -92,17 +91,8 @@ class AppFactoryParams: @dataclass(kw_only=True, frozen=True) -class _AppFactoryCreateBaseParams(AppClientCreateSchema, AppClientCompilationParams): - on_complete: ( - Literal[ - OnComplete.NoOpOC, - OnComplete.UpdateApplicationOC, - OnComplete.DeleteApplicationOC, - OnComplete.OptInOC, - OnComplete.CloseOutOC, - ] - | None - ) = None +class _AppFactoryCreateBaseParams(AppClientCreateSchema): + on_complete: CreateOnComplete | None = None @dataclass(kw_only=True, frozen=True) @@ -110,11 +100,6 @@ class AppFactoryCreateParams(_AppFactoryCreateBaseParams, AppClientBareCallParam pass -@dataclass(kw_only=True, frozen=True) -class AppFactoryCreateWithSendParams(AppFactoryCreateParams, SendParams): - pass - - @dataclass(kw_only=True, frozen=True) class AppFactoryCreateMethodCallParams(_AppFactoryCreateBaseParams, AppClientMethodCallParams): pass @@ -135,11 +120,6 @@ class AppFactoryCreateMethodCallResult(SendSingleTransactionResult, Generic[ABIR abi_return: ABIReturnT | None = None -@dataclass(kw_only=True, frozen=True) -class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, SendParams): - pass - - @dataclass(frozen=True) class SendAppFactoryTransactionResult(SendAppTransactionResult[Arc56ReturnValueType]): pass @@ -225,10 +205,11 @@ def __init__(self, factory: "AppFactory") -> None: self._factory = factory self._algorand = factory._algorand - def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParams: + def create( + self, params: AppFactoryCreateParams | None = None, compilation_params: AppClientCompilationParams | None = None + ) -> AppCreateParams: base_params = params or AppFactoryCreateParams() - - compiled = self._factory.compile(base_params) + compiled = self._factory.compile(compilation_params) return AppCreateParams( **{ @@ -298,8 +279,10 @@ def __init__(self, factory: "AppFactory") -> None: def bare(self) -> _BareParamsBuilder: return self._bare - def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCallParams: - compiled = self._factory.compile(params) + def create( + self, params: AppFactoryCreateMethodCallParams, compilation_params: AppClientCompilationParams | None = None + ) -> AppCreateMethodCallParams: + compiled = self._factory.compile(compilation_params) return AppCreateMethodCallParams( **{ @@ -396,32 +379,34 @@ def __init__(self, factory: "AppFactory") -> None: self._algorand = factory._algorand def create( - self, params: AppFactoryCreateWithSendParams | None = None + self, + params: AppFactoryCreateParams | None = None, + send_params: AppCallSendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> tuple[AppClient, SendAppCreateTransactionResult]: - base_params = params or AppFactoryCreateWithSendParams() - - # Use replace() to create new instance with overridden values - create_params = replace( - base_params, - updatable=base_params.updatable if base_params.updatable is not None else self._factory._updatable, - deletable=base_params.deletable if base_params.deletable is not None else self._factory._deletable, - deploy_time_params=( - base_params.deploy_time_params - if base_params.deploy_time_params is not None - else self._factory._deploy_time_params - ), + compilation_params = compilation_params or AppClientCompilationParams() + compilation_params["updatable"] = ( + compilation_params.get("updatable") + if compilation_params.get("updatable") is not None + else self._factory._updatable ) - - compiled = self._factory.compile( - AppClientCompilationParams( - deploy_time_params=create_params.deploy_time_params, - updatable=create_params.updatable, - deletable=create_params.deletable, - ) + compilation_params["deletable"] = ( + compilation_params.get("deletable") + if compilation_params.get("deletable") is not None + else self._factory._deletable + ) + compilation_params["deploy_time_params"] = ( + compilation_params.get("deploy_time_params") + if compilation_params.get("deploy_time_params") is not None + else self._factory._deploy_time_params ) + compiled = self._factory.compile(compilation_params) + result = self._factory._handle_call_errors( - lambda: self._algorand.send.app_create(self._factory.params.bare.create(create_params)) + lambda: self._algorand.send.app_create( + self._factory.params.bare.create(params, compilation_params), send_params + ) ) return ( @@ -454,30 +439,34 @@ def bare(self) -> _AppFactoryBareSendAccessor: return self._bare def create( - self, params: AppFactoryCreateMethodCallParams + self, + params: AppFactoryCreateMethodCallParams, + send_params: AppCallSendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> tuple[AppClient, AppFactoryCreateMethodCallResult[Arc56ReturnValueType]]: - create_params = replace( - params, - updatable=params.updatable if params.updatable is not None else self._factory._updatable, - deletable=params.deletable if params.deletable is not None else self._factory._deletable, - deploy_time_params=( - params.deploy_time_params - if params.deploy_time_params is not None - else self._factory._deploy_time_params - ), + compilation_params = compilation_params or AppClientCompilationParams() + compilation_params["updatable"] = ( + compilation_params.get("updatable") + if compilation_params.get("updatable") is not None + else self._factory._updatable ) - - compiled = self._factory.compile( - AppClientCompilationParams( - deploy_time_params=create_params.deploy_time_params, - updatable=create_params.updatable, - deletable=create_params.deletable, - ) + compilation_params["deletable"] = ( + compilation_params.get("deletable") + if compilation_params.get("deletable") is not None + else self._factory._deletable + ) + compilation_params["deploy_time_params"] = ( + compilation_params.get("deploy_time_params") + if compilation_params.get("deploy_time_params") is not None + else self._factory._deploy_time_params ) + compiled = self._factory.compile(compilation_params) result = self._factory._handle_call_errors( lambda: self._factory._parse_method_call_return( - lambda: self._algorand.send.app_create_method_call(self._factory.params.create(create_params)), + lambda: self._algorand.send.app_create_method_call( + self._factory.params.create(params, compilation_params), send_params + ), self._factory._app_spec.get_arc56_method(params.method), ) ) @@ -561,7 +550,7 @@ def deploy( # noqa: PLR0913 app_name: str | None = None, max_rounds_to_wait: int | None = None, suppress_log: bool = False, - populate_app_call_resources: bool = False, + populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, ) -> tuple[AppClient, AppFactoryDeployResponse]: """Deploy the application with the specified parameters.""" @@ -580,20 +569,24 @@ def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: return self.params.create( AppFactoryCreateMethodCallParams( **asdict(create_params), - updatable=resolved_updatable, - deletable=resolved_deletable, - deploy_time_params=resolved_deploy_time_params, - ) + ), + compilation_params={ + "updatable": resolved_updatable, + "deletable": resolved_deletable, + "deploy_time_params": resolved_deploy_time_params, + }, ) base_params = create_params or AppClientBareCallCreateParams() return self.params.bare.create( AppFactoryCreateParams( **asdict(base_params) if base_params else {}, - updatable=resolved_updatable, - deletable=resolved_deletable, - deploy_time_params=resolved_deploy_time_params, - ) + ), + compilation_params={ + "updatable": resolved_updatable, + "deletable": resolved_deletable, + "deploy_time_params": resolved_deploy_time_params, + }, ) def prepare_update_args() -> AppUpdateMethodCallParams | AppUpdateParams: @@ -718,13 +711,14 @@ def import_source_maps(self, source_maps: AppSourceMaps) -> None: self._approval_source_map = source_maps.approval_source_map self._clear_source_map = source_maps.clear_source_map - def compile(self, compilation: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + def compile(self, compilation_params: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + compilation = compilation_params or AppClientCompilationParams() result = AppClient.compile( app_spec=self._app_spec, app_manager=self._algorand.app, - deploy_time_params=compilation.deploy_time_params if compilation else None, - updatable=compilation.updatable if compilation else None, - deletable=compilation.deletable if compilation else None, + deploy_time_params=compilation.get("deploy_time_params") if compilation else None, + updatable=compilation.get("updatable") if compilation else None, + deletable=compilation.get("deletable") if compilation else None, ) if result.compiled_approval: diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 619aebec..8385c415 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -7,6 +7,7 @@ from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( AssetOptInParams, AssetOptOutParams, @@ -153,11 +154,6 @@ def bulk_opt_in( # noqa: PLR0913 self, account: str | Account | TransactionSigner, asset_ids: list[int], - *, - suppress_log: bool = False, - max_rounds_to_wait: int | None = None, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, signer: TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, @@ -168,16 +164,12 @@ def bulk_opt_in( # noqa: PLR0913 validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, + send_params: SendParams | None = None, ) -> list[BulkAssetOptInOutResult]: """Opt an account in to a list of Algorand Standard Assets. :param account: The account to opt-in :param asset_ids: The list of asset IDs to opt-in to - :param suppress_log: Whether to suppress logging, defaults to False - :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, - defaults to None - :param populate_app_call_resources: Whether to populate app call resources, defaults to None - :param cover_app_call_inner_txn_fees: Whether to cover app call inner transaction fees, defaults to None :param signer: The signer to use for the transaction, defaults to None :param rekey_to: The address to rekey the account to, defaults to None :param note: The note to include in the transaction, defaults to None @@ -188,6 +180,7 @@ def bulk_opt_in( # noqa: PLR0913 :param validity_window: The validity window to include in the transaction, defaults to None :param first_valid_round: The first valid round to include in the transaction, defaults to None :param last_valid_round: The last valid round to include in the transaction, defaults to None + :param send_params: The send parameters to use for the transaction, defaults to None :return: An array of records matching asset ID to transaction ID of the opt in """ results: list[BulkAssetOptInOutResult] = [] @@ -200,10 +193,6 @@ def bulk_opt_in( # noqa: PLR0913 params = AssetOptInParams( sender=sender, asset_id=asset_id, - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, signer=signer, rekey_to=rekey_to, note=note, @@ -217,12 +206,7 @@ def bulk_opt_in( # noqa: PLR0913 ) composer.add_asset_opt_in(params) - result = composer.send( - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, - ) + result = composer.send(send_params) for i, asset_id in enumerate(asset_group): results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) @@ -231,14 +215,10 @@ def bulk_opt_in( # noqa: PLR0913 def bulk_opt_out( # noqa: C901, PLR0913 self, + *, account: str | Account | TransactionSigner, asset_ids: list[int], - *, ensure_zero_balance: bool = True, - suppress_log: bool = False, - max_rounds_to_wait: int | None = None, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, signer: TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, @@ -249,17 +229,13 @@ def bulk_opt_out( # noqa: C901, PLR0913 validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, + send_params: SendParams | None = None, ) -> list[BulkAssetOptInOutResult]: """Opt an account out of a list of Algorand Standard Assets. :param account: The account to opt-out :param asset_ids: The list of asset IDs to opt-out of :param ensure_zero_balance: Whether to check if the account has a zero balance first, defaults to True - :param suppress_log: Whether to suppress logging, defaults to False - :param max_rounds_to_wait: The maximum number of rounds to wait for the transaction to be confirmed, - defaults to None - :param populate_app_call_resources: Whether to populate app call resources, defaults to None - :param cover_app_call_inner_txn_fees: Whether to cover app call inner transaction fees, defaults to None :param signer: The signer to use for the transaction, defaults to None :param rekey_to: The address to rekey the account to, defaults to None :param note: The note to include in the transaction, defaults to None @@ -270,6 +246,7 @@ def bulk_opt_out( # noqa: C901, PLR0913 :param validity_window: The validity window to include in the transaction, defaults to None :param first_valid_round: The first valid round to include in the transaction, defaults to None :param last_valid_round: The last valid round to include in the transaction, defaults to None + :param send_params: The send parameters to use for the transaction, defaults to None :raises ValueError: If ensure_zero_balance is True and account has non-zero balance or is not opted in :return: An array of records matching asset ID to transaction ID of the opt out """ @@ -308,10 +285,6 @@ def bulk_opt_out( # noqa: C901, PLR0913 sender=sender, asset_id=asset_id, creator=asset_info.creator, - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, signer=signer, rekey_to=rekey_to, note=note, @@ -325,12 +298,7 @@ def bulk_opt_out( # noqa: C901, PLR0913 ) composer.add_asset_opt_out(params) - result = composer.send( - max_rounds_to_wait=max_rounds_to_wait, - suppress_log=suppress_log, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, - ) + result = composer.send(send_params) for i, asset_id in enumerate(asset_group): results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 51ad1d93..99232918 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -91,12 +91,14 @@ def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT: raise ValueError(f"Transaction is not of type {txn_type.__name__}") -@dataclass(kw_only=True, frozen=True) -class SendParams: - max_rounds_to_wait: int | None = None - suppress_log: bool | None = None - populate_app_call_resources: bool | None = None - cover_app_call_inner_txn_fees: bool | None = None +class SendParams(TypedDict, total=False): + max_rounds_to_wait: int | None + suppress_log: bool | None + + +class AppCallSendParams(SendParams, total=False): + populate_app_call_resources: bool | None + cover_app_call_inner_txn_fees: bool | None @dataclass(kw_only=True, frozen=True) diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index e3da7221..7f142521 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -105,6 +105,6 @@ def deploy( # noqa: PLR0913 app_name: str | None = None, max_rounds_to_wait: int | None = None, suppress_log: bool = False, - populate_app_call_resources: bool = False, + populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 684bfde1..dc555950 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -18,7 +18,7 @@ TransactionSigner, TransactionWithSigner, ) -from algosdk.transaction import OnComplete +from algosdk.transaction import ApplicationCallTxn, OnComplete, SuggestedParams from algosdk.v2client.algod import AlgodClient from algosdk.v2client.models.simulate_request import SimulateRequest from typing_extensions import deprecated @@ -28,7 +28,7 @@ from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method from algokit_utils.config import config from algokit_utils.models.state import BoxIdentifier, BoxReference -from algokit_utils.models.transaction import SendParams, TransactionWrapper +from algokit_utils.models.transaction import AppCallSendParams, SendParams, TransactionWrapper if TYPE_CHECKING: from collections.abc import Callable @@ -96,12 +96,13 @@ class _CommonTxnParams: @dataclass(kw_only=True, frozen=True) -class _CommonTxnWithSendParams(_CommonTxnParams, SendParams): - pass +class AdditionalAtcContext: + max_fees: dict[int, AlgoAmount] | None = None + suggested_params: SuggestedParams | None = None @dataclass(kw_only=True, frozen=True) -class PaymentParams(_CommonTxnWithSendParams): +class PaymentParams(_CommonTxnParams): """Parameters for a payment transaction. :ivar receiver: The account that will receive the ALGO @@ -116,7 +117,7 @@ class PaymentParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetCreateParams(_CommonTxnWithSendParams): +class AssetCreateParams(_CommonTxnParams): """Parameters for creating a new asset. :ivar total: The total amount of the smallest divisible unit to create @@ -146,7 +147,7 @@ class AssetCreateParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetConfigParams(_CommonTxnWithSendParams): +class AssetConfigParams(_CommonTxnParams): """Parameters for configuring an existing asset. :ivar asset_id: ID of the asset @@ -164,7 +165,7 @@ class AssetConfigParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetFreezeParams(_CommonTxnWithSendParams): +class AssetFreezeParams(_CommonTxnParams): """Parameters for freezing an asset. :ivar asset_id: The ID of the asset @@ -178,7 +179,7 @@ class AssetFreezeParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetDestroyParams(_CommonTxnWithSendParams): +class AssetDestroyParams(_CommonTxnParams): """Parameters for destroying an asset. :ivar asset_id: ID of the asset @@ -188,7 +189,7 @@ class AssetDestroyParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class OnlineKeyRegistrationParams(_CommonTxnWithSendParams): +class OnlineKeyRegistrationParams(_CommonTxnParams): """Parameters for online key registration. :ivar vote_key: The root participation public key @@ -208,7 +209,7 @@ class OnlineKeyRegistrationParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class OfflineKeyRegistrationParams(_CommonTxnWithSendParams): +class OfflineKeyRegistrationParams(_CommonTxnParams): """Parameters for offline key registration. :ivar prevent_account_from_ever_participating_again: Whether to prevent the account from ever participating again @@ -218,7 +219,7 @@ class OfflineKeyRegistrationParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetTransferParams(_CommonTxnWithSendParams): +class AssetTransferParams(_CommonTxnParams): """Parameters for transferring an asset. :ivar asset_id: ID of the asset @@ -236,7 +237,7 @@ class AssetTransferParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetOptInParams(_CommonTxnWithSendParams): +class AssetOptInParams(_CommonTxnParams): """Parameters for opting into an asset. :ivar asset_id: ID of the asset @@ -246,7 +247,7 @@ class AssetOptInParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AssetOptOutParams(_CommonTxnWithSendParams): +class AssetOptOutParams(_CommonTxnParams): """Parameters for opting out of an asset. :ivar asset_id: ID of the asset @@ -258,7 +259,7 @@ class AssetOptOutParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AppCallParams(_CommonTxnWithSendParams): +class AppCallParams(_CommonTxnParams): """Parameters for calling an application. :ivar on_complete: The OnComplete action @@ -295,7 +296,7 @@ class AppCreateSchema(TypedDict): @dataclass(kw_only=True, frozen=True) -class AppCreateParams(_CommonTxnWithSendParams): +class AppCreateParams(_CommonTxnParams): """Parameters for creating an application. :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) @@ -325,7 +326,7 @@ class AppCreateParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AppUpdateParams(_CommonTxnWithSendParams): +class AppUpdateParams(_CommonTxnParams): """Parameters for updating an application. :ivar app_id: ID of the application @@ -353,7 +354,7 @@ class AppUpdateParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AppDeleteParams(_CommonTxnWithSendParams): +class AppDeleteParams(_CommonTxnParams): """Parameters for deleting an application. :ivar app_id: ID of the application @@ -375,7 +376,7 @@ class AppDeleteParams(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class _BaseAppMethodCall(_CommonTxnWithSendParams): +class _BaseAppMethodCall(_CommonTxnParams): app_id: int method: Method args: list | None = None @@ -387,7 +388,7 @@ class _BaseAppMethodCall(_CommonTxnWithSendParams): @dataclass(kw_only=True, frozen=True) -class AppMethodCallParams(_CommonTxnWithSendParams): +class AppMethodCallParams(_CommonTxnParams): """Parameters for calling an application method. :ivar app_id: ID of the application @@ -647,10 +648,12 @@ def _get_group_execution_info( # noqa: C901, PLR0912 algod: AlgodClient, populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, - max_fees: dict[int, AlgoAmount] | None = None, - suggested_params: algosdk.transaction.SuggestedParams | None = None, + additional_atc_context: AdditionalAtcContext | None = None, ) -> ExecutionInfo: # Create simulation request + suggested_params = additional_atc_context.suggested_params if additional_atc_context else None + max_fees = additional_atc_context.max_fees if additional_atc_context else None + simulate_request = SimulateRequest( txn_groups=[], allow_unnamed_resources=True, @@ -808,8 +811,7 @@ def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 algod: AlgodClient, populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, - max_fees: dict[int, AlgoAmount] | None = None, - suggested_params: algosdk.transaction.SuggestedParams | None = None, + additional_atc_context: AdditionalAtcContext | None = None, ) -> AtomicTransactionComposer: """Prepare a transaction group for sending by handling execution info and resources. @@ -817,14 +819,14 @@ def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 :param algod: Algod client for simulation :param populate_app_call_resources: Whether to populate app call resources :param cover_app_call_inner_txn_fees: Whether to cover inner txn fees - :param max_fees: Max fees allowed per transaction index - :param suggested_params: Suggested transaction parameters + :param additional_atc_context: Additional context for the AtomicTransactionComposer :return: Modified AtomicTransactionComposer ready for sending """ # Get execution info via simulation execution_info = _get_group_execution_info( - atc, algod, populate_app_call_resources, cover_app_call_inner_txn_fees, max_fees, suggested_params + atc, algod, populate_app_call_resources, cover_app_call_inner_txn_fees, additional_atc_context ) + max_fees = additional_atc_context.max_fees if additional_atc_context else None group = atc.build_group() @@ -1164,8 +1166,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, - max_fees: dict[int, AlgoAmount] | None = None, - suggested_params: algosdk.transaction.SuggestedParams | None = None, + additional_atc_context: AdditionalAtcContext | None = None, ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group. @@ -1178,8 +1179,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 :param suppress_log: If True, suppress logging, defaults to None :param populate_app_call_resources: If True, populate app call resources, defaults to None :param cover_app_call_inner_txn_fees: If True, cover app call inner transaction fees, defaults to None - :param max_fees: Optional max fees for each transaction, defaults to None - :param suggested_params: Optional suggested params for each transaction, defaults to None + :param additional_atc_context: Additional context for the AtomicTransactionComposer :return: Results from sending the transaction group :raises Exception: If there is an error sending the transactions :raises error: If there is an error from the Algorand node @@ -1204,8 +1204,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 algod, populate_app_call_resources, cover_app_call_inner_txn_fees, - max_fees, - suggested_params, + additional_atc_context, ) transactions_to_send = [t.txn for t in transactions_with_signer] @@ -1606,29 +1605,28 @@ def execute( *, max_rounds_to_wait: int | None = None, ) -> SendAtomicTransactionComposerResults: - return self.send( - max_rounds_to_wait=max_rounds_to_wait, - ) + return self.send(SendParams(max_rounds_to_wait=max_rounds_to_wait)) def send( self, - *, - max_rounds_to_wait: int | None = None, - suppress_log: bool | None = None, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + params: SendParams | AppCallSendParams | None = None, ) -> SendAtomicTransactionComposerResults: """Send the transaction group to the network. - :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation - :param suppress_log: Whether to suppress transaction logging - :param populate_app_call_resources: Whether to populate app call resources - :param cover_app_call_inner_txn_fees: Whether to cover inner transaction fees for app calls + :param params: Parameters for the send operation :return: The transaction send results :raises Exception: If the transaction fails """ group = self.build().transactions - wait_rounds = max_rounds_to_wait + + if not params: + has_app_call = any(isinstance(txn.txn, ApplicationCallTxn) for txn in group) + params = AppCallSendParams() if has_app_call else SendParams() + + cover_app_call_inner_txn_fees: bool | None = params.get("cover_app_call_inner_txn_fees") # type: ignore[assignment] + populate_app_call_resources: bool | None = params.get("populate_app_call_resources") # type: ignore[assignment] + + wait_rounds = params.get("max_rounds_to_wait") sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_txn_fees else None if wait_rounds is None: last_round = max(txn.txn.last_valid_round for txn in group) @@ -1641,11 +1639,13 @@ def send( self._atc, self._algod, max_rounds_to_wait=wait_rounds, - max_fees=self._txn_max_fees, - suppress_log=suppress_log, + suppress_log=params.get("suppress_log"), populate_app_call_resources=populate_app_call_resources, cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, - suggested_params=sp, + additional_atc_context=AdditionalAtcContext( + suggested_params=sp, + max_fees=self._txn_max_fees, + ), ) except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e @@ -1801,7 +1801,7 @@ def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSign def _common_txn_build_step( # noqa: C901 self, build_txn: Callable[[dict], algosdk.transaction.Transaction], - params: _CommonTxnWithSendParams, + params: _CommonTxnParams, txn_params: dict, ) -> TransactionWithContext: # Clone suggested params diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index eae6fa3f..007e0e81 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -11,7 +11,7 @@ from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.config import config -from algokit_utils.models.transaction import TransactionWrapper +from algokit_utils.models.transaction import AppCallSendParams, SendParams, TransactionWrapper from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, @@ -48,7 +48,7 @@ logger = config.logger -T = TypeVar("T", bound=TxnParams) +TxnParamsT = TypeVar("TxnParamsT", bound=TxnParams) @dataclass(frozen=True, kw_only=True) @@ -176,11 +176,11 @@ def new_group(self) -> TransactionComposer: def _send( self, - c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], - pre_log: Callable[[T, Transaction], str] | None = None, - post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendSingleTransactionResult]: - def send_transaction(params: T) -> SendSingleTransactionResult: + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, SendParams | None], SendSingleTransactionResult]: + def send_transaction(params: TxnParamsT, send_params: SendParams | None = None) -> SendSingleTransactionResult: composer = self.new_group() c(composer)(params) @@ -189,10 +189,7 @@ def send_transaction(params: T) -> SendSingleTransactionResult: logger.debug(pre_log(params, transaction)) raw_result = composer.send( - populate_app_call_resources=params.populate_app_call_resources, - cover_app_call_inner_txn_fees=params.cover_app_call_inner_txn_fees, - max_rounds_to_wait=params.max_rounds_to_wait, - suppress_log=params.suppress_log, + send_params, ) raw_result_dict = raw_result.__dict__.copy() raw_result_dict["transactions"] = raw_result.transactions @@ -214,12 +211,14 @@ def send_transaction(params: T) -> SendSingleTransactionResult: def _send_app_call( self, - c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], - pre_log: Callable[[T, Transaction], str] | None = None, - post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppTransactionResult[ABIReturn]]: - def send_app_call(params: T) -> SendAppTransactionResult[ABIReturn]: - result = self._send(c, pre_log, post_log)(params) + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppTransactionResult[ABIReturn]]: + def send_app_call( + params: TxnParamsT, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + result = self._send(c, pre_log, post_log)(params, send_params) return SendAppTransactionResult[ABIReturn]( **result.__dict__, abi_return=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), @@ -229,12 +228,14 @@ def send_app_call(params: T) -> SendAppTransactionResult[ABIReturn]: def _send_app_update_call( self, - c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], - pre_log: Callable[[T, Transaction], str] | None = None, - post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppUpdateTransactionResult[ABIReturn]]: - def send_app_update_call(params: T) -> SendAppUpdateTransactionResult[ABIReturn]: - result = self._send_app_call(c, pre_log, post_log)(params) + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppUpdateTransactionResult[ABIReturn]]: + def send_app_update_call( + params: TxnParamsT, send_params: AppCallSendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: + result = self._send_app_call(c, pre_log, post_log)(params, send_params) if not isinstance( params, AppCreateParams | AppUpdateParams | AppCreateMethodCallParams | AppUpdateMethodCallParams @@ -262,12 +263,14 @@ def send_app_update_call(params: T) -> SendAppUpdateTransactionResult[ABIReturn] def _send_app_create_call( self, - c: Callable[[TransactionComposer], Callable[[T], TransactionComposer]], - pre_log: Callable[[T, Transaction], str] | None = None, - post_log: Callable[[T, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[T], SendAppCreateTransactionResult[ABIReturn]]: - def send_app_create_call(params: T) -> SendAppCreateTransactionResult[ABIReturn]: - result = self._send_app_update_call(c, pre_log, post_log)(params) + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppCreateTransactionResult[ABIReturn]]: + def send_app_create_call( + params: TxnParamsT, send_params: AppCallSendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: + result = self._send_app_update_call(c, pre_log, post_log)(params, send_params) app_id = int(result.confirmation["application-index"]) # type: ignore[call-overload] return SendAppCreateTransactionResult[ABIReturn]( @@ -283,10 +286,11 @@ def _get_method_call_for_log(self, method: algosdk.abi.Method, args: list[Any]) args_str = str([str(a) if not isinstance(a, bytes | bytearray) else a.hex() for a in args]) return f"{method.name}({args_str})" - def payment(self, params: PaymentParams) -> SendSingleTransactionResult: + def payment(self, params: PaymentParams, send_params: SendParams | None = None) -> SendSingleTransactionResult: """Send a payment transaction to transfer Algo between accounts. :param params: Payment transaction parameters + :param send_params: Send parameters :return: Result of the payment transaction """ return self._send( @@ -295,12 +299,15 @@ def payment(self, params: PaymentParams) -> SendSingleTransactionResult: f"Sending {params.amount} from {params.sender} to {params.receiver} " f"via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransactionResult: + def asset_create( + self, params: AssetCreateParams, send_params: SendParams | None = None + ) -> SendSingleAssetCreateTransactionResult: """Create a new Algorand Standard Asset. :param params: Asset creation parameters + :param send_params: Send parameters :return: Result containing the new asset ID """ result = self._send( @@ -312,17 +319,20 @@ def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransa f"{params.sender} with ID {result.confirmation['asset-index']} via transaction " # type: ignore[call-overload] f"{result.tx_ids[-1]}" ), - )(params) + )(params, send_params) return SendSingleAssetCreateTransactionResult( **result.__dict__, asset_id=int(result.confirmation["asset-index"]), # type: ignore[call-overload] ) - def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: + def asset_config( + self, params: AssetConfigParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Configure an existing Algorand Standard Asset. :param params: Asset configuration parameters + :param send_params: Send parameters :return: Result of the configuration transaction """ return self._send( @@ -330,12 +340,15 @@ def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult pre_log=lambda params, transaction: ( f"Configuring asset with ID {params.asset_id} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def asset_freeze(self, params: AssetFreezeParams) -> SendSingleTransactionResult: + def asset_freeze( + self, params: AssetFreezeParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Freeze or unfreeze an Algorand Standard Asset for an account. :param params: Asset freeze parameters + :param send_params: Send parameters :return: Result of the freeze transaction """ return self._send( @@ -343,12 +356,15 @@ def asset_freeze(self, params: AssetFreezeParams) -> SendSingleTransactionResult pre_log=lambda params, transaction: ( f"Freezing asset with ID {params.asset_id} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def asset_destroy(self, params: AssetDestroyParams) -> SendSingleTransactionResult: + def asset_destroy( + self, params: AssetDestroyParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Destroys an Algorand Standard Asset. :param params: Asset destruction parameters + :param send_params: Send parameters :return: Result of the destroy transaction """ return self._send( @@ -356,12 +372,15 @@ def asset_destroy(self, params: AssetDestroyParams) -> SendSingleTransactionResu pre_log=lambda params, transaction: ( f"Destroying asset with ID {params.asset_id} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def asset_transfer(self, params: AssetTransferParams) -> SendSingleTransactionResult: + def asset_transfer( + self, params: AssetTransferParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Transfer an Algorand Standard Asset. :param params: Asset transfer parameters + :param send_params: Send parameters :return: Result of the transfer transaction """ return self._send( @@ -370,12 +389,15 @@ def asset_transfer(self, params: AssetTransferParams) -> SendSingleTransactionRe f"Transferring {params.amount} units of asset with ID {params.asset_id} from " f"{params.sender} to {params.receiver} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def asset_opt_in(self, params: AssetOptInParams) -> SendSingleTransactionResult: + def asset_opt_in( + self, params: AssetOptInParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Opt an account into an Algorand Standard Asset. :param params: Asset opt-in parameters + :param send_params: Send parameters :return: Result of the opt-in transaction """ return self._send( @@ -384,17 +406,19 @@ def asset_opt_in(self, params: AssetOptInParams) -> SendSingleTransactionResult: f"Opting in {params.sender} to asset with ID {params.asset_id} via transaction " f"{transaction.get_txid()}" ), - )(params) + )(params, send_params) def asset_opt_out( self, *, params: AssetOptOutParams, + send_params: SendParams | None = None, ensure_zero_balance: bool = True, ) -> SendSingleTransactionResult: """Opt an account out of an Algorand Standard Asset. :param params: Asset opt-out parameters + :param send_params: Send parameters :param ensure_zero_balance: Check if account has zero balance before opt-out, defaults to True :raises ValueError: If account has non-zero balance or is not opted in :return: Result of the opt-out transaction @@ -427,76 +451,103 @@ def asset_opt_out( f"Opting {params.sender} out of asset with ID {params.asset_id} to creator " f"{creator} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def app_create(self, params: AppCreateParams) -> SendAppCreateTransactionResult[ABIReturn]: + def app_create( + self, params: AppCreateParams, send_params: AppCallSendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: """Create a new application. :param params: Application creation parameters + :param send_params: Send parameters :return: Result containing the new application ID and address """ - return self._send_app_create_call(lambda c: c.add_app_create)(params) + return self._send_app_create_call(lambda c: c.add_app_create)(params, send_params) - def app_update(self, params: AppUpdateParams) -> SendAppUpdateTransactionResult[ABIReturn]: + def app_update( + self, params: AppUpdateParams, send_params: AppCallSendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: """Update an application. :param params: Application update parameters + :param send_params: Send parameters :return: Result containing the compiled programs """ - return self._send_app_update_call(lambda c: c.add_app_update)(params) + return self._send_app_update_call(lambda c: c.add_app_update)(params, send_params) - def app_delete(self, params: AppDeleteParams) -> SendAppTransactionResult[ABIReturn]: + def app_delete( + self, params: AppDeleteParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Delete an application. :param params: Application deletion parameters + :param send_params: Send parameters :return: Result of the deletion transaction """ - return self._send_app_call(lambda c: c.add_app_delete)(params) + return self._send_app_call(lambda c: c.add_app_delete)(params, send_params) - def app_call(self, params: AppCallParams) -> SendAppTransactionResult[ABIReturn]: + def app_call( + self, params: AppCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Call an application. - :param params: Application call parameters + :param params: Application call parameters + :param send_params: Send parameters :return: Result containing any ABI return value """ - return self._send_app_call(lambda c: c.add_app_call)(params) + return self._send_app_call(lambda c: c.add_app_call)(params, send_params) - def app_create_method_call(self, params: AppCreateMethodCallParams) -> SendAppCreateTransactionResult[ABIReturn]: + def app_create_method_call( + self, params: AppCreateMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: """Call an application's create method. :param params: Method call parameters for application creation + :param send_params: Send parameters :return: Result containing the new application ID and address """ - return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params) + return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params, send_params) - def app_update_method_call(self, params: AppUpdateMethodCallParams) -> SendAppUpdateTransactionResult[ABIReturn]: + def app_update_method_call( + self, params: AppUpdateMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: """Call an application's update method. :param params: Method call parameters for application update + :param send_params: Send parameters :return: Result containing the compiled programs """ - return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params) + return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params, send_params) - def app_delete_method_call(self, params: AppDeleteMethodCallParams) -> SendAppTransactionResult[ABIReturn]: + def app_delete_method_call( + self, params: AppDeleteMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Call an application's delete method. :param params: Method call parameters for application deletion + :param send_params: Send parameters :return: Result of the deletion transaction """ - return self._send_app_call(lambda c: c.add_app_delete_method_call)(params) + return self._send_app_call(lambda c: c.add_app_delete_method_call)(params, send_params) - def app_call_method_call(self, params: AppCallMethodCallParams) -> SendAppTransactionResult[ABIReturn]: + def app_call_method_call( + self, params: AppCallMethodCallParams, send_params: AppCallSendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: """Call an application's call method. :param params: Method call parameters + :param send_params: Send parameters :return: Result containing any ABI return value """ - return self._send_app_call(lambda c: c.add_app_call_method_call)(params) + return self._send_app_call(lambda c: c.add_app_call_method_call)(params, send_params) - def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSingleTransactionResult: + def online_key_registration( + self, params: OnlineKeyRegistrationParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Register an online key. :param params: Key registration parameters + :param send_params: Send parameters :return: Result of the registration transaction """ return self._send( @@ -504,12 +555,15 @@ def online_key_registration(self, params: OnlineKeyRegistrationParams) -> SendSi pre_log=lambda params, transaction: ( f"Registering online key for {params.sender} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) - def offline_key_registration(self, params: OfflineKeyRegistrationParams) -> SendSingleTransactionResult: + def offline_key_registration( + self, params: OfflineKeyRegistrationParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: """Register an offline key. :param params: Key registration parameters + :param send_params: Send parameters :return: Result of the registration transaction """ return self._send( @@ -517,4 +571,4 @@ def offline_key_registration(self, params: OfflineKeyRegistrationParams) -> Send pre_log=lambda params, transaction: ( f"Registering offline key for {params.sender} via transaction {transaction.get_txid()}" ), - )(params) + )(params, send_params) diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index 64a26abf..ad78719b 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -13,7 +13,7 @@ from algokit_utils.applications.abi import ABIType from algokit_utils.applications.app_client import ( AppClient, - AppClientMethodCallWithSendParams, + AppClientMethodCallParams, AppClientParams, FundAppAccountParams, ) @@ -307,7 +307,7 @@ def test_resolve_from_network( def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: call = test_app_client.create_transaction.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi", args=["test"], box_references=[BoxReference(app_id=0, name=b"1")], @@ -319,7 +319,7 @@ def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: # Test with string box reference call2 = test_app_client.create_transaction.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi", args=["test"], box_references=["1"], @@ -345,7 +345,7 @@ def test_construct_transaction_with_abi_encoding_including_transaction( # Call the ABI method with the payment transaction result = test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi_txn", args=[payment_txn, "test"], ) @@ -386,7 +386,7 @@ def sign_transactions( return original_signer.sign_transactions(txn_group, indexes) test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi_txn", args=[txn, "test"], sender=funded_account.address, @@ -420,7 +420,7 @@ def test_sign_transaction_in_group_with_different_signer_if_provided( # Call method with transaction and signer test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi_txn", args=[TransactionWithSigner(txn=txn, signer=test_account.signer), "test"], ) @@ -439,7 +439,7 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no ) result = test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="call_abi_foreign_refs", app_references=[345], account_references=[test_account.address], @@ -460,9 +460,7 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: # Test global state - test_app_client.send.call( - AppClientMethodCallWithSendParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) - ) + test_app_client.send.call(AppClientMethodCallParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])])) global_state = test_app_client.get_global_state() assert "int1" in global_state @@ -477,10 +475,8 @@ def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> assert global_state["bytes2"].value_raw == bytes([1, 2, 3, 4]) # Test local state - test_app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) - test_app_client.send.call( - AppClientMethodCallWithSendParams(method="set_local", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) - ) + test_app_client.send.opt_in(AppClientMethodCallParams(method="opt_in")) + test_app_client.send.call(AppClientMethodCallParams(method="set_local", args=[1, 2, "asdf", bytes([1, 2, 3, 4])])) local_state = test_app_client.get_local_state(funded_account.address) assert "local_int1" in local_state @@ -502,14 +498,14 @@ def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> test_app_client.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="set_box", args=[box_name1, "value1"], box_references=[box_name1], ) ) test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="set_box", args=[box_name2, "value2"], box_references=[box_name2], @@ -532,7 +528,7 @@ def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> expected_value_decoded = "1234524352" expected_value = "\x00\n" + expected_value_decoded test_app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="set_box", args=[box_name1, expected_value], box_references=[box_name1], @@ -602,7 +598,7 @@ def test_box_methods_with_manually_encoded_abi_args( # Call the method to set the box value test_app_client_puya.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="set_box_bytes", args=[box_name, ABIType.from_string(value_type).encode(box_value)], box_references=[box_identifier], @@ -645,7 +641,7 @@ def test_box_methods_with_arc4_returns_parametrized( # Send the transaction to set the box value test_app_client_puya.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method=method, args=["box1", arg_value], box_references=[box_reference], @@ -685,9 +681,9 @@ def test_abi_with_default_arg_method( default_signer=funded_account.signer, ) # app_client.send. - app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) + app_client.send.opt_in(AppClientMethodCallParams(method="opt_in")) app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="set_local", args=[1, 2, "banana", [1, 2, 3, 4]], ) @@ -698,20 +694,20 @@ def test_abi_with_default_arg_method( # Test with defined value defined_value_result = app_client.send.call( - AppClientMethodCallWithSendParams(method=method_signature, args=[defined_value]) + AppClientMethodCallParams(method=method_signature, args=[defined_value]) ) assert defined_value_result.abi_return == "Local state, defined value" # Test with default value - default_value_result = app_client.send.call(AppClientMethodCallWithSendParams(method=method_signature, args=[None])) + default_value_result = app_client.send.call(AppClientMethodCallParams(method=method_signature, args=[None])) assert default_value_result assert default_value_result.abi_return == "Local state, banana" def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: with pytest.raises(LogicError) as exc_info: - test_app_client_with_sourcemaps.send.call(AppClientMethodCallWithSendParams(method="error")) + test_app_client_with_sourcemaps.send.call(AppClientMethodCallParams(method="error")) error = exc_info.value assert error.pc == 885 diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 1e9c900e..65a268b6 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -10,17 +10,15 @@ AppClient, AppClientMethodCallCreateParams, AppClientMethodCallParams, - AppClientMethodCallWithCompilationAndSendParams, - AppClientMethodCallWithSendParams, AppClientParams, ) from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_factory import ( AppFactory, AppFactoryCreateMethodCallParams, - AppFactoryCreateWithSendParams, + AppFactoryCreateParams, ) -from algokit_utils.errors.logic_error import LogicError +from algokit_utils.errors import LogicError from algokit_utils.models.account import Account from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import PaymentParams @@ -68,14 +66,15 @@ def arc56_factory( def test_create_app(factory: AppFactory) -> None: """Test creating an app using the factory""" app_client, result = factory.send.bare.create( - params=AppFactoryCreateWithSendParams( - deploy_time_params={ + params=AppFactoryCreateParams(), + compilation_params={ + "deploy_time_params": { # It should strip off the TMPL_ "TMPL_UPDATABLE": 0, "DELETABLE": 0, "VALUE": 1, } - ) + }, ) assert app_client.app_id > 0 @@ -116,14 +115,16 @@ def test_create_app_with_constructor_deploy_time_params(algorand: AlgorandClient def test_create_app_with_oncomplete_overload(factory: AppFactory) -> None: app_client, result = factory.send.bare.create( - params=AppFactoryCreateWithSendParams( + params=AppFactoryCreateParams( on_complete=OnComplete.OptInOC, - updatable=True, - deletable=True, - deploy_time_params={ + ), + compilation_params={ + "updatable": True, + "deletable": True, + "deploy_time_params": { "VALUE": 1, }, - ) + }, ) assert result.transaction.application_call @@ -321,16 +322,16 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: def test_create_then_call_app(factory: AppFactory) -> None: app_client, _ = factory.send.bare.create( - AppFactoryCreateWithSendParams( - deploy_time_params={ - "UPDATABLE": 1, - "DELETABLE": 1, + compilation_params={ + "updatable": True, + "deletable": True, + "deploy_time_params": { "VALUE": 1, }, - ) + }, ) - call = app_client.send.call(AppClientMethodCallWithSendParams(method="call_abi", args=["test"])) + call = app_client.send.call(AppClientMethodCallParams(method="call_abi", args=["test"])) assert call.abi_return == "Hello, test" @@ -338,16 +339,16 @@ def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, rekey_to = algorand.account.random() app_client, _ = factory.send.bare.create( - AppFactoryCreateWithSendParams( - deploy_time_params={ - "UPDATABLE": 1, - "DELETABLE": 1, + compilation_params={ + "updatable": True, + "deletable": True, + "deploy_time_params": { "VALUE": 1, }, - ) + }, ) - app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in", rekey_to=rekey_to.address)) + app_client.send.opt_in(AppClientMethodCallParams(method="opt_in", rekey_to=rekey_to.address)) # If the rekey didn't work this will throw rekeyed_account = algorand.account.rekeyed(funded_account.address, rekey_to) @@ -361,12 +362,14 @@ def test_create_app_with_abi(factory: AppFactory) -> None: AppFactoryCreateMethodCallParams( method="create_abi", args=["string_io"], - deploy_time_params={ + ), + compilation_params={ + "deploy_time_params": { "UPDATABLE": 0, "DELETABLE": 0, "VALUE": 1, }, - ) + }, ) assert call_return.abi_return @@ -380,17 +383,19 @@ def test_update_app_with_abi(factory: AppFactory) -> None: "VALUE": 1, } app_client, _ = factory.send.bare.create( - AppFactoryCreateWithSendParams( - deploy_time_params=deploy_time_params, - ) + compilation_params={ + "deploy_time_params": deploy_time_params, + }, ) call_return = app_client.send.update( - AppClientMethodCallWithCompilationAndSendParams( + AppClientMethodCallParams( method="update_abi", args=["string_io"], - deploy_time_params=deploy_time_params, - ) + ), + compilation_params={ + "deploy_time_params": deploy_time_params, + }, ) assert call_return.abi_return == "string_io" @@ -399,17 +404,17 @@ def test_update_app_with_abi(factory: AppFactory) -> None: def test_delete_app_with_abi(factory: AppFactory) -> None: app_client, _ = factory.send.bare.create( - AppFactoryCreateWithSendParams( - deploy_time_params={ + compilation_params={ + "deploy_time_params": { "UPDATABLE": 0, "DELETABLE": 1, "VALUE": 1, }, - ) + }, ) call_return = app_client.send.delete( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="delete_abi", args=["string_io"], ) @@ -440,7 +445,7 @@ def test_export_import_sourcemaps( # Test error handling before importing source maps with pytest.raises(LogicError) as exc_info: - new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + new_client.send.call(AppClientMethodCallParams(method="error")) assert "assert failed" in exc_info.value.message @@ -449,7 +454,7 @@ def test_export_import_sourcemaps( # Test error handling after importing source maps with pytest.raises(LogicError) as exc_info: - new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + new_client.send.call(AppClientMethodCallParams(method="error")) error = exc_info.value assert ( @@ -475,7 +480,7 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( ) with pytest.raises(Exception, match="this is an error"): - app_client.send.call(AppClientMethodCallWithSendParams(method="throwError")) + app_client.send.call(AppClientMethodCallParams(method="throwError")) def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( @@ -508,7 +513,7 @@ def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( # Test error handling with pytest.raises(LogicError) as exc_info: - app_client.send.call(AppClientMethodCallWithSendParams(method="tmpl")) + app_client.send.call(AppClientMethodCallParams(method="tmpl")) assert ( exc_info.value.trace().strip() diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index a5386c1f..a0e41a44 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -211,4 +211,4 @@ def test_bulk_opt_out_not_opted_in_fails(algorand: AlgorandClient, sender: Accou # Then attempt to opt-out with pytest.raises(ValueError, match="is not opted-in"): - algorand.asset.bulk_opt_out(receiver.address, [asset_id]) + algorand.asset.bulk_opt_out(account=receiver.address, asset_ids=[asset_id]) diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index edac55be..1d9f0285 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -23,10 +23,7 @@ ) from algokit_utils.algorand import AlgorandClient from algokit_utils.applications import AppFactoryCreateMethodCallParams -from algokit_utils.applications.app_client import ( - AppClient, - AppClientMethodCallWithSendParams, -) +from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams from algokit_utils.common import Program from algokit_utils.models import Account from algokit_utils.models.amount import AlgoAmount @@ -64,9 +61,12 @@ def client_fixture(algorand: AlgorandClient, funded_account: Account) -> AppClie app_spec=app_spec, default_sender=funded_account.address, default_signer=funded_account.signer ) app_client, _ = app_factory.send.create( - AppFactoryCreateMethodCallParams( - method="create", deletable=True, updatable=True, deploy_time_params={"VERSION": 1} - ) + AppFactoryCreateMethodCallParams(method="create"), + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, ) return app_client @@ -154,7 +154,7 @@ def test_simulate_and_persist_response_via_app_call( cwd = tmp_path_factory.mktemp("cwd") mock_config.project_root = cwd - client_fixture.send.call(AppClientMethodCallWithSendParams(method="hello", args=["test"])) + client_fixture.send.call(AppClientMethodCallParams(method="hello", args=["test"])) output_path = cwd / "debug_traces" diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index 061ff6ce..f3259fee 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -10,12 +10,8 @@ from algokit_utils import Account from algokit_utils.algorand import AlgorandClient -from algokit_utils.applications.app_client import ( - AppClient, - AppClientMethodCallWithSendParams, - FundAppAccountParams, -) -from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateWithSendParams +from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams, FundAppAccountParams +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateParams from algokit_utils.config import config from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.amount import AlgoAmount @@ -59,7 +55,7 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ self.app_client, _ = factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) self.app_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(2334300))) self.app_client.send.call( - AppClientMethodCallWithSendParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) + AppClientMethodCallParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) ) yield @@ -82,100 +78,122 @@ def test_accounts_address_balance_invalid_ref(self, algorand: AlgorandClient) -> random_account = algorand.account.random() with pytest.raises(LogicError, match=f"invalid Account reference {random_account.address}"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[random_account.address], - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_accounts_address_balance_valid_ref(self, algorand: AlgorandClient) -> None: random_account = algorand.account.random() self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[random_account.address], - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_boxes_invalid_ref(self) -> None: with pytest.raises(LogicError, match="invalid Box reference"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="smallBox", - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_boxes_valid_ref(self) -> None: self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="smallBox", - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="mediumBox", - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_apps_external_unavailable_app(self) -> None: with pytest.raises(LogicError, match="unavailable App"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="externalAppCall", - populate_app_call_resources=False, static_fee=AlgoAmount.from_micro_algo(2_000), - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_apps_external_app(self) -> None: self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="externalAppCall", - populate_app_call_resources=True, static_fee=AlgoAmount.from_micro_algo(2_000), - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_assets_unavailable_asset(self) -> None: with pytest.raises(LogicError, match="unavailable Asset"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="assetTotal", - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_assets_valid_asset(self) -> None: self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="assetTotal", - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_cross_product_reference_has_asset(self, funded_account: Account) -> None: self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="hasAsset", args=[funded_account.address], - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_cross_product_reference_invalid_external_local(self, funded_account: Account) -> None: with pytest.raises(LogicError, match="unavailable App"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="externalLocal", args=[funded_account.address], - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_cross_product_reference_external_local( @@ -183,22 +201,27 @@ def test_cross_product_reference_external_local( ) -> None: algorand.send.app_call_method_call( external_client.params.opt_in( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="optInToApplication", sender=funded_account.address, - ) - ) + ), + ), + send_params={ + "populate_app_call_resources": True, + }, ) algorand.send.app_call_method_call( self.app_client.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="externalLocal", args=[funded_account.address], sender=funded_account.address, - populate_app_call_resources=True, - ) - ) + ), + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_address_balance_invalid_account_reference( @@ -206,33 +229,39 @@ def test_address_balance_invalid_account_reference( ) -> None: with pytest.raises(LogicError, match="invalid Account reference"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[algosdk.account.generate_account()[1]], - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) def test_address_balance( self, ) -> None: self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[algosdk.account.generate_account()[1]], on_complete=OnComplete.NoOpOC, - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_cross_product_reference_invalid_has_asset(self, funded_account: Account) -> None: with pytest.raises(LogicError, match="unavailable Asset"): self.app_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="hasAsset", args=[funded_account.address], - populate_app_call_resources=False, - ) + ), + send_params={ + "populate_app_call_resources": False, + }, ) @@ -284,17 +313,17 @@ def test_same_account(self, algorand: AlgorandClient, funded_account: Account) - txn_group = algorand.send.new_group() txn_group.add_app_call_method_call( self.v8_client.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[random_account.address], sender=funded_account.address, signer=rekeyed_to.signer, - ) - ) + ), + ), ) txn_group.add_app_call_method_call( self.v9_client.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[random_account.address], sender=funded_account.address, @@ -303,7 +332,11 @@ def test_same_account(self, algorand: AlgorandClient, funded_account: Account) - ) ) - result = txn_group.send(populate_app_call_resources=True) + result = txn_group.send( + { + "populate_app_call_resources": True, + } + ) v8_accounts = getattr(result.transactions[0].application_call, "accounts", None) or [] v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] @@ -312,7 +345,13 @@ def test_same_account(self, algorand: AlgorandClient, funded_account: Account) - def test_app_account(self, algorand: AlgorandClient, funded_account: Account) -> None: self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(328500))) self.v8_client.send.call( - AppClientMethodCallWithSendParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) + AppClientMethodCallParams( + method="bootstrap", + static_fee=AlgoAmount.from_micro_algo(3_000), + ), + send_params={ + "populate_app_call_resources": True, + }, ) external_app_id = int(self.v8_client.get_global_state()["externalAppID"].value) @@ -321,16 +360,16 @@ def test_app_account(self, algorand: AlgorandClient, funded_account: Account) -> txn_group = algorand.send.new_group() txn_group.add_app_call_method_call( self.v8_client.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="externalAppCall", static_fee=AlgoAmount.from_micro_algo(2_000), sender=funded_account.address, - ) - ) + ), + ), ) txn_group.add_app_call_method_call( self.v9_client.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="addressBalance", args=[external_app_addr], sender=funded_account.address, @@ -338,7 +377,11 @@ def test_app_account(self, algorand: AlgorandClient, funded_account: Account) -> ) ) - result = txn_group.send(populate_app_call_resources=True) + result = txn_group.send( + { + "populate_app_call_resources": True, + } + ) v8_apps = getattr(result.transactions[0].application_call, "foreign_apps", None) or [] v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] @@ -370,10 +413,12 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ def test_error_during_simulate(self) -> None: with pytest.raises(LogicError) as exc_info: self.external_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="error", - populate_app_call_resources=True, - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) assert "Error during resource population simulation in transaction 0" in exc_info.value.logic_error_str @@ -389,22 +434,28 @@ def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: Accoun self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(106100))) self.external_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="boxWithPayment", args=[payment_with_signer], - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) def test_sender_asset_holding(self) -> None: self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_000))) self.external_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="createAsset", static_fee=AlgoAmount.from_micro_algo(2_000), - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) - result = self.external_client.send.call(AppClientMethodCallWithSendParams(method="senderAssetBalance")) + result = self.external_client.send.call(AppClientMethodCallParams(method="senderAssetBalance")) assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 @@ -415,12 +466,15 @@ def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: Account self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_001))) self.external_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="createAsset", static_fee=AlgoAmount.from_micro_algo(2_001), - ) + ), + send_params={ + "populate_app_call_resources": True, + }, ) - result = self.external_client.send.call(AppClientMethodCallWithSendParams(method="senderAssetBalance")) + result = self.external_client.send.call(AppClientMethodCallParams(method="senderAssetBalance")) assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 @@ -440,9 +494,9 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ factory = algorand.client.get_app_factory(app_spec=inner_fee_spec, default_sender=funded_account.address) # Create 3 app instances - self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app1")) - self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app2")) - self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateWithSendParams(note=b"app3")) + self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app1")) + self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app2")) + self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app3")) # Fund app accounts for client in [self.app_client1, self.app_client2, self.app_client3]: @@ -454,34 +508,48 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ def test_throws_when_no_max_fee(self) -> None: """Test that error is thrown when no max fee is supplied""" - - params = AppClientMethodCallWithSendParams(method="no_op", cover_app_call_inner_txn_fees=True) - with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): - self.app_client1.send.call(params) + self.app_client1.send.call( + AppClientMethodCallParams( + method="no_op", + ), + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) def test_throws_when_inner_fees_not_covered(self) -> None: """Test that error is thrown when inner transaction fees are not covered""" expected_fee = 7000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=False, ) with pytest.raises(Exception, match="fee too small"): - self.app_client1.send.call(params) + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": False, + }, + ) def test_does_not_alter_fee_without_inners(self) -> None: """Test that fee is not altered when app call has no inner transactions""" expected_fee = 1000 - params = AppClientMethodCallWithSendParams( - method="no_op", cover_app_call_inner_txn_fees=True, max_fee=AlgoAmount.from_micro_algos(2000) + params = AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(2000), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, ) - result = self.app_client1.send.call(params) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -490,41 +558,53 @@ def test_throws_when_max_fee_too_small(self) -> None: """Test that error is thrown when max fee is too small to cover inner fees""" expected_fee = 7000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee - 1), - cover_app_call_inner_txn_fees=True, ) with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): - self.app_client1.send.call(params) + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: """Test that error is thrown when static fee is too small for inner transaction fees""" expected_fee = 7000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], static_fee=AlgoAmount.from_micro_algos(expected_fee - 1), - cover_app_call_inner_txn_fees=True, ) with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): - self.app_client1.send.call(params) + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) def test_alters_fee_handling_when_no_itxns_covered(self) -> None: """Test that fee handling is altered when no inner transaction fees are covered""" expected_fee = 7000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -533,13 +613,17 @@ def test_alters_fee_handling_when_all_inners_covered(self) -> None: """Test that fee handling is altered when all inner transaction fees are covered""" expected_fee = 1000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -548,13 +632,17 @@ def test_alters_fee_handling_when_some_inners_covered(self) -> None: """Test that fee handling is altered when some inner transaction fees are covered""" expected_fee = 5300 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -563,13 +651,17 @@ def test_alters_fee_when_some_inners_have_surplus(self) -> None: """Test that fee handling is altered when some inner transaction fees are covered""" expected_fee = 2000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -578,14 +670,14 @@ def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fe txn_1_expected_fee = 5800 txn_2_expected_fee = 6000 - txn_1_params = AppClientMethodCallWithSendParams( + txn_1_params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], static_fee=AlgoAmount.from_micro_algos(txn_1_expected_fee), note=b"txn_1", ) - txn_2_params = AppClientMethodCallWithSendParams( + txn_2_params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(txn_2_expected_fee), @@ -596,7 +688,7 @@ def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fe self.app_client1.algorand.new_group() .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) assert result.transactions[0].raw.fee == txn_1_expected_fee @@ -608,13 +700,17 @@ def test_does_not_alter_static_fee_with_surplus(self) -> None: """Test that a static fee with surplus is not altered""" expected_fee = 6000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], static_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee @@ -622,13 +718,17 @@ def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: """Test fee handling with large inner fee surplus pooling to lower siblings""" expected_fee = 7000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -637,13 +737,17 @@ def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: """Test fee handling with inner fee surplus pooling to some lower siblings""" expected_fee = 6300 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -652,13 +756,17 @@ def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: """Test fee handling with large inner fee surplus but no pooling""" expected_fee = 10_000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_txn_fees": True, + }, + ) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -667,7 +775,7 @@ def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) """Test fee handling with multiple inner fee surplus poolings to lower siblings""" expected_fee = 7100 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="send_inners_with_fees_2", args=[ self.app_client2.app_id, @@ -675,9 +783,8 @@ def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], ], max_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -699,14 +806,14 @@ def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: A ) .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(expected_fee), ) ) ) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) assert result.transactions[0].raw.fee == expected_fee @@ -721,7 +828,7 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: self.app_client1.algorand.new_group() .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(2000), @@ -744,7 +851,7 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: static_fee=AlgoAmount.from_micro_algos(0), ) ) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) assert result.transactions[0].raw.fee == 1500 @@ -766,7 +873,7 @@ def test_handles_nested_abi_method_calls(self, funded_account: Account) -> None: # Setup transaction parameters txn_arg_call = self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(4000), @@ -781,16 +888,15 @@ def test_handles_nested_abi_method_calls(self, funded_account: Account) -> None: ) expected_fee = 2000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="nestedTxnArg", args=[ self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call, ], static_fee=AlgoAmount.from_micro_algos(expected_fee), - cover_app_call_inner_txn_fees=True, ) - result = nested_client.send.call(params) + result = nested_client.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) assert len(result.transactions) == 3 assert result.transactions[0].raw.fee == 1500 @@ -816,7 +922,7 @@ def test_throws_when_max_fee_below_calculated(self) -> None: self.app_client1.algorand.new_group() .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(1200), @@ -827,13 +933,13 @@ def test_throws_when_max_fee_below_calculated(self) -> None: # to get the execution info would fail .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="no_op", max_fee=AlgoAmount.from_micro_algos(10_000), ) ) ) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Account) -> None: @@ -850,7 +956,7 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Accou ) txn_arg_call = self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], max_fee=AlgoAmount.from_micro_algos(2000), @@ -861,7 +967,7 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Accou ValueError, match="Calculated transaction fee 5000 µALGO is greater than max of 2000 for transaction 1" ): nested_client.send.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="nestedTxnArg", args=[ self.app_client1.algorand.create_transaction.payment( @@ -873,9 +979,11 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Accou ), txn_arg_call, ], - cover_app_call_inner_txn_fees=True, max_fee=AlgoAmount.from_micro_algos(10_000), - ) + ), + send_params={ + "cover_app_call_inner_txn_fees": True, + }, ) def test_throws_when_static_fee_below_calculated(self) -> None: @@ -888,7 +996,7 @@ def test_throws_when_static_fee_below_calculated(self) -> None: self.app_client1.algorand.new_group() .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], static_fee=AlgoAmount.from_micro_algos(5000), @@ -899,13 +1007,13 @@ def test_throws_when_static_fee_below_calculated(self) -> None: # to get the execution info would fail .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="no_op", max_fee=AlgoAmount.from_micro_algos(10_000), ) ) ) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Account) -> None: @@ -918,7 +1026,7 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Accou self.app_client1.algorand.new_group() .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], static_fee=AlgoAmount.from_micro_algos(13_000), @@ -928,7 +1036,7 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Accou ) .add_app_call_method_call( self.app_client1.params.call( - AppClientMethodCallWithSendParams( + AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], static_fee=AlgoAmount.from_micro_algos(1000), @@ -943,32 +1051,32 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Accou static_fee=AlgoAmount.from_micro_algos(500), ) ) - .send(cover_app_call_inner_txn_fees=True) + .send({"cover_app_call_inner_txn_fees": True}) ) def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: """Test fee handling with expensive ABI method calls that use ensure_budget to op-up""" expected_fee = 10_000 - params = AppClientMethodCallWithSendParams( + params = AppClientMethodCallParams( method="burn_ops", args=[6200], - cover_app_call_inner_txn_fees=True, max_fee=AlgoAmount.from_micro_algos(12_000), ) - result = self.app_client1.send.call(params) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) assert result.transaction.raw.fee == expected_fee assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] self._assert_min_fee(self.app_client1, params, expected_fee) - def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallWithSendParams, fee: int) -> None: + def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallParams, fee: int) -> None: """Helper to assert minimum required fee""" if fee == 1000: return - params_copy = dataclasses.replace( - params, cover_app_call_inner_txn_fees=False, static_fee=None, extra_fee=None, suppress_log=True + params, + static_fee=None, + extra_fee=None, ) with pytest.raises(Exception, match="fee too small"): diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index c5e9bd7c..20b4442f 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -101,7 +101,7 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> composer.add_asset_create(params) built = composer.build_transactions() - response = composer.send(max_rounds_to_wait=20) + response = composer.send({"max_rounds_to_wait": 20}) created_asset = algorand.client.algod.asset_info( algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] )["params"] @@ -157,7 +157,7 @@ def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, fun assert txn.index == asset_before_config_index assert txn.manager == funded_secondary_account.address - composer.send(max_rounds_to_wait=20) + composer.send({"max_rounds_to_wait": 20}) updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] assert updated_asset["manager"] == funded_secondary_account.address @@ -184,7 +184,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No assert txn.sender == funded_account.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" - composer.send(max_rounds_to_wait=20) + composer.send({"max_rounds_to_wait": 20}) def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Account) -> None: @@ -223,7 +223,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco assert isinstance(built.transactions[0], ApplicationCallTxn) txn = built.transactions[0] assert txn.sender == funded_account.address - response = composer.send(max_rounds_to_wait=20) + response = composer.send({"max_rounds_to_wait": 20}) assert response.returns[-1].value == "Hello, world" From b6e21fecb9cc0c5791ed3c2be289e382a5ae2d82 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Sun, 26 Jan 2025 06:24:13 +0100 Subject: [PATCH 21/31] fix: remove skip signature for debug related simulate as it always true in such case --- src/algokit_utils/_debugging.py | 8 ++------ src/algokit_utils/models/transaction.py | 2 ++ src/algokit_utils/transactions/transaction_composer.py | 2 -- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index bda8be61..957b2cbe 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -215,7 +215,6 @@ def simulate_response( extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, simulation_round: int | None = None, - skip_signatures: bool | None = None, # noqa: ARG001 TODO: revisit ) -> SimulateAtomicTransactionResponse: """Simulate atomic transaction group execution""" @@ -239,7 +238,7 @@ def simulate_response( return atc.simulate(algod_client, simulate_request) -def simulate_and_persist_response( # noqa: PLR0913 +def simulate_and_persist_response( atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", @@ -250,7 +249,6 @@ def simulate_and_persist_response( # noqa: PLR0913 extra_opcode_budget: int | None = None, exec_trace_config: SimulateTraceConfig | None = None, simulation_round: int | None = None, - skip_signatures: bool | None = None, ) -> SimulateAtomicTransactionResponse: """Simulates atomic transactions and persists simulation response to a JSON file. @@ -266,8 +264,7 @@ def simulate_and_persist_response( # noqa: PLR0913 :param allow_unnamed_resources: Flag to allow unnamed resources, defaults to None :param extra_opcode_budget: Additional opcode budget, defaults to None :param exec_trace_config: Execution trace configuration, defaults to None - :param simulation_round: Round number for simulation, defaults to None - :param skip_signatures: Flag to skip signatures, defaults to None + :param simulation_round: Round number for simulation, defa ults to None :return: Simulated response after persisting for AlgoKit AVM Debugger consumption """ atc_to_simulate = atc.clone() @@ -287,7 +284,6 @@ def simulate_and_persist_response( # noqa: PLR0913 extra_opcode_budget, exec_trace_config, simulation_round, - skip_signatures, ) txn_results = response.simulate_response["txn-groups"] diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 99232918..8a88e00e 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -4,9 +4,11 @@ import algosdk __all__ = [ + "AppCallSendParams", "Arc2TransactionNote", "BaseArc2Note", "JsonFormatArc2Note", + "SendParams", "StringFormatArc2Note", "TransactionNote", "TransactionNoteData", diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index dc555950..e9a36072 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1708,7 +1708,6 @@ def simulate( extra_opcode_budget, exec_trace_config, simulation_round, - skip_signatures, ) self._handle_simulate_error(response) return SendAtomicTransactionComposerResults( @@ -1731,7 +1730,6 @@ def simulate( extra_opcode_budget, exec_trace_config, simulation_round, - skip_signatures, ) self._handle_simulate_error(response) confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ From f1036de744fe1a08b06b30e4d180c078da54040e Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 27 Jan 2025 01:21:55 +0100 Subject: [PATCH 22/31] refactor: addressing pr comments --- src/algokit_utils/__init__.py | 2 + src/algokit_utils/applications/app_client.py | 26 ++--- src/algokit_utils/applications/app_factory.py | 6 +- src/algokit_utils/models/amount.py | 16 +-- src/algokit_utils/models/transaction.py | 4 - .../transactions/transaction_composer.py | 12 +-- .../transactions/transaction_sender.py | 30 +++--- tests/models/test_algo_amount.py | 102 ++++++++++++++++++ 8 files changed, 150 insertions(+), 48 deletions(-) create mode 100644 tests/models/test_algo_amount.py diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index a621005d..dc041b73 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -11,6 +11,7 @@ # Core types and utilities that are commonly used from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME from algokit_utils.errors.logic_error import LogicError @@ -132,6 +133,7 @@ __all__ = [ # Core types and utilities "Account", + "AlgoAmount", "LogicError", "AlgorandClient", "DELETABLE_TEMPLATE_NAME", diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 79c3bf74..b74e14f6 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -42,7 +42,7 @@ CompiledTeal, ) from algokit_utils.models.state import BoxName, BoxValue -from algokit_utils.models.transaction import AppCallSendParams, SendParams +from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, @@ -1023,7 +1023,7 @@ def __init__(self, client: AppClient) -> None: def update( self, params: AppClientBareCallParams | None = None, - send_params: AppCallSendParams | None = None, + send_params: SendParams | None = None, compilation_params: AppClientCompilationParams | None = None, ) -> SendAppTransactionResult[ABIReturn]: """Send an application update transaction. @@ -1052,7 +1052,7 @@ def update( ) def opt_in( - self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Send an application opt-in transaction. @@ -1069,7 +1069,7 @@ def opt_in( ) def delete( - self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Send an application delete transaction. @@ -1086,7 +1086,7 @@ def delete( ) def clear_state( - self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Send an application clear state transaction. @@ -1103,7 +1103,7 @@ def clear_state( ) def close_out( - self, params: AppClientBareCallParams | None = None, send_params: AppCallSendParams | None = None + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Send an application close out transaction. @@ -1123,7 +1123,7 @@ def call( self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = None, - send_params: AppCallSendParams | None = None, + send_params: SendParams | None = None, ) -> SendAppTransactionResult[ABIReturn]: """Send an application call transaction. @@ -1173,7 +1173,7 @@ def fund_app_account( ) def opt_in( - self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppClientMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application opt-in transaction. @@ -1191,7 +1191,7 @@ def opt_in( ) def delete( - self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppClientMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application delete transaction. @@ -1212,7 +1212,7 @@ def update( self, params: AppClientMethodCallParams, compilation_params: AppClientCompilationParams | None = None, - send_params: AppCallSendParams | None = None, + send_params: SendParams | None = None, ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]: """Send an application update transaction. @@ -1235,7 +1235,7 @@ def update( return result def close_out( - self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppClientMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application close out transaction. @@ -1253,7 +1253,7 @@ def close_out( ) def call( - self, params: AppClientMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppClientMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[Arc56ReturnValueType]: """Send an application call transaction. @@ -1272,7 +1272,7 @@ def call( method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( self._client.params.call(params) ) - send_params = send_params or AppCallSendParams() + send_params = send_params or SendParams() simulate_response = self._client._handle_call_errors( lambda: method_call_to_simulate.simulate( allow_unnamed_resources=send_params.get("populate_app_call_resources") or True, diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 4ba282e9..fc373c3d 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -45,7 +45,7 @@ AppSourceMaps, ) from algokit_utils.models.state import TealTemplateParams -from algokit_utils.models.transaction import AppCallSendParams +from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCallParams, AppCreateParams, @@ -381,7 +381,7 @@ def __init__(self, factory: "AppFactory") -> None: def create( self, params: AppFactoryCreateParams | None = None, - send_params: AppCallSendParams | None = None, + send_params: SendParams | None = None, compilation_params: AppClientCompilationParams | None = None, ) -> tuple[AppClient, SendAppCreateTransactionResult]: compilation_params = compilation_params or AppClientCompilationParams() @@ -441,7 +441,7 @@ def bare(self) -> _AppFactoryBareSendAccessor: def create( self, params: AppFactoryCreateMethodCallParams, - send_params: AppCallSendParams | None = None, + send_params: SendParams | None = None, compilation_params: AppClientCompilationParams | None = None, ) -> tuple[AppClient, AppFactoryCreateMethodCallResult[Arc56ReturnValueType]]: compilation_params = compilation_params or AppClientCompilationParams() diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index e8888389..0baa0e03 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -1,5 +1,7 @@ from __future__ import annotations +from decimal import Decimal + import algosdk from typing_extensions import Self @@ -18,15 +20,15 @@ class AlgoAmount: >>> amount = AlgoAmount({"microAlgos": 1_000_000}) """ - def __init__(self, amount: dict[str, int]): + def __init__(self, amount: dict[str, int | Decimal]): if "microAlgos" in amount: self.amount_in_micro_algo = int(amount["microAlgos"]) elif "microAlgo" in amount: self.amount_in_micro_algo = int(amount["microAlgo"]) elif "algos" in amount: - self.amount_in_micro_algo = algosdk.util.algos_to_microalgos(float(amount["algos"])) + self.amount_in_micro_algo = int(amount["algos"] * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) elif "algo" in amount: - self.amount_in_micro_algo = algosdk.util.algos_to_microalgos(float(amount["algo"])) + self.amount_in_micro_algo = int(amount["algo"] * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) else: raise ValueError("Invalid amount provided") @@ -47,7 +49,7 @@ def micro_algo(self) -> int: return self.amount_in_micro_algo @property - def algos(self) -> int: + def algos(self) -> Decimal: """Return the amount as a number in Algo. :returns: The amount in Algo. @@ -55,7 +57,7 @@ def algos(self) -> int: return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @property - def algo(self) -> int: + def algo(self) -> Decimal: """Return the amount as a number in Algo. :returns: The amount in Algo. @@ -63,7 +65,7 @@ def algo(self) -> int: return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] @staticmethod - def from_algos(amount: int) -> AlgoAmount: + def from_algos(amount: int | Decimal) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. :param amount: The amount in Algo. @@ -75,7 +77,7 @@ def from_algos(amount: int) -> AlgoAmount: return AlgoAmount({"algos": amount}) @staticmethod - def from_algo(amount: int) -> AlgoAmount: + def from_algo(amount: int | Decimal) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. :param amount: The amount in Algo. diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 8a88e00e..956b9eca 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -4,7 +4,6 @@ import algosdk __all__ = [ - "AppCallSendParams", "Arc2TransactionNote", "BaseArc2Note", "JsonFormatArc2Note", @@ -96,9 +95,6 @@ def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT: class SendParams(TypedDict, total=False): max_rounds_to_wait: int | None suppress_log: bool | None - - -class AppCallSendParams(SendParams, total=False): populate_app_call_resources: bool | None cover_app_call_inner_txn_fees: bool | None diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index e9a36072..383947b8 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -28,7 +28,7 @@ from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method from algokit_utils.config import config from algokit_utils.models.state import BoxIdentifier, BoxReference -from algokit_utils.models.transaction import AppCallSendParams, SendParams, TransactionWrapper +from algokit_utils.models.transaction import SendParams, TransactionWrapper if TYPE_CHECKING: from collections.abc import Callable @@ -1609,7 +1609,7 @@ def execute( def send( self, - params: SendParams | AppCallSendParams | None = None, + params: SendParams | None = None, ) -> SendAtomicTransactionComposerResults: """Send the transaction group to the network. @@ -1621,13 +1621,13 @@ def send( if not params: has_app_call = any(isinstance(txn.txn, ApplicationCallTxn) for txn in group) - params = AppCallSendParams() if has_app_call else SendParams() - - cover_app_call_inner_txn_fees: bool | None = params.get("cover_app_call_inner_txn_fees") # type: ignore[assignment] - populate_app_call_resources: bool | None = params.get("populate_app_call_resources") # type: ignore[assignment] + params = SendParams() if has_app_call else SendParams() + cover_app_call_inner_txn_fees = params.get("cover_app_call_inner_txn_fees") + populate_app_call_resources = params.get("populate_app_call_resources") wait_rounds = params.get("max_rounds_to_wait") sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_txn_fees else None + if wait_rounds is None: last_round = max(txn.txn.last_valid_round for txn in group) assert sp is not None diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 007e0e81..d6794331 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -11,7 +11,7 @@ from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.config import config -from algokit_utils.models.transaction import AppCallSendParams, SendParams, TransactionWrapper +from algokit_utils.models.transaction import SendParams, TransactionWrapper from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, @@ -214,9 +214,9 @@ def _send_app_call( c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppTransactionResult[ABIReturn]]: + ) -> Callable[[TxnParamsT, SendParams | None], SendAppTransactionResult[ABIReturn]]: def send_app_call( - params: TxnParamsT, send_params: AppCallSendParams | None = None + params: TxnParamsT, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: result = self._send(c, pre_log, post_log)(params, send_params) return SendAppTransactionResult[ABIReturn]( @@ -231,9 +231,9 @@ def _send_app_update_call( c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppUpdateTransactionResult[ABIReturn]]: + ) -> Callable[[TxnParamsT, SendParams | None], SendAppUpdateTransactionResult[ABIReturn]]: def send_app_update_call( - params: TxnParamsT, send_params: AppCallSendParams | None = None + params: TxnParamsT, send_params: SendParams | None = None ) -> SendAppUpdateTransactionResult[ABIReturn]: result = self._send_app_call(c, pre_log, post_log)(params, send_params) @@ -266,9 +266,9 @@ def _send_app_create_call( c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, - ) -> Callable[[TxnParamsT, AppCallSendParams | None], SendAppCreateTransactionResult[ABIReturn]]: + ) -> Callable[[TxnParamsT, SendParams | None], SendAppCreateTransactionResult[ABIReturn]]: def send_app_create_call( - params: TxnParamsT, send_params: AppCallSendParams | None = None + params: TxnParamsT, send_params: SendParams | None = None ) -> SendAppCreateTransactionResult[ABIReturn]: result = self._send_app_update_call(c, pre_log, post_log)(params, send_params) app_id = int(result.confirmation["application-index"]) # type: ignore[call-overload] @@ -454,7 +454,7 @@ def asset_opt_out( )(params, send_params) def app_create( - self, params: AppCreateParams, send_params: AppCallSendParams | None = None + self, params: AppCreateParams, send_params: SendParams | None = None ) -> SendAppCreateTransactionResult[ABIReturn]: """Create a new application. @@ -465,7 +465,7 @@ def app_create( return self._send_app_create_call(lambda c: c.add_app_create)(params, send_params) def app_update( - self, params: AppUpdateParams, send_params: AppCallSendParams | None = None + self, params: AppUpdateParams, send_params: SendParams | None = None ) -> SendAppUpdateTransactionResult[ABIReturn]: """Update an application. @@ -476,7 +476,7 @@ def app_update( return self._send_app_update_call(lambda c: c.add_app_update)(params, send_params) def app_delete( - self, params: AppDeleteParams, send_params: AppCallSendParams | None = None + self, params: AppDeleteParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Delete an application. @@ -487,7 +487,7 @@ def app_delete( return self._send_app_call(lambda c: c.add_app_delete)(params, send_params) def app_call( - self, params: AppCallParams, send_params: AppCallSendParams | None = None + self, params: AppCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Call an application. @@ -498,7 +498,7 @@ def app_call( return self._send_app_call(lambda c: c.add_app_call)(params, send_params) def app_create_method_call( - self, params: AppCreateMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppCreateMethodCallParams, send_params: SendParams | None = None ) -> SendAppCreateTransactionResult[ABIReturn]: """Call an application's create method. @@ -509,7 +509,7 @@ def app_create_method_call( return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params, send_params) def app_update_method_call( - self, params: AppUpdateMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppUpdateMethodCallParams, send_params: SendParams | None = None ) -> SendAppUpdateTransactionResult[ABIReturn]: """Call an application's update method. @@ -520,7 +520,7 @@ def app_update_method_call( return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params, send_params) def app_delete_method_call( - self, params: AppDeleteMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppDeleteMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Call an application's delete method. @@ -531,7 +531,7 @@ def app_delete_method_call( return self._send_app_call(lambda c: c.add_app_delete_method_call)(params, send_params) def app_call_method_call( - self, params: AppCallMethodCallParams, send_params: AppCallSendParams | None = None + self, params: AppCallMethodCallParams, send_params: SendParams | None = None ) -> SendAppTransactionResult[ABIReturn]: """Call an application's call method. diff --git a/tests/models/test_algo_amount.py b/tests/models/test_algo_amount.py new file mode 100644 index 00000000..8b7d2c6a --- /dev/null +++ b/tests/models/test_algo_amount.py @@ -0,0 +1,102 @@ +from decimal import Decimal + +import pytest + +from algokit_utils.models.amount import AlgoAmount + + +def test_initialization() -> None: + # Test valid initialization formats + assert AlgoAmount({"microAlgos": 1_000_000}).micro_algos == 1_000_000 + assert AlgoAmount({"microAlgo": 500_000}).micro_algos == 500_000 + assert AlgoAmount({"algos": 1}).micro_algos == 1_000_000 + assert AlgoAmount({"algo": Decimal("0.5")}).micro_algos == 500_000 + + # Test decimal precision + assert AlgoAmount({"algos": Decimal("0.000001")}).micro_algos == 1 + assert AlgoAmount({"algo": Decimal("123.456789")}).micro_algos == 123_456_789 + + # Test invalid initialization + with pytest.raises(ValueError, match="Invalid amount provided"): + AlgoAmount({"invalid": 100}) + + +def test_from_methods() -> None: + assert AlgoAmount.from_micro_algos(500_000).micro_algos == 500_000 + assert AlgoAmount.from_micro_algo(250_000).micro_algos == 250_000 + assert AlgoAmount.from_algos(2).micro_algos == 2_000_000 + assert AlgoAmount.from_algo(Decimal("0.75")).micro_algos == 750_000 + + +def test_properties() -> None: + amount = AlgoAmount.from_micro_algos(1_234_567) + assert amount.micro_algos == 1_234_567 + assert amount.micro_algo == 1_234_567 + assert amount.algos == Decimal("1.234567") + assert amount.algo == Decimal("1.234567") + + +def test_arithmetic_operations() -> None: + a = AlgoAmount.from_algos(5) + b = AlgoAmount.from_algos(3) + + # Addition + assert (a + b).micro_algos == 8_000_000 + a += b + assert a.micro_algos == 8_000_000 + + # Subtraction + assert (a - b).micro_algos == 5_000_000 + a -= b + assert a.micro_algos == 5_000_000 + + # Right operations + assert (AlgoAmount.from_micro_algo(1000) + a).micro_algos == 5_001_000 + assert (AlgoAmount.from_algos(10) - a).micro_algos == 5_000_000 + + +def test_comparison_operators() -> None: + base = AlgoAmount.from_algos(5) + same = AlgoAmount.from_algos(5) + larger = AlgoAmount.from_algos(10) + + assert base == same + assert base != larger + assert base < larger + assert larger > base + assert base <= same + assert larger >= base + + # Test int comparison + assert base == 5_000_000 + assert base < 6_000_000 + assert base > 4_000_000 + + +def test_edge_cases() -> None: + # Zero value + zero = AlgoAmount.from_micro_algos(0) + assert zero.micro_algos == 0 + assert zero.algos == 0 + + # Very large values + large = AlgoAmount.from_algos(Decimal("1e9")) + assert large.micro_algos == 1e9 * 1e6 + + # Decimal precision limits + precise = AlgoAmount({"algos": Decimal("0.123456789")}) + assert precise.micro_algos == 123_456 + + +def test_string_representation() -> None: + assert str(AlgoAmount.from_micro_algos(1_000_000)) == "1,000,000 µALGO" + assert str(AlgoAmount.from_algos(Decimal("2.5"))) == "2,500,000 µALGO" + + +def test_type_safety() -> None: + with pytest.raises(TypeError, match="Unsupported operand type"): + # int is not AlgoAmount + AlgoAmount.from_algos(5) + 1000 # type: ignore # noqa: PGH003 + + with pytest.raises(TypeError, match="Unsupported operand type"): + AlgoAmount.from_algos(5) - "invalid" # type: ignore # noqa: PGH003 From fc17da97f8f23867d251cb6da29cc89732baa3fb Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 27 Jan 2025 12:02:14 +0100 Subject: [PATCH 23/31] refactor: addressing pr comments --- src/algokit_utils/accounts/account_manager.py | 24 +++---- .../applications/app_deployer.py | 58 +++++++-------- src/algokit_utils/applications/app_factory.py | 42 +++++------ src/algokit_utils/protocols/typed_clients.py | 4 +- .../transactions/transaction_composer.py | 3 - tests/applications/test_app_factory.py | 72 +++++++++---------- 6 files changed, 98 insertions(+), 105 deletions(-) diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 9443ce17..9d10f42c 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -28,8 +28,8 @@ __all__ = [ "AccountInformation", "AccountManager", - "EnsureFundedFromTestnetDispenserApiResponse", - "EnsureFundedResponse", + "EnsureFundedFromTestnetDispenserApiResult", + "EnsureFundedResult", ] @@ -44,16 +44,16 @@ class _CommonEnsureFundedParams: @dataclass(frozen=True, kw_only=True) -class EnsureFundedResponse(SendSingleTransactionResult, _CommonEnsureFundedParams): +class EnsureFundedResult(SendSingleTransactionResult, _CommonEnsureFundedParams): """ - Response from performing an ensure funded call. + Result from performing an ensure funded call. """ @dataclass(frozen=True, kw_only=True) -class EnsureFundedFromTestnetDispenserApiResponse(_CommonEnsureFundedParams): +class EnsureFundedFromTestnetDispenserApiResult(_CommonEnsureFundedParams): """ - Response from performing an ensure funded call using TestNet dispenser API. + Result from performing an ensure funded call using TestNet dispenser API. """ @@ -602,7 +602,7 @@ def ensure_funded( # noqa: PLR0913 validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, - ) -> EnsureFundedResponse | None: + ) -> EnsureFundedResult | None: """ Funds a given account using a dispenser account as a funding source. @@ -672,7 +672,7 @@ def ensure_funded( # noqa: PLR0913 .send(send_params) ) - return EnsureFundedResponse( + return EnsureFundedResult( returns=result.returns, transactions=result.transactions, confirmations=result.confirmations, @@ -703,7 +703,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, - ) -> EnsureFundedResponse | None: + ) -> EnsureFundedResult | None: """ Ensure an account is funded from a dispenser account configured in environment. @@ -778,7 +778,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 .send(send_params) ) - return EnsureFundedResponse( + return EnsureFundedResult( returns=result.returns, transactions=result.transactions, confirmations=result.confirmations, @@ -797,7 +797,7 @@ def ensure_funded_from_testnet_dispenser_api( min_spending_balance: AlgoAmount, *, min_funding_increment: AlgoAmount | None = None, - ) -> EnsureFundedFromTestnetDispenserApiResponse | None: + ) -> EnsureFundedFromTestnetDispenserApiResult | None: """ Ensure an account is funded using the TestNet Dispenser API. @@ -846,7 +846,7 @@ def ensure_funded_from_testnet_dispenser_api( asset_id=DispenserAssetName.ALGO, ) - return EnsureFundedFromTestnetDispenserApiResponse( + return EnsureFundedFromTestnetDispenserApiResult( transaction_id=result.tx_id, amount_funded=AlgoAmount.from_micro_algo(result.amount), ) diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 3c680d09..6e12c785 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -32,7 +32,7 @@ "APP_DEPLOY_NOTE_DAPP", "AppDeployMetaData", "AppDeployParams", - "AppDeployResponse", + "AppDeployResult", "AppDeployer", "AppLookup", "AppMetaData", @@ -175,12 +175,12 @@ class AppDeployParams: # Union type for all possible deploy results @dataclass(frozen=True) -class AppDeployResponse: +class AppDeployResult: app: AppMetaData operation_performed: OperationPerformed - create_response: SendAppCreateTransactionResult[ABIReturn] | None = None - update_response: SendAppUpdateTransactionResult[ABIReturn] | None = None - delete_response: SendAppTransactionResult[ABIReturn] | None = None + create_result: SendAppCreateTransactionResult[ABIReturn] | None = None + update_result: SendAppUpdateTransactionResult[ABIReturn] | None = None + delete_result: SendAppTransactionResult[ABIReturn] | None = None class AppDeployer: @@ -197,7 +197,7 @@ def __init__( self._indexer = indexer self._app_lookups: dict[str, AppLookup] = {} - def deploy(self, deployment: AppDeployParams) -> AppDeployResponse: + def deploy(self, deployment: AppDeployParams) -> AppDeployResult: # Create new instances with updated notes logger.info( f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " @@ -325,7 +325,7 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResponse: ) logger.debug("No detected changes in app, nothing to do.", suppress_log=deployment.suppress_log) - return AppDeployResponse( + return AppDeployResult( app=existing_app, operation_performed=OperationPerformed.Nothing, ) @@ -335,11 +335,11 @@ def _create_app( deployment: AppDeployParams, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResponse: + ) -> AppDeployResult: """Create a new application""" if isinstance(deployment.create_params, AppCreateMethodCallParams): - create_response = self._transaction_sender.app_create_method_call( + create_result = self._transaction_sender.app_create_method_call( AppCreateMethodCallParams( **{ **asdict(deployment.create_params), @@ -349,7 +349,7 @@ def _create_app( ) ) else: - create_response = self._transaction_sender.app_create( + create_result = self._transaction_sender.app_create( AppCreateParams( **{ **asdict(deployment.create_params), @@ -361,24 +361,24 @@ def _create_app( app_metadata = AppMetaData( reference=AppReference( - app_id=create_response.app_id, app_address=get_application_address(create_response.app_id) + app_id=create_result.app_id, app_address=get_application_address(create_result.app_id) ), deploy_metadata=deployment.metadata, - created_round=create_response.confirmation.get("confirmed-round", 0) - if isinstance(create_response.confirmation, dict) + created_round=create_result.confirmation.get("confirmed-round", 0) + if isinstance(create_result.confirmation, dict) else 0, - updated_round=create_response.confirmation.get("confirmed-round", 0) - if isinstance(create_response.confirmation, dict) + updated_round=create_result.confirmation.get("confirmed-round", 0) + if isinstance(create_result.confirmation, dict) else 0, deleted=False, ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeployResponse( + return AppDeployResult( app=app_metadata, operation_performed=OperationPerformed.Create, - create_response=create_response, + create_result=create_result, ) def _replace_app( @@ -387,7 +387,7 @@ def _replace_app( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResponse: + ) -> AppDeployResult: composer = self._transaction_sender.new_group() # Add create transaction @@ -434,8 +434,8 @@ def _replace_app( result = composer.send() - create_response = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index) - delete_response = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index) + create_result = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index) + delete_result = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index) app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] app_metadata = AppMetaData( @@ -447,12 +447,12 @@ def _replace_app( ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeployResponse( + return AppDeployResult( app=app_metadata, operation_performed=OperationPerformed.Replace, - create_response=create_response, - update_response=None, - delete_response=delete_response, + create_result=create_result, + update_result=None, + delete_result=delete_result, ) def _update_app( @@ -461,7 +461,7 @@ def _update_app( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResponse: + ) -> AppDeployResult: """Update an existing application""" if isinstance(deployment.update_params, AppUpdateMethodCallParams): @@ -497,10 +497,10 @@ def _update_app( self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeployResponse( + return AppDeployResult( app=app_metadata, operation_performed=OperationPerformed.Update, - update_response=result, + update_result=result, ) def _handle_schema_break( @@ -509,7 +509,7 @@ def _handle_schema_break( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResponse: + ) -> AppDeployResult: if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): raise ValueError( "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " @@ -531,7 +531,7 @@ def _handle_update( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeployResponse: + ) -> AppDeployResult: if deployment.on_update in (OnUpdate.Fail, "fail"): raise ValueError( "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index fc373c3d..b7d469b7 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -32,7 +32,7 @@ from algokit_utils.applications.app_deployer import ( AppDeployMetaData, AppDeployParams, - AppDeployResponse, + AppDeployResult, AppLookup, AppMetaData, OnSchemaBreak, @@ -69,7 +69,7 @@ "AppFactoryCreateMethodCallParams", "AppFactoryCreateMethodCallResult", "AppFactoryCreateParams", - "AppFactoryDeployResponse", + "AppFactoryDeployResult", "AppFactoryParams", "SendAppCreateFactoryTransactionResult", "SendAppFactoryTransactionResult", @@ -136,24 +136,24 @@ class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult[Arc56 @dataclass(frozen=True) -class AppFactoryDeployResponse: +class AppFactoryDeployResult: """Result from deploying an application via AppFactory""" app: AppMetaData operation_performed: OperationPerformed - create_response: SendAppCreateFactoryTransactionResult | None = None - update_response: SendAppUpdateFactoryTransactionResult | None = None - delete_response: SendAppFactoryTransactionResult | None = None + create_result: SendAppCreateFactoryTransactionResult | None = None + update_result: SendAppUpdateFactoryTransactionResult | None = None + delete_result: SendAppFactoryTransactionResult | None = None @classmethod - def from_deploy_response( + def from_deploy_result( cls, - response: AppDeployResponse, + response: AppDeployResult, deploy_params: AppDeployParams, app_spec: Arc56Contract, app_compilation_data: AppClientCompilationResult | None = None, ) -> Self: - def to_factory_response( + def to_factory_result( response_data: SendAppTransactionResult[ABIReturn] | SendAppCreateTransactionResult | SendAppUpdateTransactionResult @@ -185,16 +185,16 @@ def to_factory_response( return cls( app=response.app, operation_performed=response.operation_performed, - create_response=to_factory_response( - response.create_response, + create_result=to_factory_result( + response.create_result, deploy_params.create_params, ), - update_response=to_factory_response( - response.update_response, + update_result=to_factory_result( + response.update_result, deploy_params.update_params, ), - delete_response=to_factory_response( - response.delete_response, + delete_result=to_factory_result( + response.delete_result, deploy_params.delete_params, ), ) @@ -552,7 +552,7 @@ def deploy( # noqa: PLR0913 suppress_log: bool = False, populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, - ) -> tuple[AppClient, AppFactoryDeployResponse]: + ) -> tuple[AppClient, AppFactoryDeployResult]: """Deploy the application with the specified parameters.""" # Resolve control parameters with factory defaults resolved_updatable = ( @@ -626,17 +626,17 @@ def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: populate_app_call_resources=populate_app_call_resources, cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, ) - deploy_response = self._algorand.app_deployer.deploy(deploy_params) + deploy_result = self._algorand.app_deployer.deploy(deploy_params) # Prepare app client and factory deploy response app_client = self.get_app_client_by_id( - app_id=deploy_response.app.app_id, + app_id=deploy_result.app.app_id, app_name=app_name, default_sender=self._default_sender, default_signer=self._default_signer, ) - factory_deploy_response = AppFactoryDeployResponse.from_deploy_response( - response=deploy_response, + factory_deploy_result = AppFactoryDeployResult.from_deploy_result( + response=deploy_result, deploy_params=deploy_params, app_spec=app_client.app_spec, app_compilation_data=self.compile( @@ -648,7 +648,7 @@ def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: ), ) - return app_client, factory_deploy_response + return app_client, factory_deploy_result def get_app_client_by_id( self, diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index 7f142521..af26e8a9 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from algokit_utils.algorand import AlgorandClient - from algokit_utils.applications.app_factory import AppFactoryDeployResponse + from algokit_utils.applications.app_factory import AppFactoryDeployResult __all__ = [ "TypedAppClientProtocol", @@ -107,4 +107,4 @@ def deploy( # noqa: PLR0913 suppress_log: bool = False, populate_app_call_resources: bool | None = None, cover_app_call_inner_txn_fees: bool | None = None, - ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResponse"]: ... + ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResult"]: ... diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 383947b8..9d096441 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1982,8 +1982,6 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 else self._get_signer(params.sender) or algosdk.atomic_transaction_composer.EmptySigner(), "method_args": list(reversed(method_args)), "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, - "note": params.note, - "lease": params.lease, "boxes": [AppManager.get_box_reference(ref) for ref in params.box_references] if params.box_references else None, @@ -2004,7 +2002,6 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 else None, "approval_program": approval_program, "clear_program": clear_program, - "rekey_to": params.rekey_to, "extra_pages": extra_pages, } diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 65a268b6..63290598 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -155,9 +155,9 @@ def test_deploy_app_create(factory: AppFactory) -> None: ) assert deploy_result.operation_performed == OperationPerformed.Create - assert deploy_result.create_response - assert deploy_result.create_response.app_id > 0 - assert app_client.app_id == deploy_result.create_response.app_id + assert deploy_result.create_result + assert deploy_result.create_result.app_id > 0 + assert app_client.app_id == deploy_result.create_result.app_id assert app_client.app_address == get_application_address(app_client.app_id) @@ -170,7 +170,7 @@ def test_deploy_app_create_abi(factory: AppFactory) -> None: ) assert deploy_result.operation_performed == OperationPerformed.Create - create_result = deploy_result.create_response + create_result = deploy_result.create_result assert create_result is not None assert deploy_result.app.app_id > 0 app_index = create_result.confirmation["application-index"] # type: ignore[call-overload] @@ -186,7 +186,7 @@ def test_deploy_app_update(factory: AppFactory) -> None: updatable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create - assert create_deploy_result.create_response + assert create_deploy_result.create_result updated_app_client, update_deploy_result = factory.deploy( deploy_time_params={ @@ -195,17 +195,17 @@ def test_deploy_app_update(factory: AppFactory) -> None: on_update=OnUpdate.UpdateApp, ) assert update_deploy_result.operation_performed == OperationPerformed.Update - assert update_deploy_result.update_response + assert update_deploy_result.update_result assert create_deploy_result.app.app_id == update_deploy_result.app.app_id assert create_deploy_result.app.app_address == update_deploy_result.app.app_address - assert create_deploy_result.create_response.confirmation + assert create_deploy_result.create_result.confirmation assert create_deploy_result.app.updatable assert create_deploy_result.app.updatable == update_deploy_result.app.updatable assert create_deploy_result.app.updated_round != update_deploy_result.app.updated_round assert create_deploy_result.app.created_round == update_deploy_result.app.created_round - assert update_deploy_result.update_response.confirmation - confirmed_round = update_deploy_result.update_response.confirmation["confirmed-round"] # type: ignore[call-overload] + assert update_deploy_result.update_result.confirmation + confirmed_round = update_deploy_result.update_result.confirmation["confirmed-round"] # type: ignore[call-overload] assert update_deploy_result.app.updated_round == confirmed_round @@ -217,8 +217,8 @@ def test_deploy_app_update_abi(factory: AppFactory) -> None: updatable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create - assert create_deploy_result.create_response - created_app = create_deploy_result.create_response + assert create_deploy_result.create_result + created_app = create_deploy_result.create_result _, update_deploy_result = factory.deploy( deploy_time_params={ @@ -229,20 +229,18 @@ def test_deploy_app_update_abi(factory: AppFactory) -> None: ) assert update_deploy_result.operation_performed == OperationPerformed.Update - assert update_deploy_result.update_response + assert update_deploy_result.update_result assert update_deploy_result.app.app_id == created_app.app_id assert update_deploy_result.app.app_address == created_app.app_address - assert update_deploy_result.update_response.confirmation is not None + assert update_deploy_result.update_result.confirmation is not None assert update_deploy_result.app.created_round == create_deploy_result.app.created_round assert update_deploy_result.app.updated_round != update_deploy_result.app.created_round assert ( - update_deploy_result.app.updated_round == update_deploy_result.update_response.confirmation["confirmed-round"] # type: ignore[call-overload] + update_deploy_result.app.updated_round == update_deploy_result.update_result.confirmation["confirmed-round"] # type: ignore[call-overload] ) - assert update_deploy_result.update_response.transaction.application_call - assert ( - update_deploy_result.update_response.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC - ) - assert update_deploy_result.update_response.abi_return == "args_io" + assert update_deploy_result.update_result.transaction.application_call + assert update_deploy_result.update_result.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC + assert update_deploy_result.update_result.abi_return == "args_io" def test_deploy_app_replace(factory: AppFactory) -> None: @@ -253,7 +251,7 @@ def test_deploy_app_replace(factory: AppFactory) -> None: deletable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create - assert create_deploy_result.create_response + assert create_deploy_result.create_result _, replace_deploy_result = factory.deploy( deploy_time_params={ @@ -267,18 +265,17 @@ def test_deploy_app_replace(factory: AppFactory) -> None: assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address( replace_deploy_result.app.app_id ) - assert replace_deploy_result.create_response is not None - assert replace_deploy_result.delete_response is not None - assert replace_deploy_result.delete_response.confirmation is not None + assert replace_deploy_result.create_result is not None + assert replace_deploy_result.delete_result is not None + assert replace_deploy_result.delete_result.confirmation is not None assert ( - len(replace_deploy_result.create_response.transactions) - + len(replace_deploy_result.delete_response.transactions) + len(replace_deploy_result.create_result.transactions) + len(replace_deploy_result.delete_result.transactions) == 2 ) - assert replace_deploy_result.delete_response.transaction.application_call - assert replace_deploy_result.delete_response.transaction.application_call.index == create_deploy_result.app.app_id + assert replace_deploy_result.delete_result.transaction.application_call + assert replace_deploy_result.delete_result.transaction.application_call.index == create_deploy_result.app.app_id assert ( - replace_deploy_result.delete_response.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + replace_deploy_result.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC ) @@ -303,21 +300,20 @@ def test_deploy_app_replace_abi(factory: AppFactory) -> None: assert replace_deploy_result.operation_performed == OperationPerformed.Replace assert replace_deploy_result.app.app_id > create_deploy_result.app.app_id assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address(replaced_app_client.app_id) - assert replace_deploy_result.create_response is not None - assert replace_deploy_result.delete_response is not None - assert replace_deploy_result.delete_response.confirmation is not None + assert replace_deploy_result.create_result is not None + assert replace_deploy_result.delete_result is not None + assert replace_deploy_result.delete_result.confirmation is not None assert ( - len(replace_deploy_result.create_response.transactions) - + len(replace_deploy_result.delete_response.transactions) + len(replace_deploy_result.create_result.transactions) + len(replace_deploy_result.delete_result.transactions) == 2 ) - assert replace_deploy_result.delete_response.transaction.application_call - assert replace_deploy_result.delete_response.transaction.application_call.index == create_deploy_result.app.app_id + assert replace_deploy_result.delete_result.transaction.application_call + assert replace_deploy_result.delete_result.transaction.application_call.index == create_deploy_result.app.app_id assert ( - replace_deploy_result.delete_response.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + replace_deploy_result.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC ) - assert replace_deploy_result.create_response.abi_return == "arg_io" - assert replace_deploy_result.delete_response.abi_return == "arg2_io" + assert replace_deploy_result.create_result.abi_return == "arg_io" + assert replace_deploy_result.delete_result.abi_return == "arg2_io" def test_create_then_call_app(factory: AppFactory) -> None: From 690b567a5398f60352c8bf7e4d8270909ea3a529 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 27 Jan 2025 15:38:00 +0100 Subject: [PATCH 24/31] refactor: addressing pr comments --- legacy_v2_tests/test_debug_utils.py | 6 +- src/algokit_utils/__init__.py | 232 +----------------- src/algokit_utils/_legacy_v2/__init__.py | 177 +++++++++++++ .../_legacy_v2/_ensure_funded.py | 2 +- src/algokit_utils/_legacy_v2/_transfer.py | 2 +- src/algokit_utils/_legacy_v2/account.py | 2 +- .../_legacy_v2/application_client.py | 38 +-- src/algokit_utils/_legacy_v2/asset.py | 2 +- src/algokit_utils/_legacy_v2/deploy.py | 4 +- src/algokit_utils/_legacy_v2/models.py | 8 + src/algokit_utils/account.py | 3 +- src/algokit_utils/accounts/account_manager.py | 217 ++++++++-------- .../accounts/kmd_account_manager.py | 4 +- src/algokit_utils/algorand.py | 25 +- src/algokit_utils/applications/__init__.py | 2 + src/algokit_utils/applications/abi.py | 8 +- src/algokit_utils/applications/app_client.py | 57 ++--- .../applications/app_deployer.py | 144 +++++------ src/algokit_utils/applications/app_factory.py | 69 +++--- src/algokit_utils/applications/enums.py | 40 +++ src/algokit_utils/assets/asset_manager.py | 12 +- src/algokit_utils/clients/client_manager.py | 81 +++--- src/algokit_utils/models/account.py | 62 ++++- src/algokit_utils/models/network.py | 10 +- src/algokit_utils/models/transaction.py | 8 +- src/algokit_utils/protocols/__init__.py | 1 + src/algokit_utils/protocols/account.py | 22 ++ src/algokit_utils/protocols/typed_clients.py | 60 +++-- .../transactions/transaction_composer.py | 23 +- tests/accounts/test_account_manager.py | 4 +- tests/applications/test_app_client.py | 34 +-- tests/applications/test_app_factory.py | 137 +++++++---- tests/applications/test_app_manager.py | 4 +- tests/assets/test_asset_manager.py | 20 +- .../clients/algorand_client/test_transfer.py | 32 +-- tests/conftest.py | 19 +- tests/test_debug_utils.py | 13 +- tests/transactions/test_resource_packing.py | 44 ++-- .../transactions/test_transaction_composer.py | 59 +++-- .../transactions/test_transaction_creator.py | 28 +-- tests/transactions/test_transaction_sender.py | 48 ++-- 41 files changed, 942 insertions(+), 821 deletions(-) create mode 100644 src/algokit_utils/applications/enums.py create mode 100644 src/algokit_utils/protocols/account.py diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index e75dd1b5..13b4f518 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -15,11 +15,11 @@ persist_sourcemaps, simulate_and_persist_response, ) +from algokit_utils._legacy_v2.account import get_account from algokit_utils._legacy_v2.application_client import ApplicationClient -from algokit_utils.account import get_account -from algokit_utils.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.models import Account from algokit_utils.common import Program -from algokit_utils.models import Account from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index dc041b73..399a81cd 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -10,223 +10,15 @@ """ # Core types and utilities that are commonly used -from algokit_utils.models.account import Account -from algokit_utils.models.amount import AlgoAmount -from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed -from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME -from algokit_utils.errors.logic_error import LogicError -from algokit_utils.algorand import AlgorandClient - -# Common managers/clients that are frequently used entry points -from algokit_utils.accounts.account_manager import AccountManager -from algokit_utils.applications.app_client import AppClient -from algokit_utils.applications.app_factory import AppFactory -from algokit_utils.assets.asset_manager import AssetManager -from algokit_utils.clients.client_manager import ClientManager -from algokit_utils.transactions.transaction_composer import TransactionComposer - -# Commonly used constants -from algokit_utils.clients.dispenser_api_client import ( - DISPENSER_ACCESS_TOKEN_KEY, - TestNetDispenserApiClient, - DISPENSER_REQUEST_TIMEOUT, -) - -# ==== LEGACY V2 SUPPORT BEGIN ==== -# These imports are maintained for backwards compatibility -from algokit_utils._legacy_v2._ensure_funded import ( - EnsureBalanceParameters, - EnsureFundedResponse, - ensure_funded, -) -from algokit_utils._legacy_v2._transfer import ( - TransferAssetParameters, - TransferParameters, - transfer, - transfer_asset, -) -from algokit_utils._legacy_v2.account import ( - create_kmd_wallet_account, - get_account, - get_account_from_mnemonic, - get_dispenser_account, - get_kmd_wallet_account, - get_localnet_default_account, - get_or_create_kmd_wallet_account, -) -from algokit_utils._legacy_v2.application_client import ( - ApplicationClient, - execute_atc_with_logic_error, - get_next_version, - get_sender_from_signer, - num_extra_program_pages, -) -from algokit_utils._legacy_v2.application_specification import ( - ApplicationSpecification, - AppSpecStateDict, - CallConfig, - DefaultArgumentDict, - DefaultArgumentType, - MethodConfigDict, - MethodHints, - OnCompleteActionName, -) -from algokit_utils._legacy_v2.asset import opt_in, opt_out -from algokit_utils._legacy_v2.common import Program -from algokit_utils._legacy_v2.deploy import ( - NOTE_PREFIX, - ABICallArgs, - ABICallArgsDict, - ABICreateCallArgs, - ABICreateCallArgsDict, - AppDeployMetaData, - AppLookup, - AppMetaData, - AppReference, - DeployCallArgs, - DeployCallArgsDict, - DeployCreateCallArgs, - DeployCreateCallArgsDict, - DeploymentFailedError, - DeployResponse, - TemplateValueDict, - TemplateValueMapping, - get_app_id_from_tx_id, - get_creator_apps, - replace_template_variables, -) -from algokit_utils._legacy_v2.models import ( - ABIArgsDict, - ABIMethod, - ABITransactionResponse, - CommonCallParameters, - CommonCallParametersDict, - CreateCallParameters, - CreateCallParametersDict, - CreateTransactionParameters, - OnCompleteCallParameters, - OnCompleteCallParametersDict, - TransactionParameters, - TransactionParametersDict, - TransactionResponse, -) -from algokit_utils._legacy_v2.network_clients import ( - AlgoClientConfig, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client_from_algod_client, - is_localnet, - is_mainnet, - is_testnet, -) -# ==== LEGACY V2 SUPPORT END ==== - -# Debugging utilities -from algokit_utils._debugging import ( - PersistSourceMapInput, - persist_sourcemaps, - simulate_and_persist_response, -) - -__all__ = [ - # Core types and utilities - "Account", - "AlgoAmount", - "LogicError", - "AlgorandClient", - "DELETABLE_TEMPLATE_NAME", - "UPDATABLE_TEMPLATE_NAME", - # Common managers/clients - "AccountManager", - "AppClient", - "AppFactory", - "AssetManager", - "ClientManager", - "TransactionComposer", - "TestNetDispenserApiClient", - # Constants - "DISPENSER_ACCESS_TOKEN_KEY", - "DISPENSER_REQUEST_TIMEOUT", - "NOTE_PREFIX", - # Legacy v2 exports - maintained for backwards compatibility - "ABIArgsDict", - "ABICallArgs", - "ABICallArgsDict", - "ABICreateCallArgs", - "ABICreateCallArgsDict", - "ABIMethod", - "ABITransactionResponse", - "AlgoClientConfig", - "AppDeployMetaData", - "AppLookup", - "AppMetaData", - "AppReference", - "AppSpecStateDict", - "ApplicationClient", - "ApplicationSpecification", - "CallConfig", - "CommonCallParameters", - "CommonCallParametersDict", - "CreateCallParameters", - "CreateCallParametersDict", - "CreateTransactionParameters", - "DefaultArgumentDict", - "DefaultArgumentType", - "DeployCallArgs", - "DeployCallArgsDict", - "DeployCreateCallArgs", - "DeployCreateCallArgsDict", - "DeployResponse", - "DeploymentFailedError", - "EnsureBalanceParameters", - "EnsureFundedResponse", - "MethodConfigDict", - "MethodHints", - "OnCompleteActionName", - "OnCompleteCallParameters", - "OnCompleteCallParametersDict", - "OnSchemaBreak", - "OnUpdate", - "OperationPerformed", - "PersistSourceMapInput", - "Program", - "TemplateValueDict", - "TemplateValueMapping", - "TransactionParameters", - "TransactionParametersDict", - "TransactionResponse", - "TransferAssetParameters", - "TransferParameters", - # Legacy v2 functions - "create_kmd_wallet_account", - "ensure_funded", - "execute_atc_with_logic_error", - "get_account", - "get_account_from_mnemonic", - "get_algod_client", - "get_algonode_config", - "get_app_id_from_tx_id", - "get_creator_apps", - "get_default_localnet_config", - "get_dispenser_account", - "get_indexer_client", - "get_kmd_client_from_algod_client", - "get_kmd_wallet_account", - "get_localnet_default_account", - "get_next_version", - "get_or_create_kmd_wallet_account", - "get_sender_from_signer", - "is_localnet", - "is_mainnet", - "is_testnet", - "num_extra_program_pages", - "opt_in", - "opt_out", - "persist_sourcemaps", - "replace_template_variables", - "simulate_and_persist_response", - "transfer", - "transfer_asset", -] +from algokit_utils.applications import * # noqa: F403 +from algokit_utils.assets import * # noqa: F403 +from algokit_utils.protocols import * # noqa: F403 +from algokit_utils.models import * # noqa: F403 +from algokit_utils.accounts import * # noqa: F403 +from algokit_utils.clients import * # noqa: F403 +from algokit_utils.transactions import * # noqa: F403 +from algokit_utils.errors import * # noqa: F403 +from algokit_utils.algorand import * # noqa: F403 + +# Legacy types and utilities +from algokit_utils._legacy_v2 import * # noqa: F403 diff --git a/src/algokit_utils/_legacy_v2/__init__.py b/src/algokit_utils/_legacy_v2/__init__.py index e69de29b..25c8b08f 100644 --- a/src/algokit_utils/_legacy_v2/__init__.py +++ b/src/algokit_utils/_legacy_v2/__init__.py @@ -0,0 +1,177 @@ +"""AlgoKit Python Utilities (Legacy V2) - a set of utilities for building solutions on Algorand + +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + + from algokit_utils.accounts import KmdAccountManager + from algokit_utils.applications import AppClient + from algokit_utils.applications.app_spec import Arc52Contract + etc. +""" + +# Debugging utilities +from algokit_utils._legacy_v2._ensure_funded import ( + EnsureBalanceParameters, + EnsureFundedResponse, + ensure_funded, +) +from algokit_utils._legacy_v2._transfer import ( + TransferAssetParameters, + TransferParameters, + transfer, + transfer_asset, +) +from algokit_utils._legacy_v2.account import ( + create_kmd_wallet_account, + get_account, + get_account_from_mnemonic, + get_dispenser_account, + get_kmd_wallet_account, + get_localnet_default_account, + get_or_create_kmd_wallet_account, +) +from algokit_utils._legacy_v2.application_client import ( + ApplicationClient, + execute_atc_with_logic_error, + get_next_version, + get_sender_from_signer, + num_extra_program_pages, +) +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + AppSpecStateDict, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) +from algokit_utils._legacy_v2.asset import opt_in, opt_out +from algokit_utils._legacy_v2.common import Program +from algokit_utils._legacy_v2.deploy import ( + NOTE_PREFIX, + ABICallArgs, + ABICallArgsDict, + ABICreateCallArgs, + ABICreateCallArgsDict, + AppDeployMetaData, + AppLookup, + AppMetaData, + AppReference, + DeployCallArgs, + DeployCallArgsDict, + DeployCreateCallArgs, + DeployCreateCallArgsDict, + DeploymentFailedError, + DeployResponse, + TemplateValueDict, + TemplateValueMapping, + get_app_id_from_tx_id, + get_creator_apps, + replace_template_variables, +) +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIMethod, + ABITransactionResponse, + Account, + CommonCallParameters, + CommonCallParametersDict, + CreateCallParameters, + CreateCallParametersDict, + CreateTransactionParameters, + OnCompleteCallParameters, + OnCompleteCallParametersDict, + TransactionParameters, + TransactionParametersDict, + TransactionResponse, +) +from algokit_utils._legacy_v2.network_clients import ( + AlgoClientConfig, + get_algod_client, + get_algonode_config, + get_default_localnet_config, + get_indexer_client, + get_kmd_client_from_algod_client, + is_localnet, + is_mainnet, + is_testnet, +) + +__all__ = [ + "NOTE_PREFIX", + "ABIArgsDict", + "ABICallArgs", + "ABICallArgsDict", + "ABICreateCallArgs", + "ABICreateCallArgsDict", + "ABIMethod", + "ABITransactionResponse", + "Account", + "AlgoClientConfig", + "AppDeployMetaData", + "AppLookup", + "AppMetaData", + "AppReference", + "AppSpecStateDict", + "ApplicationClient", + "ApplicationSpecification", + "CallConfig", + "CommonCallParameters", + "CommonCallParametersDict", + "CreateCallParameters", + "CreateCallParametersDict", + "CreateTransactionParameters", + "DefaultArgumentDict", + "DefaultArgumentType", + "DeployCallArgs", + "DeployCallArgsDict", + "DeployCreateCallArgs", + "DeployCreateCallArgsDict", + "DeployResponse", + "DeploymentFailedError", + "EnsureBalanceParameters", + "EnsureFundedResponse", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", + "OnCompleteCallParameters", + "OnCompleteCallParametersDict", + "Program", + "TemplateValueDict", + "TemplateValueMapping", + "TransactionParameters", + "TransactionParametersDict", + "TransactionResponse", + "TransferAssetParameters", + "TransferParameters", + # Legacy v2 functions + "create_kmd_wallet_account", + "ensure_funded", + "execute_atc_with_logic_error", + "get_account", + "get_account_from_mnemonic", + "get_algod_client", + "get_algonode_config", + "get_app_id_from_tx_id", + "get_creator_apps", + "get_default_localnet_config", + "get_dispenser_account", + "get_indexer_client", + "get_kmd_client_from_algod_client", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_next_version", + "get_or_create_kmd_wallet_account", + "get_sender_from_signer", + "is_localnet", + "is_mainnet", + "is_testnet", + "num_extra_program_pages", + "opt_in", + "opt_out", + "replace_template_variables", + "transfer", + "transfer_asset", +] diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index ef4d6fbe..1add7522 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -8,12 +8,12 @@ from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.account import get_dispenser_account +from algokit_utils._legacy_v2.models import Account from algokit_utils._legacy_v2.network_clients import is_testnet from algokit_utils.clients.dispenser_api_client import ( DispenserAssetName, TestNetDispenserApiClient, ) -from algokit_utils.models.account import Account @dataclass(kw_only=True) diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index e3ae4e5c..b5136051 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -8,7 +8,7 @@ from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams from typing_extensions import deprecated -from algokit_utils.models.account import Account +from algokit_utils._legacy_v2.models import Account if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index e9161cd4..fa0bfa52 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -8,8 +8,8 @@ from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer +from algokit_utils._legacy_v2.models import Account from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet -from algokit_utils.models.account import Account if TYPE_CHECKING: from collections.abc import Callable diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index ddf10079..d3a12852 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import base64 import copy import json @@ -31,12 +33,6 @@ import algokit_utils._legacy_v2.application_specification as au_spec import algokit_utils._legacy_v2.deploy as au_deploy -from algokit_utils._debugging import ( - PersistSourceMapInput, - persist_sourcemaps, - simulate_and_persist_response, - simulate_response, -) from algokit_utils._legacy_v2.common import Program from algokit_utils._legacy_v2.logic_error import LogicError, parse_logic_error from algokit_utils._legacy_v2.models import ( @@ -44,6 +40,7 @@ ABIArgType, ABIMethod, ABITransactionResponse, + Account, CreateCallParameters, CreateCallParametersDict, OnCompleteCallParameters, @@ -54,7 +51,6 @@ TransactionResponse, ) from algokit_utils.config import config -from algokit_utils.models.account import Account if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -121,7 +117,7 @@ class ApplicationClient: @overload def __init__( self, - algod_client: "AlgodClient", + algod_client: AlgodClient, app_spec: au_spec.ApplicationSpecification | Path, *, app_id: int = 0, @@ -134,11 +130,11 @@ def __init__( @overload def __init__( self, - algod_client: "AlgodClient", + algod_client: AlgodClient, app_spec: au_spec.ApplicationSpecification | Path, *, creator: str | Account, - indexer_client: "IndexerClient | None" = None, + indexer_client: IndexerClient | None = None, existing_deployments: au_deploy.AppLookup | None = None, signer: TransactionSigner | Account | None = None, sender: str | None = None, @@ -149,12 +145,12 @@ def __init__( def __init__( # noqa: PLR0913 self, - algod_client: "AlgodClient", + algod_client: AlgodClient, app_spec: au_spec.ApplicationSpecification | Path, *, app_id: int = 0, creator: str | Account | None = None, - indexer_client: "IndexerClient | None" = None, + indexer_client: IndexerClient | None = None, existing_deployments: au_deploy.AppLookup | None = None, signer: TransactionSigner | Account | None = None, sender: str | None = None, @@ -242,7 +238,7 @@ def prepare( sender: str | None = None, app_id: int | None = None, template_values: au_deploy.TemplateValueDict | None = None, - ) -> "ApplicationClient": + ) -> ApplicationClient: """Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. Will also substitute provided template_values into the associated app_spec in the copy""" new_client: ApplicationClient = copy.copy(self) @@ -253,7 +249,7 @@ def prepare( def _prepare( self, - target: "ApplicationClient", + target: ApplicationClient, *, signer: TransactionSigner | Account | None = None, sender: str | None = None, @@ -348,6 +344,8 @@ def deploy( # noqa: PLR0913 ) if config.debug and config.project_root: + from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps + persist_sourcemaps( sources=[ PersistSourceMapInput( @@ -635,6 +633,8 @@ def call( hints = self._method_hints(method) if hints and hints.read_only: if config.debug and config.project_root and config.trace_all: + from algokit_utils._debugging import simulate_and_persist_response + simulate_and_persist_response( atc, config.project_root, self.algod_client, config.trace_buffer_size_mb ) @@ -870,6 +870,8 @@ def _check_is_compiled(self) -> tuple[Program, Program]: ) if config.debug and config.project_root: + from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps + persist_sourcemaps( sources=[ PersistSourceMapInput( @@ -889,6 +891,8 @@ def _check_is_compiled(self) -> tuple[Program, Program]: def _simulate_readonly_call( self, method: Method, atc: AtomicTransactionComposer ) -> ABITransactionResponse | TransactionResponse: + from algokit_utils._debugging import simulate_response + response = simulate_response(atc, self.algod_client) traces = None if config.debug: @@ -1198,7 +1202,7 @@ def resolve_signer_sender( def substitute_template_and_compile( - algod_client: "AlgodClient", + algod_client: AlgodClient, app_spec: au_spec.ApplicationSpecification, template_values: au_deploy.TemplateValueMapping, ) -> tuple[Program, Program]: @@ -1272,7 +1276,7 @@ def _try_convert_to_logic_error( ) def execute_atc_with_logic_error( atc: AtomicTransactionComposer, - algod_client: "AlgodClient", + algod_client: AlgodClient, approval_program: str, wait_rounds: int = 4, approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, @@ -1285,6 +1289,8 @@ def execute_atc_with_logic_error( {py:class}`LogicError` ``` """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response + try: if config.debug and config.project_root and config.trace_all: simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb) diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 7aa3b9d6..b16b266b 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -7,7 +7,7 @@ from algosdk.v2client.algod import AlgodClient from typing_extensions import deprecated -from algokit_utils.models.account import Account +from algokit_utils._legacy_v2.models import Account __all__ = ["opt_in", "opt_out"] logger = logging.getLogger(__name__) diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index fc73f84b..91d6eb14 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -21,12 +21,12 @@ from algokit_utils._legacy_v2.models import ( ABIArgsDict, ABIMethod, + Account, CreateCallParameters, TransactionResponse, ) -from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_manager import AppManager -from algokit_utils.models.account import Account +from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index 316e1005..da9d129e 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -11,6 +11,7 @@ ) from typing_extensions import deprecated +from algokit_utils.models.account import SigningAccount from algokit_utils.models.simulate import SimulationTrace # Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) @@ -20,6 +21,7 @@ "ABIArgsDict", "ABIMethod", "ABITransactionResponse", + "Account", "CreateCallParameters", "CreateCallParametersDict", "CreateTransactionParameters", @@ -33,6 +35,12 @@ ReturnType = TypeVar("ReturnType") +@deprecated("Use 'SigningAccount' instead") +@dataclasses.dataclass(kw_only=True) +class Account(SigningAccount): + """An account that can be used to sign transactions""" + + @dataclasses.dataclass(kw_only=True) class TransactionResponse: """Response for a non ABI call""" diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index 27343963..1a049e5e 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -2,7 +2,8 @@ warnings.warn( """The legacy v2 account module is deprecated and will be removed in a future version. - Use `Account` abstraction from `algokit_utils.models` instead. + Use `SigningAccount` abstraction from `algokit_utils.models` instead or + classes compliant with `TransactionSignerAccountProtocol` obtained from `AccountManager`. """, DeprecationWarning, stacklevel=2, diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 9d10f42c..127d8551 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -3,19 +3,29 @@ from dataclasses import dataclass from typing import Any +import algosdk from algosdk import mnemonic -from algosdk.atomic_transaction_composer import LogicSigTransactionSigner, TransactionSigner +from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.mnemonic import to_private_key -from algosdk.transaction import LogicSigAccount, SuggestedParams +from algosdk.transaction import LogicSigAccount as AlgosdkLogicSigAccount +from algosdk.transaction import SuggestedParams from typing_extensions import Self from algokit_utils.accounts.kmd_account_manager import KmdAccountManager from algokit_utils.clients.client_manager import ClientManager from algokit_utils.clients.dispenser_api_client import DispenserAssetName, TestNetDispenserApiClient from algokit_utils.config import config -from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account, MultiSigAccount, MultisigMetadata +from algokit_utils.models.account import ( + DISPENSER_ACCOUNT_NAME, + LogicSigAccount, + MultiSigAccount, + MultisigMetadata, + SigningAccount, + TransactionSignerAccount, +) from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.account import TransactionSignerAccountProtocol from algokit_utils.transactions.transaction_composer import ( PaymentParams, SendAtomicTransactionComposerResults, @@ -65,11 +75,11 @@ class AccountInformation: See `https://developer.algorand.org/docs/rest-apis/algod/#account` for detailed field descriptions. :ivar str address: The account's address - :ivar int amount: The account's current balance in microAlgos - :ivar int amount_without_pending_rewards: The account's balance in microAlgos without the pending rewards - :ivar int min_balance: The account's minimum required balance in microAlgos - :ivar int pending_rewards: The amount of pending rewards in microAlgos - :ivar int rewards: The amount of rewards earned in microAlgos + :ivar AlgoAmount amount: The account's current balance + :ivar AlgoAmount amount_without_pending_rewards: The account's balance without the pending rewards + :ivar AlgoAmount min_balance: The account's minimum required balance + :ivar AlgoAmount pending_rewards: The amount of pending rewards + :ivar AlgoAmount rewards: The amount of rewards earned :ivar int round: The round for which this information is relevant :ivar str status: The account's status (e.g., 'Offline', 'Online') :ivar int|None total_apps_opted_in: Number of applications this account has opted into @@ -97,11 +107,11 @@ class AccountInformation: """ address: str - amount: int - amount_without_pending_rewards: int - min_balance: int - pending_rewards: int - rewards: int + amount: AlgoAmount + amount_without_pending_rewards: AlgoAmount + min_balance: AlgoAmount + pending_rewards: AlgoAmount + rewards: AlgoAmount round: int status: str total_apps_opted_in: int | None = None @@ -144,10 +154,14 @@ class AccountManager: def __init__(self, client_manager: ClientManager): self._client_manager = client_manager self._kmd_account_manager = KmdAccountManager(client_manager) - self._signers = dict[str, TransactionSigner]() + self._accounts = dict[str, TransactionSignerAccountProtocol]() self._default_signer: TransactionSigner | None = None - def set_default_signer(self, signer: TransactionSigner) -> Self: + @property + def kmd(self) -> KmdAccountManager: + return self._kmd_account_manager + + def set_default_signer(self, signer: TransactionSigner | TransactionSignerAccountProtocol) -> Self: """ Sets the default signer to use if no other signer is specified. @@ -164,7 +178,7 @@ def set_default_signer(self, signer: TransactionSigner) -> Self: >>> # then the default signer will be used >>> signer = account_manager.get_signer("{SENDERADDRESS}") """ - self._default_signer = signer + self._default_signer = signer if isinstance(signer, TransactionSigner) else signer.signer return self def set_signer(self, sender: str, signer: TransactionSigner) -> Self: @@ -178,10 +192,25 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: :example: >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) """ - self._signers[sender] = signer + self._accounts[sender] = TransactionSignerAccount(address=sender, signer=signer) + return self + + def set_signers(self, *, another_account_manager: "AccountManager", overwrite_existing: bool = True) -> Self: + """ + Merges the given `AccountManager` into this one. + + :param another_account_manager: The `AccountManager` to merge into this one + :param overwrite_existing: Whether to overwrite existing signers in this manager + :returns: The `AccountManager` instance for method chaining + """ + self._accounts = ( + {**self._accounts, **another_account_manager._accounts} # noqa: SLF001 + if overwrite_existing + else {**another_account_manager._accounts, **self._accounts} # noqa: SLF001 + ) return self - def set_signer_from_account(self, account: Account | LogicSigAccount | MultiSigAccount) -> Self: + def set_signer_from_account(self, account: TransactionSignerAccountProtocol) -> Self: """ Tracks the given account for later signing. @@ -194,18 +223,13 @@ def set_signer_from_account(self, account: Account | LogicSigAccount | MultiSigA :example: >>> account_manager = AccountManager(client_manager) >>> account_manager.set_signer_from_account(Account.new_account()) - >>> account_manager.set_signer_from_account(LogicSigAccount(program, args)) + >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) """ - if isinstance(account, LogicSigAccount): - addr = account.address() - self._signers[addr] = LogicSigTransactionSigner(account) - else: - addr = account.address - self._signers[addr] = account.signer + self._accounts[account.address] = account return self - def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSigner: + def get_signer(self, sender: str | TransactionSignerAccountProtocol) -> TransactionSigner: """ Returns the `TransactionSigner` for the given sender address. @@ -218,55 +242,40 @@ def get_signer(self, sender: str | Account | LogicSigAccount) -> TransactionSign :example: >>> signer = account_manager.get_signer("SENDERADDRESS") """ - signer = self._signers.get(self._get_address(sender)) or self._default_signer + signer = self._accounts.get(self._get_address(sender)) or self._default_signer if not signer: raise ValueError(f"No signer found for address {sender}") - return signer + return signer if isinstance(signer, TransactionSigner) else signer.signer - def get_account(self, sender: str) -> Account: + def get_account(self, sender: str) -> TransactionSignerAccountProtocol: """ - Returns the `Account` for the given sender address. + Returns the `TransactionSignerAccountProtocol` for the given sender address. :param sender: The sender address - :returns: The `Account` + :returns: The `TransactionSignerAccountProtocol` :raises ValueError: If no account is found or if the account is not a regular account :example: >>> sender = account_manager.random() >>> # ... - >>> # Returns the `Account` for `sender` that has previously been registered + >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered >>> account = account_manager.get_account(sender) """ - account = self._signers.get(sender) + account = self._accounts.get(sender) if not account: raise ValueError(f"No account found for address {sender}") - if not isinstance(account, Account): + if not isinstance(account, SigningAccount): raise ValueError(f"Account {sender} is not a regular account") return account - def get_logic_sig_account(self, sender: str) -> LogicSigAccount: - """ - Returns the `LogicSigAccount` for the given sender address. - - :param sender: The sender address - :returns: The `LogicSigAccount` - :raises ValueError: If no account is found or if the account is not a logic signature account - """ - account = self._signers.get(sender) - if not account: - raise ValueError(f"No account found for address {sender}") - if not isinstance(account, LogicSigAccount): - raise ValueError(f"Account {sender} is not a logic sig account") - return account - - def get_information(self, sender: str | Account) -> AccountInformation: + def get_information(self, sender: str | TransactionSignerAccountProtocol) -> AccountInformation: """ Returns the given sender account's current status, balance and spendable amounts. See ``_ for response data schema details. - :param sender: The address of the sender/account to look up + :param sender: The address or account compliant with `TransactionSignerAccountProtocol` protocol to look up :returns: The account information :example: @@ -276,55 +285,56 @@ def get_information(self, sender: str | Account) -> AccountInformation: info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) info = {k.replace("-", "_"): v for k, v in info.items()} + for key, value in info.items(): + if key in ("amount", "amount_without_pending_rewards", "min_balance", "pending_rewards", "rewards"): + info[key] = AlgoAmount.from_micro_algo(value) return AccountInformation(**info) - def _register_account(self, private_key: str) -> Account: + def _register_account(self, private_key: str, address: str | None = None) -> SigningAccount: """ Helper method to create and register an account with its signer. :param private_key: The private key for the account + :param address: The address for the account :returns: The registered Account instance """ - account = Account(private_key=private_key) - self._signers[account.address] = account.signer + address = address or str(algosdk.account.address_from_private_key(private_key)) + account = SigningAccount(private_key=private_key, address=address) + self._accounts[address or account.address] = TransactionSignerAccount( + address=account.address, signer=account.signer + ) return account - def _register_logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + def _register_logicsig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: """ Helper method to create and register a logic signature account. :param program: The bytes that make up the compiled logic signature :param args: The (binary) arguments to pass into the logic signature - :returns: The registered LogicSigAccount instance + :returns: The registered AlgosdkLogicSigAccount instance """ - logic_sig = LogicSigAccount(program, args) - self._signers[logic_sig.address()] = LogicSigTransactionSigner(logic_sig) + logic_sig = LogicSigAccount(AlgosdkLogicSigAccount(program, args)) + self._accounts[logic_sig.address] = logic_sig return logic_sig - def _register_multi_sig( - self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] - ) -> MultiSigAccount: + def _register_multisig(self, metadata: MultisigMetadata, signing_accounts: list[SigningAccount]) -> MultiSigAccount: """ Helper method to create and register a multisig account. - :param version: The version of the multisig account - :param threshold: The threshold number of signatures required - :param addrs: The list of addresses that can sign + :param metadata: The metadata for the multisig account :param signing_accounts: The list of accounts that are present to sign :returns: The registered MultisigAccount instance """ - msig_account = MultiSigAccount( - MultisigMetadata(version=version, threshold=threshold, addresses=addrs), - signing_accounts, - ) - self._signers[str(msig_account.address)] = msig_account.signer + msig_account = MultiSigAccount(metadata, signing_accounts) + self._accounts[str(msig_account.address)] = MultiSigAccount(metadata, signing_accounts) return msig_account - def from_mnemonic(self, mnemonic: str) -> Account: + def from_mnemonic(self, *, mnemonic: str, sender: str | None = None) -> SigningAccount: """ Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. :param mnemonic: The mnemonic secret representing the private key of an account + :param sender: Optional address to use as the sender :returns: The account .. warning:: @@ -334,10 +344,9 @@ def from_mnemonic(self, mnemonic: str) -> Account: :example: >>> account = account_manager.from_mnemonic("mnemonic secret ...") """ - private_key = to_private_key(mnemonic) - return self._register_account(private_key) + return self._register_account(to_private_key(mnemonic), sender) - def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Account: + def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> SigningAccount: """ Tracks and returns an Algorand account with private key loaded by convention from environment variables. @@ -378,7 +387,7 @@ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Ac def from_kmd( self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None - ) -> Account: + ) -> SigningAccount: """ Tracks and returns an Algorand account with private key loaded from the given KMD wallet. @@ -400,7 +409,7 @@ def from_kmd( return self._register_account(kmd_account.private_key) - def logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + def logicsig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: """ Tracks and returns an account that represents a logic signature. @@ -411,17 +420,13 @@ def logic_sig(self, program: bytes, args: list[bytes] | None = None) -> LogicSig :example: >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) """ - return self._register_logic_sig(program, args) + return self._register_logicsig(program, args) - def multi_sig( - self, version: int, threshold: int, addrs: list[str], signing_accounts: list[Account] - ) -> MultiSigAccount: + def multisig(self, metadata: MultisigMetadata, signing_accounts: list[SigningAccount]) -> MultiSigAccount: """ Tracks and returns an account that supports partial or full multisig signing. - :param version: The version of the multisig account - :param threshold: The threshold number of signatures required - :param addrs: The list of addresses that can sign + :param metadata: The metadata for the multisig account :param signing_accounts: The signers that are currently present :returns: A multisig account wrapper @@ -433,9 +438,9 @@ def multi_sig( ... signing_accounts=[account1, account2] ... ) """ - return self._register_multi_sig(version, threshold, addrs, signing_accounts) + return self._register_multisig(metadata, signing_accounts) - def random(self) -> Account: + def random(self) -> SigningAccount: """ Tracks and returns a new, random Algorand account. @@ -444,10 +449,10 @@ def random(self) -> Account: :example: >>> account = account_manager.random() """ - account = Account.new_account() + account = SigningAccount.new_account() return self._register_account(account.private_key) - def localnet_dispenser(self) -> Account: + def localnet_dispenser(self) -> SigningAccount: """ Returns an Algorand account with private key loaded for the default LocalNet dispenser account. @@ -461,7 +466,7 @@ def localnet_dispenser(self) -> Account: kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() return self._register_account(kmd_account.private_key) - def dispenser_from_environment(self) -> Account: + def dispenser_from_environment(self) -> SigningAccount: """ Returns an account (with private key loaded) that can act as a dispenser from environment variables. @@ -477,7 +482,9 @@ def dispenser_from_environment(self) -> Account: return self.from_environment(DISPENSER_ACCOUNT_NAME) return self.localnet_dispenser() - def rekeyed(self, sender: Account | str, account: Account) -> Account: + def rekeyed( + self, *, sender: str, account: TransactionSignerAccountProtocol + ) -> TransactionSignerAccount | SigningAccount: """ Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. @@ -489,14 +496,16 @@ def rekeyed(self, sender: Account | str, account: Account) -> Account: >>> account = account.from_mnemonic("mnemonic secret ...") >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") """ - sender_address = sender.address if isinstance(sender, Account) else sender - self._signers[sender_address] = account.signer - return Account(address=sender_address, private_key=account.private_key) + sender_address = sender.address if isinstance(sender, SigningAccount) else sender + self._accounts[sender_address] = TransactionSignerAccount(address=sender_address, signer=account.signer) + if isinstance(account, SigningAccount): + return SigningAccount(address=sender_address, private_key=account.private_key) + return TransactionSignerAccount(address=sender_address, signer=account.signer) def rekey_account( # noqa: PLR0913 self, - account: str | Account, - rekey_to: str | Account, + account: str, + rekey_to: str | TransactionSignerAccountProtocol, *, # Common transaction parameters signer: TransactionSigner | None = None, note: bytes | None = None, @@ -575,8 +584,8 @@ def rekey_account( # noqa: PLR0913 ) # If rekey_to is a signing account, set it as the signer for this account - if isinstance(rekey_to, Account): - self.rekeyed(account, rekey_to) + if isinstance(rekey_to, SigningAccount): + self.rekeyed(sender=account, account=rekey_to) if not suppress_log: logger.info(f"Rekeyed {sender_address} to {rekey_address} via transaction {result.tx_ids[-1]}") @@ -585,8 +594,8 @@ def rekey_account( # noqa: PLR0913 def ensure_funded( # noqa: PLR0913 self, - account_to_fund: str | Account, - dispenser_account: str | Account, + account_to_fund: str | SigningAccount, + dispenser_account: str | SigningAccount, min_spending_balance: AlgoAmount, min_funding_increment: AlgoAmount | None = None, # Sender params @@ -686,7 +695,7 @@ def ensure_funded( # noqa: PLR0913 def ensure_funded_from_environment( # noqa: PLR0913 self, - account_to_fund: str | Account, + account_to_fund: str | SigningAccount, min_spending_balance: AlgoAmount, *, # Force remaining params to be keyword-only min_funding_increment: AlgoAmount | None = None, @@ -792,7 +801,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 def ensure_funded_from_testnet_dispenser_api( self, - account_to_fund: str | Account, + account_to_fund: str | SigningAccount, dispenser_client: TestNetDispenserApiClient, min_spending_balance: AlgoAmount, *, @@ -851,12 +860,10 @@ def ensure_funded_from_testnet_dispenser_api( amount_funded=AlgoAmount.from_micro_algo(result.amount), ) - def _get_address(self, sender: str | Account | LogicSigAccount) -> str: + def _get_address(self, sender: str | TransactionSignerAccountProtocol) -> str: match sender: - case Account(): + case TransactionSignerAccountProtocol(): return sender.address - case LogicSigAccount(): - return sender.address() case str(): return sender case _: @@ -877,11 +884,11 @@ def _get_suggested_params() -> SuggestedParams: def _calculate_fund_amount( self, min_spending_balance: int, - current_spending_balance: int, + current_spending_balance: AlgoAmount, min_funding_increment: int, ) -> int | None: if min_spending_balance > current_spending_balance: - min_fund_amount = min_spending_balance - current_spending_balance + min_fund_amount = (min_spending_balance - current_spending_balance).micro_algo return max(min_fund_amount, min_funding_increment) return None diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index a7f8d142..ae3c8c7b 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -5,7 +5,7 @@ from algokit_utils.clients.client_manager import ClientManager from algokit_utils.config import config -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer @@ -14,7 +14,7 @@ logger = config.logger -class KmdAccount(Account): +class KmdAccount(SigningAccount): """Account retrieved from KMD with signing capabilities, extending base Account. Provides an account implementation that can be used to sign transactions using keys stored in KMD. diff --git a/src/algokit_utils/algorand.py b/src/algokit_utils/algorand.py index 7c03c8f5..83a8c56d 100644 --- a/src/algokit_utils/algorand.py +++ b/src/algokit_utils/algorand.py @@ -13,7 +13,8 @@ from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs +from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig +from algokit_utils.protocols.account import TransactionSignerAccountProtocol from algokit_utils.transactions.transaction_composer import ( TransactionComposer, ) @@ -61,11 +62,13 @@ def set_default_validity_window(self, validity_window: int) -> typing_extensions self._default_validity_window = validity_window return self - def set_default_signer(self, signer: TransactionSigner) -> typing_extensions.Self: + def set_default_signer( + self, signer: TransactionSigner | TransactionSignerAccountProtocol + ) -> typing_extensions.Self: """ Sets the default signer to use if no other signer is specified. - :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount` + :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol` :return: The `AlgorandClient` so method calls can be chained """ self._account_manager.set_default_signer(signer) @@ -82,6 +85,16 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extension self._account_manager.set_signer(sender, signer) return self + def set_signer_account(self, signer: TransactionSignerAccountProtocol) -> typing_extensions.Self: + """ + Sets the default signer to use if no other signer is specified. + + :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol` + :return: The `AlgorandClient` so method calls can be chained + """ + self._account_manager.set_default_signer(signer) + return self + def set_suggested_params( self, suggested_params: SuggestedParams, until: float | None = None ) -> typing_extensions.Self: @@ -235,9 +248,9 @@ def from_environment() -> "AlgorandClient": @staticmethod def from_config( - algod_config: AlgoClientConfig, - indexer_config: AlgoClientConfig | None = None, - kmd_config: AlgoClientConfig | None = None, + algod_config: AlgoClientNetworkConfig, + indexer_config: AlgoClientNetworkConfig | None = None, + kmd_config: AlgoClientNetworkConfig | None = None, ) -> "AlgorandClient": """ Returns an `AlgorandClient` from the given config. diff --git a/src/algokit_utils/applications/__init__.py b/src/algokit_utils/applications/__init__.py index 9e4e3158..e872ef12 100644 --- a/src/algokit_utils/applications/__init__.py +++ b/src/algokit_utils/applications/__init__.py @@ -1,5 +1,7 @@ +from algokit_utils.applications.abi import * # noqa: F403 from algokit_utils.applications.app_client import * # noqa: F403 from algokit_utils.applications.app_deployer import * # noqa: F403 from algokit_utils.applications.app_factory import * # noqa: F403 from algokit_utils.applications.app_manager import * # noqa: F403 from algokit_utils.applications.app_spec import * # noqa: F403 +from algokit_utils.applications.enums import * # noqa: F403 diff --git a/src/algokit_utils/applications/abi.py b/src/algokit_utils/applications/abi.py index 2dbfd6b1..7c0ef42f 100644 --- a/src/algokit_utils/applications/abi.py +++ b/src/algokit_utils/applications/abi.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Any, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias import algosdk from algosdk.abi.method import Method as AlgorandABIMethod @@ -7,7 +9,9 @@ from algokit_utils.applications.app_spec.arc56 import Arc56Contract, StructField from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method -from algokit_utils.models.state import BoxName + +if TYPE_CHECKING: + from algokit_utils.models.state import BoxName ABIValue: TypeAlias = ( bool | int | str | bytes | bytearray | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index b74e14f6..6d797ec5 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -43,6 +43,7 @@ ) from algokit_utils.models.state import BoxName, BoxValue from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.account import TransactionSignerAccountProtocol from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, AppCallParams, @@ -66,7 +67,7 @@ from algosdk.atomic_transaction_composer import TransactionSigner from algokit_utils.algorand import AlgorandClient - from algokit_utils.applications.app_deployer import AppLookup + from algokit_utils.applications.app_deployer import ApplicationLookup from algokit_utils.applications.app_manager import AppManager from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams @@ -217,10 +218,6 @@ class FundAppAccountParams: :ivar last_valid_round: Optional last valid round :ivar amount: Amount to fund :ivar close_remainder_to: Optional address to close remainder to - :ivar max_rounds_to_wait: Optional maximum rounds to wait - :ivar suppress_log: Optional flag to suppress logging - :ivar populate_app_call_resources: Optional flag to populate app call resources - :ivar cover_app_call_inner_txn_fees: Optional flag to cover app call inner transaction fees :ivar on_complete: Optional on complete action """ @@ -237,10 +234,6 @@ class FundAppAccountParams: last_valid_round: int | None = None amount: AlgoAmount close_remainder_to: str | None = None - max_rounds_to_wait: int | None = None - suppress_log: bool | None = None - populate_app_call_resources: bool | None = None - cover_app_call_inner_txn_fees: bool | None = None on_complete: algosdk.transaction.OnComplete | None = None @@ -810,9 +803,7 @@ def update( self._client.compile( app_spec=self._client.app_spec, app_manager=self._algorand.app, - deploy_time_params=compilation_params.get("deploy_time_params"), - updatable=compilation_params.get("updatable"), - deletable=compilation_params.get("deletable"), + compilation_params=compilation_params, ).__dict__ if compilation_params else {} @@ -1040,7 +1031,11 @@ def update( params = params or AppClientBareCallParams() compilation = compilation_params or AppClientCompilationParams() compiled = self._client.compile_app( - compilation.get("deploy_time_params"), compilation.get("updatable"), compilation.get("deletable") + { + "deploy_time_params": compilation.get("deploy_time_params"), + "updatable": compilation.get("updatable"), + "deletable": compilation.get("deletable"), + } ) bare_params = self._client.params.bare.update(params) bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) @@ -1314,7 +1309,7 @@ class AppClientParams: algorand: AlgorandClient app_id: int app_name: str | None = None - default_sender: str | bytes | None = None + default_sender: str | None = None default_signer: TransactionSigner | None = None approval_source_map: SourceMap | None = None clear_source_map: SourceMap | None = None @@ -1445,7 +1440,7 @@ def from_network( app_spec: Arc56Contract | Arc32Contract | str, algorand: AlgorandClient, app_name: str | None = None, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, @@ -1500,12 +1495,12 @@ def from_creator_and_name( app_name: str, app_spec: Arc56Contract | Arc32Contract | str, algorand: AlgorandClient, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, + app_lookup_cache: ApplicationLookup | None = None, ) -> AppClient: """Create an AppClient instance from creator address and application name. @@ -1547,20 +1542,20 @@ def from_creator_and_name( def compile( app_spec: Arc56Contract, app_manager: AppManager, - deploy_time_params: TealTemplateParams | None = None, - updatable: bool | None = None, - deletable: bool | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> AppClientCompilationResult: """Compile the application's TEAL code. :param app_spec: The application specification :param app_manager: The application manager instance - :param deploy_time_params: Optional deployment time parameters - :param updatable: Optional flag indicating if app is updatable - :param deletable: Optional flag indicating if app is deletable + :param compilation_params: Optional compilation parameters :return: The compilation result :raises ValueError: If attempting to compile without source or byte code """ + compilation_params = compilation_params or AppClientCompilationParams() + deploy_time_params = compilation_params.get("deploy_time_params") + updatable = compilation_params.get("updatable") + deletable = compilation_params.get("deletable") def is_base64(s: str) -> bool: try: @@ -1707,18 +1702,14 @@ def get_line_for_pc(input_pc: int) -> int | None: def compile_app( self, - deploy_time_params: TealTemplateParams | None = None, - updatable: bool | None = None, - deletable: bool | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> AppClientCompilationResult: """Compile the application's TEAL code. - :param deploy_time_params: Optional deployment time parameters - :param updatable: Optional flag indicating if app is updatable - :param deletable: Optional flag indicating if app is deletable + :param compilation_params: Optional compilation parameters :return: The compilation result """ - result = AppClient.compile(self._app_spec, self._algorand.app, deploy_time_params, updatable, deletable) + result = AppClient.compile(self._app_spec, self._algorand.app, compilation_params) if result.compiled_approval: self._approval_source_map = result.compiled_approval.source_map @@ -1730,7 +1721,7 @@ def compile_app( def clone( self, app_name: str | None = _MISSING, # type: ignore[assignment] - default_sender: str | bytes | None = _MISSING, # type: ignore[assignment] + default_sender: str | None = _MISSING, # type: ignore[assignment] default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment] approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] @@ -1924,7 +1915,9 @@ def _get_sender(self, sender: str | None) -> str: ) return sender or self._default_sender # type: ignore[return-value] - def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: + def _get_signer( + self, sender: str | None, signer: TransactionSigner | TransactionSignerAccountProtocol | None + ) -> TransactionSigner | TransactionSignerAccountProtocol | None: return signer or (self._default_signer if not sender or sender == self._default_sender else None) def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 6e12c785..830c3452 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -2,7 +2,6 @@ import dataclasses import json from dataclasses import asdict, dataclass -from enum import Enum from typing import Literal from algosdk.logic import get_application_address @@ -10,8 +9,10 @@ from algokit_utils.applications.abi import ABIReturn from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.config import config from algokit_utils.models.state import TealTemplateParams +from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCallParams, AppCreateParams, @@ -30,13 +31,13 @@ __all__ = [ "APP_DEPLOY_NOTE_DAPP", - "AppDeployMetaData", "AppDeployParams", "AppDeployResult", "AppDeployer", - "AppLookup", - "AppMetaData", - "AppReference", + "AppDeploymentMetaData", + "ApplicationLookup", + "ApplicationMetaData", + "ApplicationReference", "OnSchemaBreak", "OnUpdate", "OperationPerformed", @@ -48,8 +49,8 @@ logger = config.logger -@dataclasses.dataclass(frozen=True) -class AppDeployMetaData: +@dataclasses.dataclass +class AppDeploymentMetaData: """Metadata about an application stored in a transaction note during creation.""" name: str @@ -62,7 +63,7 @@ def dictify(self) -> dict[str, str | bool]: @dataclasses.dataclass(frozen=True) -class AppReference: +class ApplicationReference: """Information about an Algorand app""" app_id: int @@ -70,11 +71,11 @@ class AppReference: @dataclasses.dataclass(frozen=True) -class AppMetaData: +class ApplicationMetaData: """Complete metadata about a deployed app""" - reference: AppReference - deploy_metadata: AppDeployMetaData + reference: ApplicationReference + deploy_metadata: AppDeploymentMetaData created_round: int updated_round: int deleted: bool = False @@ -105,78 +106,38 @@ def updatable(self) -> bool | None: @dataclasses.dataclass -class AppLookup: - """Cache of {py:class}`AppMetaData` for a specific `creator` +class ApplicationLookup: + """Cache of {py:class}`ApplicationMetaData` for a specific `creator` Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple apps or discovering multiple app_ids """ creator: str - apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) - - -class OnSchemaBreak(Enum): - """Action to take if an Application's schema has breaking changes""" - - Fail = 0 - """Fail the deployment""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new Application""" - - -class OnUpdate(Enum): - """Action to take if an Application has been updated""" - - Fail = 0 - """Fail the deployment""" - UpdateApp = 1 - """Update the Application with the new approval and clear programs""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new application""" - - -class OperationPerformed(Enum): - """Describes the actions taken during deployment""" - - Nothing = 0 - """An existing Application was found""" - Create = 1 - """No existing Application was found, created a new Application""" - Update = 2 - """An existing Application was found, but was out of date, updated to latest version""" - Replace = 3 - """An existing Application was found, but was out of date, created a new Application and deleted the original""" + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) @dataclass(kw_only=True) class AppDeployParams: """Parameters for deploying an app""" - metadata: AppDeployMetaData + metadata: AppDeploymentMetaData deploy_time_params: TealTemplateParams | None = None - on_schema_break: Literal["replace", "fail", "append"] | OnSchemaBreak = OnSchemaBreak.Fail - on_update: Literal["update", "replace", "fail", "append"] | OnUpdate = OnUpdate.Fail + on_schema_break: (Literal["replace", "fail", "append"] | OnSchemaBreak) | None = None + on_update: (Literal["update", "replace", "fail", "append"] | OnUpdate) | None = None create_params: AppCreateParams | AppCreateMethodCallParams update_params: AppUpdateParams | AppUpdateMethodCallParams delete_params: AppDeleteParams | AppDeleteMethodCallParams - existing_deployments: AppLookup | None = None + existing_deployments: ApplicationLookup | None = None ignore_cache: bool = False max_fee: int | None = None - max_rounds_to_wait: int | None = None - suppress_log: bool = False - populate_app_call_resources: bool | None = None - cover_app_call_inner_txn_fees: bool | None = None + send_params: SendParams | None = None # Union type for all possible deploy results @dataclass(frozen=True) class AppDeployResult: - app: AppMetaData + app: ApplicationMetaData operation_performed: OperationPerformed create_result: SendAppCreateTransactionResult[ABIReturn] | None = None update_result: SendAppUpdateTransactionResult[ABIReturn] | None = None @@ -195,17 +156,20 @@ def __init__( self._app_manager = app_manager self._transaction_sender = transaction_sender self._indexer = indexer - self._app_lookups: dict[str, AppLookup] = {} + self._app_lookups: dict[str, ApplicationLookup] = {} def deploy(self, deployment: AppDeployParams) -> AppDeployResult: # Create new instances with updated notes + send_params = deployment.send_params or SendParams() + suppress_log = send_params.get("suppress_log") or False + logger.info( f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of " f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and " f"{len(deployment.create_params.clear_state_program)} bytes of " f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}", - suppress_log=deployment.suppress_log, + suppress_log=suppress_log, ) note = TransactionComposer.arc2_note( { @@ -306,7 +270,7 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResult: }, "to": deployment.create_params.schema, }, - suppress_log=deployment.suppress_log, + suppress_log=suppress_log, ) return self._handle_schema_break( @@ -324,7 +288,7 @@ def deploy(self, deployment: AppDeployParams) -> AppDeployResult: clear_program=clear_program, ) - logger.debug("No detected changes in app, nothing to do.", suppress_log=deployment.suppress_log) + logger.debug("No detected changes in app, nothing to do.", suppress_log=suppress_log) return AppDeployResult( app=existing_app, operation_performed=OperationPerformed.Nothing, @@ -346,7 +310,8 @@ def _create_app( "approval_program": approval_program, "clear_state_program": clear_program, } - ) + ), + send_params=deployment.send_params, ) else: create_result = self._transaction_sender.app_create( @@ -356,11 +321,12 @@ def _create_app( "approval_program": approval_program, "clear_state_program": clear_program, } - ) + ), + send_params=deployment.send_params, ) - app_metadata = AppMetaData( - reference=AppReference( + app_metadata = ApplicationMetaData( + reference=ApplicationReference( app_id=create_result.app_id, app_address=get_application_address(create_result.app_id) ), deploy_metadata=deployment.metadata, @@ -384,7 +350,7 @@ def _create_app( def _replace_app( self, deployment: AppDeployParams, - existing_app: AppMetaData, + existing_app: ApplicationMetaData, approval_program: bytes, clear_program: bytes, ) -> AppDeployResult: @@ -438,8 +404,8 @@ def _replace_app( delete_result = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index) app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] - app_metadata = AppMetaData( - reference=AppReference(app_id=app_id, app_address=get_application_address(app_id)), + app_metadata = ApplicationMetaData( + reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)), deploy_metadata=deployment.metadata, created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] @@ -458,7 +424,7 @@ def _replace_app( def _update_app( self, deployment: AppDeployParams, - existing_app: AppMetaData, + existing_app: ApplicationMetaData, approval_program: bytes, clear_program: bytes, ) -> AppDeployResult: @@ -473,7 +439,8 @@ def _update_app( "approval_program": approval_program, "clear_state_program": clear_program, } - ) + ), + send_params=deployment.send_params, ) else: result = self._transaction_sender.app_update( @@ -484,11 +451,12 @@ def _update_app( "approval_program": approval_program, "clear_state_program": clear_program, } - ) + ), + send_params=deployment.send_params, ) - app_metadata = AppMetaData( - reference=AppReference(app_id=existing_app.app_id, app_address=existing_app.app_address), + app_metadata = ApplicationMetaData( + reference=ApplicationReference(app_id=existing_app.app_id, app_address=existing_app.app_address), deploy_metadata=deployment.metadata, created_round=existing_app.created_round, updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, @@ -506,11 +474,11 @@ def _update_app( def _handle_schema_break( self, deployment: AppDeployParams, - existing_app: AppMetaData, + existing_app: ApplicationMetaData, approval_program: bytes, clear_program: bytes, ) -> AppDeployResult: - if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): + if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail") or deployment.on_schema_break is None: raise ValueError( "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " "If you want to try deleting and recreating the app then " @@ -528,11 +496,11 @@ def _handle_schema_break( def _handle_update( self, deployment: AppDeployParams, - existing_app: AppMetaData, + existing_app: ApplicationMetaData, approval_program: bytes, clear_program: bytes, ) -> AppDeployResult: - if deployment.on_update in (OnUpdate.Fail, "fail"): + if deployment.on_update in (OnUpdate.Fail, "fail") or deployment.on_update is None: raise ValueError( "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." ) @@ -554,19 +522,19 @@ def _handle_update( raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") - def _update_app_lookup(self, sender: str, app_metadata: AppMetaData) -> None: + def _update_app_lookup(self, sender: str, app_metadata: ApplicationMetaData) -> None: """Update the app lookup cache""" lookup = self._app_lookups.get(sender) if not lookup: - self._app_lookups[sender] = AppLookup( + self._app_lookups[sender] = ApplicationLookup( creator=sender, apps={app_metadata.name: app_metadata}, ) else: lookup.apps[app_metadata.name] = app_metadata - def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> AppLookup: + def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> ApplicationLookup: """Get apps created by an account""" if not ignore_cache and creator_address in self._app_lookups: @@ -578,7 +546,7 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = "but received a call to get_creator_apps" ) - app_lookup: dict[str, AppMetaData] = {} + app_lookup: dict[str, ApplicationMetaData] = {} # Get all apps created by account created_apps = self._indexer.search_applications(creator=creator_address) @@ -609,9 +577,9 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :]) if metadata.get("name"): - app_lookup[metadata["name"]] = AppMetaData( - reference=AppReference(app_id=app_id, app_address=get_application_address(app_id)), - deploy_metadata=AppDeployMetaData( + app_lookup[metadata["name"]] = ApplicationMetaData( + reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)), + deploy_metadata=AppDeploymentMetaData( name=metadata["name"], version=metadata.get("version", "1.0"), deletable=metadata.get("deletable"), @@ -627,6 +595,6 @@ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = ) continue - lookup = AppLookup(creator=creator_address, apps=app_lookup) + lookup = ApplicationLookup(creator=creator_address, apps=app_lookup) self._app_lookups[creator_address] = lookup return lookup diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index b7d469b7..ffcd3339 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -30,11 +30,11 @@ CreateOnComplete, ) from algokit_utils.applications.app_deployer import ( - AppDeployMetaData, + AppDeploymentMetaData, AppDeployParams, AppDeployResult, - AppLookup, - AppMetaData, + ApplicationLookup, + ApplicationMetaData, OnSchemaBreak, OnUpdate, OperationPerformed, @@ -44,7 +44,6 @@ from algokit_utils.models.application import ( AppSourceMaps, ) -from algokit_utils.models.state import TealTemplateParams from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCallParams, @@ -82,12 +81,10 @@ class AppFactoryParams: algorand: AlgorandClient app_spec: Arc56Contract | ApplicationSpecification | str app_name: str | None = None - default_sender: str | bytes | None = None + default_sender: str | None = None default_signer: TransactionSigner | None = None version: str | None = None - updatable: bool | None = None - deletable: bool | None = None - deploy_time_params: TealTemplateParams | None = None + compilation_params: AppClientCompilationParams | None = None @dataclass(kw_only=True, frozen=True) @@ -139,7 +136,7 @@ class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult[Arc56 class AppFactoryDeployResult: """Result from deploying an application via AppFactory""" - app: AppMetaData + app: ApplicationMetaData operation_performed: OperationPerformed create_result: SendAppCreateFactoryTransactionResult | None = None update_result: SendAppUpdateFactoryTransactionResult | None = None @@ -501,15 +498,17 @@ def __init__(self, params: AppFactoryParams) -> None: self._version = params.version or "1.0" self._default_sender = params.default_sender self._default_signer = params.default_signer - self._deploy_time_params = params.deploy_time_params - self._updatable = params.updatable - self._deletable = params.deletable self._approval_source_map: SourceMap | None = None self._clear_source_map: SourceMap | None = None self._params_accessor = _MethodParamsBuilder(self) self._send_accessor = _TransactionSender(self) self._create_transaction_accessor = _TransactionCreator(self) + compilation_params = params.compilation_params or AppClientCompilationParams() + self._deploy_time_params = compilation_params.get("deploy_time_params") + self._updatable = compilation_params.get("updatable") + self._deletable = compilation_params.get("deletable") + @property def app_name(self) -> str: return self._app_name @@ -534,34 +533,35 @@ def send(self) -> _TransactionSender: def create_transaction(self) -> _TransactionCreator: return self._create_transaction_accessor - def deploy( # noqa: PLR0913 + def deploy( self, *, - deploy_time_params: TealTemplateParams | None = None, - on_update: OnUpdate = OnUpdate.Fail, - on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + on_update: OnUpdate | None = None, + on_schema_break: OnSchemaBreak | None = None, create_params: AppClientMethodCallCreateParams | AppClientBareCallCreateParams | None = None, update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, - existing_deployments: AppLookup | None = None, + existing_deployments: ApplicationLookup | None = None, ignore_cache: bool = False, - updatable: bool | None = None, - deletable: bool | None = None, app_name: str | None = None, - max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> tuple[AppClient, AppFactoryDeployResult]: """Deploy the application with the specified parameters.""" # Resolve control parameters with factory defaults + send_params = send_params or SendParams() + compilation_params = compilation_params or AppClientCompilationParams() resolved_updatable = ( - updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") + upd + if (upd := compilation_params.get("updatable")) is not None + else self._updatable or self._get_deploy_time_control("updatable") ) resolved_deletable = ( - deletable if deletable is not None else self._deletable or self._get_deploy_time_control("deletable") + dlb + if (dlb := compilation_params.get("deletable")) is not None + else self._deletable or self._get_deploy_time_control("deletable") ) - resolved_deploy_time_params = deploy_time_params or self._deploy_time_params + resolved_deploy_time_params = compilation_params.get("deploy_time_params") or self._deploy_time_params def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: """Prepare create arguments based on parameter type.""" @@ -615,16 +615,13 @@ def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: create_params=prepare_create_args(), update_params=prepare_update_args(), delete_params=prepare_delete_args(), - metadata=AppDeployMetaData( + metadata=AppDeploymentMetaData( name=app_name or self._app_name, version=self._version, updatable=resolved_updatable, deletable=resolved_deletable, ), - suppress_log=suppress_log, - max_rounds_to_wait=max_rounds_to_wait, - populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + send_params=send_params, ) deploy_result = self._algorand.app_deployer.deploy(deploy_params) @@ -654,7 +651,7 @@ def get_app_client_by_id( self, app_id: int, app_name: str | None = None, - default_sender: str | bytes | None = None, # Address can be string or bytes + default_sender: str | None = None, # Address can be string or bytes default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, @@ -676,10 +673,10 @@ def get_app_client_by_creator_and_name( self, creator_address: str, app_name: str, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, + app_lookup_cache: ApplicationLookup | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: @@ -716,9 +713,7 @@ def compile(self, compilation_params: AppClientCompilationParams | None = None) result = AppClient.compile( app_spec=self._app_spec, app_manager=self._algorand.app, - deploy_time_params=compilation.get("deploy_time_params") if compilation else None, - updatable=compilation.get("updatable") if compilation else None, - deletable=compilation.get("deletable") if compilation else None, + compilation_params=compilation, ) if result.compiled_approval: diff --git a/src/algokit_utils/applications/enums.py b/src/algokit_utils/applications/enums.py new file mode 100644 index 00000000..20d7e786 --- /dev/null +++ b/src/algokit_utils/applications/enums.py @@ -0,0 +1,40 @@ +from enum import Enum + +# NOTE: this is moved to a separate file to avoid circular imports + + +class OnSchemaBreak(Enum): + """Action to take if an Application's schema has breaking changes""" + + Fail = 0 + """Fail the deployment""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new Application""" + + +class OnUpdate(Enum): + """Action to take if an Application has been updated""" + + Fail = 0 + """Fail the deployment""" + UpdateApp = 1 + """Update the Application with the new approval and clear programs""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new application""" + + +class OperationPerformed(Enum): + """Describes the actions taken during deployment""" + + Nothing = 0 + """An existing Application was found""" + Create = 1 + """No existing Application was found, created a new Application""" + Update = 2 + """An existing Application was found, but was out of date, updated to latest version""" + Replace = 3 + """An existing Application was found, but was out of date, created a new Application and deleted the original""" diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 8385c415..571b748b 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -5,7 +5,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner from algosdk.v2client import algod -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.transaction import SendParams from algokit_utils.transactions.transaction_composer import ( @@ -131,7 +131,7 @@ def get_by_id(self, asset_id: int) -> AssetInformation: ) def get_account_information( - self, sender: str | Account | TransactionSigner, asset_id: int + self, sender: str | SigningAccount | TransactionSigner, asset_id: int ) -> AccountAssetInformation: """Returns the given sender account's asset holding for a given asset. @@ -152,7 +152,7 @@ def get_account_information( def bulk_opt_in( # noqa: PLR0913 self, - account: str | Account | TransactionSigner, + account: str, asset_ids: list[int], signer: TransactionSigner | None = None, rekey_to: str | None = None, @@ -216,7 +216,7 @@ def bulk_opt_in( # noqa: PLR0913 def bulk_opt_out( # noqa: C901, PLR0913 self, *, - account: str | Account | TransactionSigner, + account: str, asset_ids: list[int], ensure_zero_balance: bool = True, signer: TransactionSigner | None = None, @@ -306,10 +306,10 @@ def bulk_opt_out( # noqa: C901, PLR0913 return results @staticmethod - def _get_address_from_sender(sender: str | Account | TransactionSigner) -> str: + def _get_address_from_sender(sender: str | SigningAccount | TransactionSigner) -> str: if isinstance(sender, str): return sender - if isinstance(sender, Account): + if isinstance(sender, SigningAccount): return sender.address if isinstance(sender, AccountTransactionSigner): return str(algosdk.account.address_from_private_key(sender.private_key)) diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 6d0b3a06..9cd029f5 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from dataclasses import dataclass from typing import TYPE_CHECKING, Literal, TypeVar @@ -12,16 +14,15 @@ from algosdk.v2client.indexer import IndexerClient from algokit_utils._legacy_v2.application_specification import ApplicationSpecification -from algokit_utils.applications.app_client import AppClient, AppClientParams -from algokit_utils.applications.app_deployer import AppLookup +from algokit_utils.applications.app_deployer import ApplicationLookup from algokit_utils.applications.app_spec.arc56 import Arc56Contract from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient -from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs -from algokit_utils.models.state import TealTemplateParams +from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol if TYPE_CHECKING: from algokit_utils.algorand import AlgorandClient + from algokit_utils.applications.app_client import AppClient, AppClientCompilationParams from algokit_utils.applications.app_factory import AppFactory __all__ = [ @@ -69,7 +70,7 @@ class NetworkDetail: genesis_hash: str -def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: +def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkConfig: server = os.getenv(f"{environment_prefix}_SERVER") if server is None: raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") @@ -77,7 +78,7 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: if port: parsed = parse.urlparse(server) server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() - return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) + return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) class ClientManager: @@ -89,7 +90,7 @@ class ClientManager: :param algorand_client: AlgorandClient instance """ - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: "AlgorandClient"): + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClient): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs elif isinstance(clients_or_configs, AlgoClientConfigs): @@ -204,10 +205,8 @@ def get_app_factory( default_sender: str | None = None, default_signer: TransactionSigner | None = None, version: str | None = None, - updatable: bool | None = None, - deletable: bool | None = None, - deploy_time_params: TealTemplateParams | None = None, - ) -> "AppFactory": + compilation_params: AppClientCompilationParams | None = None, + ) -> AppFactory: """Get an application factory for deploying smart contracts. :param app_spec: Application specification @@ -215,9 +214,7 @@ def get_app_factory( :param default_sender: Optional default sender address :param default_signer: Optional default transaction signer :param version: Optional version string - :param updatable: Optional flag to make app updatable - :param deletable: Optional flag to make app deletable - :param deploy_time_params: Optional deployment parameters + :param compilation_params: Optional compilation parameters :raises ValueError: If no Algorand client is configured :return: Application factory instance """ @@ -234,9 +231,7 @@ def get_app_factory( default_sender=default_sender, default_signer=default_signer, version=version, - updatable=updatable, - deletable=deletable, - deploy_time_params=deploy_time_params, + compilation_params=compilation_params, ) ) @@ -245,7 +240,7 @@ def get_app_client_by_id( app_spec: (Arc56Contract | ApplicationSpecification | str), app_id: int, app_name: str | None = None, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, @@ -262,6 +257,8 @@ def get_app_client_by_id( :raises ValueError: If no Algorand client is configured :return: Application client instance """ + from algokit_utils.applications.app_client import AppClient, AppClientParams + if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -282,7 +279,7 @@ def get_app_client_by_network( self, app_spec: (Arc56Contract | ApplicationSpecification | str), app_name: str | None = None, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, @@ -298,6 +295,8 @@ def get_app_client_by_network( :raises ValueError: If no Algorand client is configured :return: Application client instance """ + from algokit_utils.applications.app_client import AppClient + if not self._algorand: raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") @@ -316,10 +315,10 @@ def get_app_client_by_creator_and_name( creator_address: str, app_name: str, app_spec: Arc56Contract | ApplicationSpecification | str, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, + app_lookup_cache: ApplicationLookup | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> AppClient: @@ -336,6 +335,8 @@ def get_app_client_by_creator_and_name( :param clear_source_map: Optional clear program source map :return: Application client instance """ + from algokit_utils.applications.app_client import AppClient + return AppClient.from_creator_and_name( creator_address=creator_address, app_name=app_name, @@ -350,7 +351,7 @@ def get_app_client_by_creator_and_name( ) @staticmethod - def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: + def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClient: """Get an Algod client from config or environment. :param config: Optional client configuration @@ -369,7 +370,7 @@ def get_algod_client_from_environment() -> AlgodClient: return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) @staticmethod - def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: + def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient: """Get a KMD client from config or environment. :param config: Optional client configuration @@ -387,7 +388,7 @@ def get_kmd_client_from_environment() -> KMDClient: return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) @staticmethod - def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: + def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> IndexerClient: """Get an Indexer client from config or environment. :param config: Optional client configuration @@ -423,7 +424,7 @@ def get_typed_app_client_by_creator_and_name( default_sender: str | None = None, default_signer: TransactionSigner | None = None, ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, + app_lookup_cache: ApplicationLookup | None = None, ) -> TypedAppClientT: """Get a typed application client by creator address and name. @@ -530,9 +531,7 @@ def get_typed_app_factory( default_sender: str | bytes | None = None, default_signer: TransactionSigner | None = None, version: str | None = None, - updatable: bool | None = None, - deletable: bool | None = None, - deploy_time_params: TealTemplateParams | None = None, + compilation_params: AppClientCompilationParams | None = None, ) -> TypedFactoryT: """Get a typed application factory. @@ -541,9 +540,7 @@ def get_typed_app_factory( :param default_sender: Optional default sender address :param default_signer: Optional default transaction signer :param version: Optional version string - :param updatable: Optional flag to make app updatable - :param deletable: Optional flag to make app deletable - :param deploy_time_params: Optional deployment parameters + :param compilation_params: Optional compilation parameters :raises ValueError: If no Algorand client is configured :return: Typed application factory instance """ @@ -556,9 +553,7 @@ def get_typed_app_factory( default_sender=default_sender, default_signer=default_signer, version=version, - updatable=updatable, - deletable=deletable, - deploy_time_params=deploy_time_params, + compilation_params=compilation_params, ) @staticmethod @@ -583,7 +578,7 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: # Include KMD config only for local networks (not mainnet/testnet) kmd_config = ( - AlgoClientConfig( + AlgoClientNetworkConfig( server=algod_config.server, token=algod_config.token, port=os.getenv("KMD_PORT", "4002") ) if not any(net in algod_server.lower() for net in ["mainnet", "testnet"]) @@ -602,7 +597,9 @@ def get_config_from_environment_or_localnet() -> AlgoClientConfigs: ) @staticmethod - def get_default_localnet_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: + def get_default_localnet_config( + config_or_port: Literal["algod", "indexer", "kmd"] | int, + ) -> AlgoClientNetworkConfig: """Get default configuration for local network services. :param config_or_port: Service name or port number @@ -614,10 +611,10 @@ def get_default_localnet_config(config_or_port: Literal["algod", "indexer", "kmd else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port] ) - return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) + return AlgoClientNetworkConfig(server=f"http://localhost:{port}", token="a" * 64) @staticmethod - def get_algod_config_from_environment() -> AlgoClientConfig: + def get_algod_config_from_environment() -> AlgoClientNetworkConfig: """Retrieve the algod configuration from environment variables. Will raise an error if ALGOD_SERVER environment variable is not set @@ -626,7 +623,7 @@ def get_algod_config_from_environment() -> AlgoClientConfig: return _get_config_from_environment("ALGOD") @staticmethod - def get_indexer_config_from_environment() -> AlgoClientConfig: + def get_indexer_config_from_environment() -> AlgoClientNetworkConfig: """Retrieve the indexer configuration from environment variables. Will raise an error if INDEXER_SERVER environment variable is not set @@ -635,7 +632,7 @@ def get_indexer_config_from_environment() -> AlgoClientConfig: return _get_config_from_environment("INDEXER") @staticmethod - def get_kmd_config_from_environment() -> AlgoClientConfig: + def get_kmd_config_from_environment() -> AlgoClientNetworkConfig: """Retrieve the kmd configuration from environment variables. :return: KMD client configuration @@ -645,7 +642,7 @@ def get_kmd_config_from_environment() -> AlgoClientConfig: @staticmethod def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"] - ) -> AlgoClientConfig: + ) -> AlgoClientNetworkConfig: """Returns the Algorand configuration to point to the free tier of the AlgoNode service. :param network: Which network to connect to - TestNet or MainNet @@ -653,7 +650,7 @@ def get_algonode_config( :return: Configuration for the specified network and service """ service_type = "api" if config == "algod" else "idx" - return AlgoClientConfig( + return AlgoClientNetworkConfig( server=f"https://{network}-{service_type}.algonode.cloud", port=443, ) diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index 2f1c46da..a6ef874e 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -2,17 +2,32 @@ import algosdk import algosdk.atomic_transaction_composer -from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.atomic_transaction_composer import AccountTransactionSigner, LogicSigTransactionSigner, TransactionSigner +from algosdk.transaction import LogicSigAccount as AlgosdkLogicSigAccount from algosdk.transaction import Multisig, MultisigTransaction -__all__ = ["DISPENSER_ACCOUNT_NAME", "Account", "MultiSigAccount", "MultisigMetadata"] +__all__ = ["DISPENSER_ACCOUNT_NAME", "MultiSigAccount", "MultisigMetadata", "SigningAccount"] DISPENSER_ACCOUNT_NAME = "DISPENSER" @dataclasses.dataclass(kw_only=True) -class Account: +class TransactionSignerAccount: + """A basic transaction signer account.""" + + address: str + signer: TransactionSigner + + def __post_init__(self) -> None: + if not isinstance(self.address, str): + raise TypeError("Address must be a string") + if not isinstance(self.signer, TransactionSigner): + raise TypeError("Signer must be a TransactionSigner instance") + + +@dataclasses.dataclass(kw_only=True) +class SigningAccount: """Holds the private key and address for an account. Provides access to the account's private key, address, public key and transaction signer. @@ -46,13 +61,13 @@ def signer(self) -> AccountTransactionSigner: return AccountTransactionSigner(self.private_key) @staticmethod - def new_account() -> "Account": + def new_account() -> "SigningAccount": """Create a new random account. :return: A new Account instance """ private_key, address = algosdk.account.generate_account() - return Account(private_key=private_key) + return SigningAccount(private_key=private_key) @dataclasses.dataclass(kw_only=True) @@ -78,12 +93,12 @@ class MultiSigAccount: """ _params: MultisigMetadata - _signing_accounts: list[Account] + _signing_accounts: list[SigningAccount] _addr: str _signer: TransactionSigner _multisig: Multisig - def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[Account]) -> None: + def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[SigningAccount]) -> None: self._params = multisig_params self._signing_accounts = signing_accounts self._multisig = Multisig(multisig_params.version, multisig_params.threshold, multisig_params.addresses) @@ -102,7 +117,7 @@ def params(self) -> MultisigMetadata: return self._params @property - def signing_accounts(self) -> list[Account]: + def signing_accounts(self) -> list[SigningAccount]: """Get the list of accounts that are present to sign. :return: The list of signing accounts @@ -139,3 +154,34 @@ def sign(self, transaction: algosdk.transaction.Transaction) -> MultisigTransact msig_txn.sign(signer.private_key) return msig_txn + + +@dataclasses.dataclass(kw_only=True) +class LogicSigAccount: + """Account wrapper that supports logic sig signing. + + Provides functionality to manage and sign transactions for a logic sig account. + """ + + _account: AlgosdkLogicSigAccount + _signer: LogicSigTransactionSigner + + def __init__(self, account: AlgosdkLogicSigAccount) -> None: + self._account = account + self._signer = LogicSigTransactionSigner(account) + + @property + def address(self) -> str: + """Get the address of the multisig account. + + :return: The multisig account address + """ + return self._account.address() + + @property + def signer(self) -> LogicSigTransactionSigner: + """Get the transaction signer for this multisig account. + + :return: The multisig transaction signer + """ + return self._signer diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py index 8cf07f3f..6b4b6226 100644 --- a/src/algokit_utils/models/network.py +++ b/src/algokit_utils/models/network.py @@ -1,13 +1,13 @@ import dataclasses __all__ = [ - "AlgoClientConfig", "AlgoClientConfigs", + "AlgoClientNetworkConfig", ] @dataclasses.dataclass -class AlgoClientConfig: +class AlgoClientNetworkConfig: """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or {py:class}`algosdk.v2client.indexer.IndexerClient`""" @@ -20,6 +20,6 @@ class AlgoClientConfig: @dataclasses.dataclass class AlgoClientConfigs: - algod_config: AlgoClientConfig - indexer_config: AlgoClientConfig | None - kmd_config: AlgoClientConfig | None + algod_config: AlgoClientNetworkConfig + indexer_config: AlgoClientNetworkConfig | None + kmd_config: AlgoClientNetworkConfig | None diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 956b9eca..68e23153 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from typing import Any, Literal, TypedDict, TypeVar import algosdk @@ -93,12 +92,9 @@ def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT: class SendParams(TypedDict, total=False): + """Parameters for sending a transaction""" + max_rounds_to_wait: int | None suppress_log: bool | None populate_app_call_resources: bool | None cover_app_call_inner_txn_fees: bool | None - - -@dataclass(kw_only=True, frozen=True) -class TransactionConfirmation: - method: str diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py index faca1df0..d77d8625 100644 --- a/src/algokit_utils/protocols/__init__.py +++ b/src/algokit_utils/protocols/__init__.py @@ -1 +1,2 @@ +from algokit_utils.protocols.account import * # noqa: F403 from algokit_utils.protocols.typed_clients import * # noqa: F403 diff --git a/src/algokit_utils/protocols/account.py b/src/algokit_utils/protocols/account.py new file mode 100644 index 00000000..b50c94a3 --- /dev/null +++ b/src/algokit_utils/protocols/account.py @@ -0,0 +1,22 @@ +from typing import Protocol, runtime_checkable + +from algosdk.atomic_transaction_composer import TransactionSigner + +__all__ = ["TransactionSignerAccountProtocol"] + + +@runtime_checkable +class TransactionSignerAccountProtocol(Protocol): + """An account that has a transaction signer. + Implemented by SigningAccount, LogicSigAccount, MultiSigAccount and TransactionSignerAccount abstractions. + """ + + @property + def address(self) -> str: + """The address of the account.""" + ... + + @property + def signer(self) -> TransactionSigner: + """The transaction signer for the account.""" + ... diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py index af26e8a9..70eee8a9 100644 --- a/src/algokit_utils/protocols/typed_clients.py +++ b/src/algokit_utils/protocols/typed_clients.py @@ -1,23 +1,26 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.source_map import SourceMap from typing_extensions import Self -from algokit_utils.applications.app_client import ( - AppClientBareCallCreateParams, - AppClientBareCallParams, - BaseAppClientMethodCallParams, -) -from algokit_utils.applications.app_deployer import ( - AppLookup, - OnSchemaBreak, - OnUpdate, -) -from algokit_utils.models.state import TealTemplateParams +from algokit_utils.models import SendParams if TYPE_CHECKING: from algokit_utils.algorand import AlgorandClient + from algokit_utils.applications.app_client import ( + AppClientBareCallCreateParams, + AppClientBareCallParams, + AppClientCompilationParams, + BaseAppClientMethodCallParams, + ) + from algokit_utils.applications.app_deployer import ( + ApplicationLookup, + OnSchemaBreak, + OnUpdate, + ) from algokit_utils.applications.app_factory import AppFactoryDeployResult __all__ = [ @@ -36,8 +39,8 @@ def from_creator_and_name( default_sender: str | None = None, default_signer: TransactionSigner | None = None, ignore_cache: bool | None = None, - app_lookup_cache: AppLookup | None = None, - algorand: "AlgorandClient", + app_lookup_cache: ApplicationLookup | None = None, + algorand: AlgorandClient, ) -> Self: ... @classmethod @@ -49,7 +52,7 @@ def from_network( default_signer: TransactionSigner | None = None, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, - algorand: "AlgorandClient", + algorand: AlgorandClient, ) -> Self: ... def __init__( @@ -59,7 +62,7 @@ def __init__( app_name: str | None = None, default_sender: str | None = None, default_signer: TransactionSigner | None = None, - algorand: "AlgorandClient", + algorand: AlgorandClient, approval_source_map: SourceMap | None = None, clear_source_map: SourceMap | None = None, ) -> None: ... @@ -67,17 +70,17 @@ def __init__( CreateParamsT = TypeVar( # noqa: PLC0105 "CreateParamsT", - bound=BaseAppClientMethodCallParams | AppClientBareCallCreateParams | None, + bound="BaseAppClientMethodCallParams | AppClientBareCallCreateParams | None", contravariant=True, ) UpdateParamsT = TypeVar( # noqa: PLC0105 "UpdateParamsT", - bound=BaseAppClientMethodCallParams | AppClientBareCallParams | None, + bound="BaseAppClientMethodCallParams | AppClientBareCallParams | None", contravariant=True, ) DeleteParamsT = TypeVar( # noqa: PLC0105 "DeleteParamsT", - bound=BaseAppClientMethodCallParams | AppClientBareCallParams | None, + bound="BaseAppClientMethodCallParams | AppClientBareCallParams | None", contravariant=True, ) @@ -85,26 +88,21 @@ def __init__( class TypedAppFactoryProtocol(Protocol, Generic[CreateParamsT, UpdateParamsT, DeleteParamsT]): def __init__( self, - algorand: "AlgorandClient", + algorand: AlgorandClient, **kwargs: Any, ) -> None: ... - def deploy( # noqa: PLR0913 + def deploy( self, *, - deploy_time_params: TealTemplateParams | None = None, - on_update: OnUpdate = OnUpdate.Fail, - on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + on_update: OnUpdate | None = None, + on_schema_break: OnSchemaBreak | None = None, create_params: CreateParamsT | None = None, update_params: UpdateParamsT | None = None, delete_params: DeleteParamsT | None = None, - existing_deployments: AppLookup | None = None, + existing_deployments: ApplicationLookup | None = None, ignore_cache: bool = False, - updatable: bool | None = None, - deletable: bool | None = None, app_name: str | None = None, - max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, - ) -> tuple[TypedAppClientProtocol, "AppFactoryDeployResult"]: ... + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> tuple[TypedAppClientProtocol, AppFactoryDeployResult]: ... diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 9d096441..2ae654cb 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -23,12 +23,13 @@ from algosdk.v2client.models.simulate_request import SimulateRequest from typing_extensions import deprecated -from algokit_utils.applications.abi import ABIReturn +from algokit_utils.applications.abi import ABIReturn, ABIValue from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method from algokit_utils.config import config from algokit_utils.models.state import BoxIdentifier, BoxReference from algokit_utils.models.transaction import SendParams, TransactionWrapper +from algokit_utils.protocols.account import TransactionSignerAccountProtocol if TYPE_CHECKING: from collections.abc import Callable @@ -37,7 +38,6 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.models import SimulateTraceConfig - from algokit_utils.applications.abi import ABIValue from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.transaction import Arc2TransactionNote @@ -83,7 +83,7 @@ @dataclass(kw_only=True, frozen=True) class _CommonTxnParams: sender: str - signer: TransactionSigner | None = None + signer: TransactionSigner | TransactionSignerAccountProtocol | None = None rekey_to: str | None = None note: bytes | None = None lease: bytes | None = None @@ -1880,10 +1880,15 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 if isinstance(arg, algosdk.transaction.Transaction): # Wrap in TransactionWithSigner + signer = ( + params.signer.signer + if isinstance(params.signer, TransactionSignerAccountProtocol) + else params.signer + ) method_args.append( TransactionWithSignerAndContext( txn=arg, - signer=params.signer if params.signer is not None else self._get_signer(params.sender), + signer=signer if signer is not None else self._get_signer(params.sender), context=TransactionContext(abi_method=None), ) ) @@ -1924,10 +1929,15 @@ def _build_method_call( # noqa: C901, PLR0912, PLR0915 case _: raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + signer = ( + params.signer.signer + if isinstance(params.signer, TransactionSignerAccountProtocol) + else params.signer + ) method_args.append( TransactionWithSignerAndContext( txn=txn.txn, - signer=params.signer or self._get_signer(params.sender), + signer=signer or self._get_signer(params.sender), context=TransactionContext(abi_method=params.method), ) ) @@ -2238,7 +2248,8 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 ): return self._build_method_call(txn, suggested_params) - signer = txn.signer or self._get_signer(txn.sender) + signer = txn.signer.signer if isinstance(txn.signer, TransactionSignerAccountProtocol) else txn.signer # type: ignore[assignment] + signer = signer or self._get_signer(txn.sender) match txn: case PaymentParams(): diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py index b14e0e70..3f4b55e0 100644 --- a/tests/accounts/test_account_manager.py +++ b/tests/accounts/test_account_manager.py @@ -1,7 +1,7 @@ import algosdk import pytest -from algokit_utils import Account +from algokit_utils import SigningAccount from algokit_utils.algorand import AlgorandClient from algokit_utils.models.amount import AlgoAmount from tests.conftest import get_unique_name @@ -13,7 +13,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index ad78719b..6aa6e8e5 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -20,7 +20,7 @@ from algokit_utils.applications.app_manager import AppManager from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Network from algokit_utils.errors.logic_error import LogicError -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.models.state import BoxReference from algokit_utils.transactions.transaction_composer import AppCreateParams, PaymentParams @@ -32,7 +32,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -56,7 +56,7 @@ def hello_world_arc32_app_spec() -> ApplicationSpecification: @pytest.fixture def hello_world_arc32_app_id( - algorand: AlgorandClient, funded_account: Account, hello_world_arc32_app_spec: ApplicationSpecification + algorand: AlgorandClient, funded_account: SigningAccount, hello_world_arc32_app_spec: ApplicationSpecification ) -> int: global_schema = hello_world_arc32_app_spec.global_state_schema local_schema = hello_world_arc32_app_spec.local_state_schema @@ -90,7 +90,7 @@ def testing_app_arc32_app_spec() -> ApplicationSpecification: @pytest.fixture def testing_app_arc32_app_id( - algorand: AlgorandClient, funded_account: Account, testing_app_arc32_app_spec: ApplicationSpecification + algorand: AlgorandClient, funded_account: SigningAccount, testing_app_arc32_app_spec: ApplicationSpecification ) -> int: global_schema = testing_app_arc32_app_spec.global_state_schema local_schema = testing_app_arc32_app_spec.local_state_schema @@ -121,7 +121,7 @@ def testing_app_arc32_app_id( @pytest.fixture def test_app_client( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, testing_app_arc32_app_spec: ApplicationSpecification, testing_app_arc32_app_id: int, ) -> AppClient: @@ -139,7 +139,7 @@ def test_app_client( @pytest.fixture def test_app_client_with_sourcemaps( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, testing_app_arc32_app_spec: ApplicationSpecification, testing_app_arc32_app_id: int, ) -> AppClient: @@ -167,7 +167,7 @@ def testing_app_puya_arc32_app_spec() -> ApplicationSpecification: @pytest.fixture def testing_app_puya_arc32_app_id( - algorand: AlgorandClient, funded_account: Account, testing_app_puya_arc32_app_spec: ApplicationSpecification + algorand: AlgorandClient, funded_account: SigningAccount, testing_app_puya_arc32_app_spec: ApplicationSpecification ) -> int: global_schema = testing_app_puya_arc32_app_spec.global_state_schema local_schema = testing_app_puya_arc32_app_spec.local_state_schema @@ -191,7 +191,7 @@ def testing_app_puya_arc32_app_id( @pytest.fixture def test_app_client_puya( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, testing_app_puya_arc32_app_spec: ApplicationSpecification, testing_app_puya_arc32_app_id: int, ) -> AppClient: @@ -208,7 +208,7 @@ def test_app_client_puya( def test_clone_overriding_default_sender_and_inheriting_app_name( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, hello_world_arc32_app_spec: ApplicationSpecification, hello_world_arc32_app_id: int, ) -> None: @@ -234,7 +234,7 @@ def test_clone_overriding_default_sender_and_inheriting_app_name( def test_clone_overriding_app_name( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, hello_world_arc32_app_spec: ApplicationSpecification, hello_world_arc32_app_id: int, ) -> None: @@ -260,7 +260,7 @@ def test_clone_overriding_app_name( def test_clone_inheriting_app_name_based_on_default_handling( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, hello_world_arc32_app_spec: ApplicationSpecification, hello_world_arc32_app_id: int, ) -> None: @@ -331,7 +331,7 @@ def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: def test_construct_transaction_with_abi_encoding_including_transaction( - algorand: AlgorandClient, funded_account: Account, test_app_client: AppClient + algorand: AlgorandClient, funded_account: SigningAccount, test_app_client: AppClient ) -> None: # Create a payment transaction with random amount amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) @@ -363,7 +363,7 @@ def test_construct_transaction_with_abi_encoding_including_transaction( def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( - algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account + algorand: AlgorandClient, test_app_client: AppClient, funded_account: SigningAccount ) -> None: # Create a payment transaction with a random amount amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) @@ -398,7 +398,7 @@ def sign_transactions( def test_sign_transaction_in_group_with_different_signer_if_provided( - algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account + algorand: AlgorandClient, test_app_client: AppClient, funded_account: SigningAccount ) -> None: # Generate a new account test_account = algorand.account.random() @@ -428,7 +428,7 @@ def test_sign_transaction_in_group_with_different_signer_if_provided( def test_construct_transaction_with_abi_encoding_including_foreign_references_not_in_signature( - algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account + algorand: AlgorandClient, test_app_client: AppClient, funded_account: SigningAccount ) -> None: test_account = algorand.account.random() algorand.account.ensure_funded( @@ -458,7 +458,7 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no assert expected_return.value == result.abi_return -def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: +def test_retrieve_state(test_app_client: AppClient, funded_account: SigningAccount) -> None: # Test global state test_app_client.send.call(AppClientMethodCallParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])])) global_state = test_app_client.get_global_state() @@ -668,7 +668,7 @@ def test_box_methods_with_arc4_returns_parametrized( def test_abi_with_default_arg_method( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, testing_app_arc32_app_id: int, testing_app_arc32_app_spec: ApplicationSpecification, ) -> None: diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 63290598..5a783fef 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -19,7 +19,7 @@ AppFactoryCreateParams, ) from algokit_utils.errors import LogicError -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import PaymentParams @@ -30,7 +30,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -46,7 +46,7 @@ def app_spec() -> str: @pytest.fixture -def factory(algorand: AlgorandClient, funded_account: Account, app_spec: str) -> AppFactory: +def factory(algorand: AlgorandClient, funded_account: SigningAccount, app_spec: str) -> AppFactory: """Create AppFactory fixture""" return algorand.client.get_app_factory(app_spec=app_spec, default_sender=funded_account.address) @@ -54,7 +54,7 @@ def factory(algorand: AlgorandClient, funded_account: Account, app_spec: str) -> @pytest.fixture def arc56_factory( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, ) -> AppFactory: """Create AppFactory fixture""" arc56_raw_spec = ( @@ -99,11 +99,13 @@ def test_create_app_with_constructor_deploy_time_params(algorand: AlgorandClient factory = algorand.client.get_app_factory( app_spec=app_spec, default_sender=random_account.address, - deploy_time_params={ - # It should strip off the TMPL_ - "TMPL_UPDATABLE": 0, - "DELETABLE": 0, - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + } }, ) @@ -137,20 +139,24 @@ def test_create_app_with_oncomplete_overload(factory: AppFactory) -> None: def test_deploy_when_immutable_and_permanent(factory: AppFactory) -> None: factory.deploy( - deletable=False, - updatable=False, on_schema_break=OnSchemaBreak.Fail, on_update=OnUpdate.Fail, - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deletable": False, + "updatable": False, + "deploy_time_params": { + "VALUE": 1, + }, }, ) def test_deploy_app_create(factory: AppFactory) -> None: app_client, deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, }, ) @@ -163,8 +169,10 @@ def test_deploy_app_create(factory: AppFactory) -> None: def test_deploy_app_create_abi(factory: AppFactory) -> None: app_client, deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, }, create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), ) @@ -180,17 +188,21 @@ def test_deploy_app_create_abi(factory: AppFactory) -> None: def test_deploy_app_update(factory: AppFactory) -> None: app_client, create_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "updatable": True, }, - updatable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create assert create_deploy_result.create_result updated_app_client, update_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 2, + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, }, on_update=OnUpdate.UpdateApp, ) @@ -211,18 +223,22 @@ def test_deploy_app_update(factory: AppFactory) -> None: def test_deploy_app_update_abi(factory: AppFactory) -> None: _, create_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "updatable": True, }, - updatable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create assert create_deploy_result.create_result created_app = create_deploy_result.create_result _, update_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 2, + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, }, on_update=OnUpdate.UpdateApp, update_params=AppClientMethodCallParams(method="update_abi", args=["args_io"]), @@ -245,17 +261,21 @@ def test_deploy_app_update_abi(factory: AppFactory) -> None: def test_deploy_app_replace(factory: AppFactory) -> None: _, create_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "deletable": True, }, - deletable=True, ) assert create_deploy_result.operation_performed == OperationPerformed.Create assert create_deploy_result.create_result _, replace_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 2, + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, }, on_update=OnUpdate.ReplaceApp, ) @@ -281,16 +301,23 @@ def test_deploy_app_replace(factory: AppFactory) -> None: def test_deploy_app_replace_abi(factory: AppFactory) -> None: _, create_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 1, + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "deletable": True, + }, + send_params={ + "populate_app_call_resources": False, }, - deletable=True, - populate_app_call_resources=False, ) replaced_app_client, replace_deploy_result = factory.deploy( - deploy_time_params={ - "VALUE": 2, + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, + "deletable": True, }, on_update=OnUpdate.ReplaceApp, create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), @@ -331,7 +358,7 @@ def test_create_then_call_app(factory: AppFactory) -> None: assert call.abi_return == "Hello, test" -def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, factory: AppFactory) -> None: +def test_call_app_with_rekey(funded_account: SigningAccount, algorand: AlgorandClient, factory: AppFactory) -> None: rekey_to = algorand.account.random() app_client, _ = factory.send.bare.create( @@ -347,7 +374,7 @@ def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, app_client.send.opt_in(AppClientMethodCallParams(method="opt_in", rekey_to=rekey_to.address)) # If the rekey didn't work this will throw - rekeyed_account = algorand.account.rekeyed(funded_account.address, rekey_to) + rekeyed_account = algorand.account.rekeyed(sender=funded_account.address, account=rekey_to) algorand.send.payment( PaymentParams(amount=AlgoAmount.from_algo(0), sender=rekeyed_account.address, receiver=funded_account.address) ) @@ -422,10 +449,10 @@ def test_delete_app_with_abi(factory: AppFactory) -> None: def test_export_import_sourcemaps( factory: AppFactory, algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, ) -> None: # Export source maps from original client - app_client, _ = factory.deploy(deploy_time_params={"VALUE": 1}) + app_client, _ = factory.deploy(compilation_params={"deploy_time_params": {"VALUE": 1}}) old_sourcemaps = app_client.export_source_maps() # Create new client instance @@ -467,11 +494,13 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( ) -> None: app_client, _ = arc56_factory.deploy( create_params=AppClientMethodCallCreateParams(method="createApplication"), - deploy_time_params={ - "bytes64TmplVar": "0" * 64, - "uint64TmplVar": 123, - "bytes32TmplVar": "0" * 32, - "bytesTmplVar": "foo", + compilation_params={ + "deploy_time_params": { + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 123, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, }, ) @@ -482,16 +511,18 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( arc56_factory: AppFactory, algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, ) -> None: # Deploy app with template parameters app_client, _ = arc56_factory.deploy( create_params=AppClientMethodCallCreateParams(method="createApplication"), - deploy_time_params={ - "bytes64TmplVar": "0" * 64, - "uint64TmplVar": 0, - "bytes32TmplVar": "0" * 32, - "bytesTmplVar": "foo", + compilation_params={ + "deploy_time_params": { + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 0, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, }, ) app_id = app_client.app_id diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index e0610466..660c7039 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -2,7 +2,7 @@ from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_manager import AppManager -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from tests.conftest import check_output_stability @@ -13,7 +13,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index a0e41a44..2c1987cb 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -1,7 +1,7 @@ import pytest from algosdk.atomic_transaction_composer import AccountTransactionSigner -from algokit_utils import Account +from algokit_utils import SigningAccount from algokit_utils.algorand import AlgorandClient from algokit_utils.assets.asset_manager import ( AccountAssetInformation, @@ -21,7 +21,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def sender(algorand: AlgorandClient) -> Account: +def sender(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -32,7 +32,7 @@ def sender(algorand: AlgorandClient) -> Account: @pytest.fixture -def receiver(algorand: AlgorandClient) -> Account: +def receiver(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -41,7 +41,7 @@ def receiver(algorand: AlgorandClient) -> Account: return new_account -def test_get_by_id(algorand: AlgorandClient, sender: Account) -> None: +def test_get_by_id(algorand: AlgorandClient, sender: SigningAccount) -> None: # First create an asset total = 1000 create_result = algorand.send.asset_create( @@ -71,7 +71,7 @@ def test_get_by_id(algorand: AlgorandClient, sender: Account) -> None: assert asset_info.creator == sender.address -def test_get_account_information_with_address(algorand: AlgorandClient, sender: Account) -> None: +def test_get_account_information_with_address(algorand: AlgorandClient, sender: SigningAccount) -> None: # First create an asset total = 1000 create_result = algorand.send.asset_create( @@ -96,7 +96,7 @@ def test_get_account_information_with_address(algorand: AlgorandClient, sender: assert account_info.frozen is False -def test_get_account_information_with_account(algorand: AlgorandClient, sender: Account) -> None: +def test_get_account_information_with_account(algorand: AlgorandClient, sender: SigningAccount) -> None: # First create an asset total = 1000 create_result = algorand.send.asset_create( @@ -121,7 +121,7 @@ def test_get_account_information_with_account(algorand: AlgorandClient, sender: assert account_info.frozen is False -def test_get_account_information_with_transaction_signer(algorand: AlgorandClient, sender: Account) -> None: +def test_get_account_information_with_transaction_signer(algorand: AlgorandClient, sender: SigningAccount) -> None: # First create an asset total = 1000 create_result = algorand.send.asset_create( @@ -147,7 +147,7 @@ def test_get_account_information_with_transaction_signer(algorand: AlgorandClien assert account_info.frozen is False -def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: Account, receiver: Account) -> None: +def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: SigningAccount, receiver: SigningAccount) -> None: # First create some assets asset_ids = [] for i in range(3): @@ -185,7 +185,9 @@ def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: Account, rec assert result.transaction_id -def test_bulk_opt_out_not_opted_in_fails(algorand: AlgorandClient, sender: Account, receiver: Account) -> None: +def test_bulk_opt_out_not_opted_in_fails( + algorand: AlgorandClient, sender: SigningAccount, receiver: SigningAccount +) -> None: # First create an asset create_result = algorand.send.asset_create( AssetCreateParams( diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py index 09b2c5ed..40f11e09 100644 --- a/tests/clients/algorand_client/test_transfer.py +++ b/tests/clients/algorand_client/test_transfer.py @@ -4,7 +4,7 @@ from algokit_utils.algorand import AlgorandClient from algokit_utils.clients.dispenser_api_client import DispenserApiConfig, TestNetDispenserApiClient -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AssetOptInParams, @@ -20,7 +20,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -30,7 +30,7 @@ def funded_account(algorand: AlgorandClient) -> Account: return new_account -def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_account: SigningAccount) -> None: second_account = algorand.account.random() result = algorand.send.payment( @@ -51,7 +51,7 @@ def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_a assert account_info.amount == 5_000_000 -def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_account: SigningAccount) -> None: second_account = algorand.account.random() algorand.send.payment( @@ -74,7 +74,7 @@ def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_ac ) -def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funded_account: SigningAccount) -> None: second_account = algorand.account.random() algorand.send.payment( @@ -97,7 +97,7 @@ def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funde ) -def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: SigningAccount) -> None: test_asset_id = generate_test_asset(algorand, funded_account, 100) second_account = algorand.account.random() @@ -139,7 +139,7 @@ def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: A def test_transfer_asa_receiver_not_opted_in( algorand: AlgorandClient, - funded_account: Account, + funded_account: SigningAccount, ) -> None: test_asset_id = generate_test_asset(algorand, funded_account, 100) second_account = algorand.account.random() @@ -156,7 +156,7 @@ def test_transfer_asa_receiver_not_opted_in( ) -def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_account: SigningAccount) -> None: test_asset_id = generate_test_asset(algorand, funded_account, 100) second_account = algorand.account.random() algorand.account.ensure_funded( @@ -178,7 +178,7 @@ def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_accou ) -def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_account: SigningAccount) -> None: second_account = algorand.account.random() algorand.account.ensure_funded( account_to_fund=second_account, @@ -199,7 +199,7 @@ def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_accoun ) -def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: test_asset_id = generate_test_asset(algorand, funded_account, 100) second_account = algorand.account.random() algorand.account.ensure_funded( @@ -236,7 +236,7 @@ def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_accoun assert test_account_info.balance == 95 -def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_account: SigningAccount) -> None: test_asset_id = generate_test_asset(algorand, funded_account, 100) second_account = algorand.account.random() clawback_account = algorand.account.random() @@ -307,7 +307,7 @@ def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_ac ) # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance -def test_ensure_funded(algorand: AlgorandClient, funded_account: Account) -> None: +def test_ensure_funded(algorand: AlgorandClient, funded_account: SigningAccount) -> None: test_account = algorand.account.random() response = algorand.account.ensure_funded( account_to_fund=test_account, @@ -340,7 +340,9 @@ def test_ensure_funded_uses_dispenser_by_default( assert account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) -def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClient, funded_account: Account) -> None: +def test_ensure_funded_respects_minimum_funding_increment( + algorand: AlgorandClient, funded_account: SigningAccount +) -> None: test_account = algorand.account.random() response = algorand.account.ensure_funded( account_to_fund=test_account, @@ -410,10 +412,10 @@ def test_ensure_funded_testnet_api_bad_response(monkeypatch: pytest.MonkeyPatch, ) -def test_rekey_works(algorand: AlgorandClient, funded_account: Account) -> None: +def test_rekey_works(algorand: AlgorandClient, funded_account: SigningAccount) -> None: second_account = algorand.account.random() - algorand.account.rekey_account(funded_account, second_account, note=b"rekey") + algorand.account.rekey_account(funded_account.address, second_account, note=b"rekey") # This will throw if the rekey wasn't successful algorand.send.payment( diff --git a/tests/conftest.py b/tests/conftest.py index dfd25858..fab07acf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,11 +10,9 @@ from dotenv import load_dotenv from algokit_utils import ( - Account, ApplicationClient, ApplicationSpecification, - EnsureBalanceParameters, - ensure_funded, + SigningAccount, replace_template_variables, ) from algokit_utils.algorand import AlgorandClient @@ -22,7 +20,7 @@ from algokit_utils.transactions.transaction_composer import AssetCreateParams if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient + pass @pytest.fixture(autouse=True, scope="session") @@ -120,7 +118,7 @@ def is_opted_in(client_fixture: ApplicationClient) -> bool: return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) -def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None) -> int: +def generate_test_asset(algorand: AlgorandClient, sender: SigningAccount, total: int | None) -> int: if total is None: total = math.floor(random.random() * 100) + 20 @@ -144,14 +142,3 @@ def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | ) return int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] - - -def assure_funds(algod_client: "AlgodClient", account: Account) -> None: - ensure_funded( - algod_client, - EnsureBalanceParameters( - account_to_fund=account, - min_spending_balance_micro_algos=300000, - min_funding_increment_micro_algos=1, - ), - ) diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index 1d9f0285..bdb0d5b3 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -25,7 +25,7 @@ from algokit_utils.applications import AppFactoryCreateMethodCallParams from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams from algokit_utils.common import Program -from algokit_utils.models import Account +from algokit_utils.models import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, @@ -41,7 +41,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -55,7 +55,7 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture -def client_fixture(algorand: AlgorandClient, funded_account: Account) -> AppClient: +def client_fixture(algorand: AlgorandClient, funded_account: SigningAccount) -> AppClient: app_spec = (Path(__file__).parent / "artifacts" / "legacy_app_client_test" / "app_client_test.json").read_text() app_factory = algorand.client.get_app_factory( app_spec=app_spec, default_sender=funded_account.address, default_signer=funded_account.signer @@ -167,7 +167,10 @@ def test_simulate_and_persist_response_via_app_call( def test_simulate_and_persist_response( - tmp_path_factory: pytest.TempPathFactory, algorand: AlgorandClient, mock_config: Mock, funded_account: Account + tmp_path_factory: pytest.TempPathFactory, + algorand: AlgorandClient, + mock_config: Mock, + funded_account: SigningAccount, ) -> None: mock_config.debug = True mock_config.trace_all = True @@ -222,7 +225,7 @@ def test_simulate_response_filename_generation( expected_filename_part: str, tmp_path_factory: pytest.TempPathFactory, client_fixture: AppClient, - funded_account: Account, + funded_account: SigningAccount, monkeypatch: pytest.MonkeyPatch, mock_config: Mock, ) -> None: diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index f3259fee..5413a4ed 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -8,7 +8,7 @@ from algosdk.atomic_transaction_composer import TransactionWithSigner from algosdk.transaction import OnComplete, PaymentTxn -from algokit_utils import Account +from algokit_utils import SigningAccount from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams, FundAppAccountParams from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateParams @@ -24,7 +24,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algos(100)) @@ -43,7 +43,7 @@ class BaseResourcePackerTest: version: int @pytest.fixture(autouse=True) - def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: config.configure(populate_app_call_resources=True) # Create app based on version @@ -63,7 +63,7 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ config.configure(populate_app_call_resources=False) @pytest.fixture - def external_client(self, algorand: AlgorandClient, funded_account: Account) -> AppClient: + def external_client(self, algorand: AlgorandClient, funded_account: SigningAccount) -> AppClient: external_spec = ( Path(__file__).parent.parent / "artifacts" / "resource-packer" / "ExternalApp.arc32.json" ).read_text() @@ -173,7 +173,7 @@ def test_assets_valid_asset(self) -> None: }, ) - def test_cross_product_reference_has_asset(self, funded_account: Account) -> None: + def test_cross_product_reference_has_asset(self, funded_account: SigningAccount) -> None: self.app_client.send.call( AppClientMethodCallParams( method="hasAsset", @@ -184,7 +184,7 @@ def test_cross_product_reference_has_asset(self, funded_account: Account) -> Non }, ) - def test_cross_product_reference_invalid_external_local(self, funded_account: Account) -> None: + def test_cross_product_reference_invalid_external_local(self, funded_account: SigningAccount) -> None: with pytest.raises(LogicError, match="unavailable App"): self.app_client.send.call( AppClientMethodCallParams( @@ -197,7 +197,7 @@ def test_cross_product_reference_invalid_external_local(self, funded_account: Ac ) def test_cross_product_reference_external_local( - self, external_client: AppClient, funded_account: Account, algorand: AlgorandClient + self, external_client: AppClient, funded_account: SigningAccount, algorand: AlgorandClient ) -> None: algorand.send.app_call_method_call( external_client.params.opt_in( @@ -252,7 +252,7 @@ def test_address_balance( }, ) - def test_cross_product_reference_invalid_has_asset(self, funded_account: Account) -> None: + def test_cross_product_reference_invalid_has_asset(self, funded_account: SigningAccount) -> None: with pytest.raises(LogicError, match="unavailable Asset"): self.app_client.send.call( AppClientMethodCallParams( @@ -281,7 +281,7 @@ class TestResourcePackerMixed: """Test resource packing with mixed AVM versions""" @pytest.fixture(autouse=True) - def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: config.configure(populate_app_call_resources=True) # Create v8 app @@ -304,9 +304,9 @@ def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[ config.configure(populate_app_call_resources=False) - def test_same_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + def test_same_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: rekeyed_to = algorand.account.random() - algorand.account.rekey_account(funded_account, rekeyed_to) + algorand.account.rekey_account(funded_account.address, rekeyed_to) random_account = algorand.account.random() @@ -342,7 +342,7 @@ def test_same_account(self, algorand: AlgorandClient, funded_account: Account) - v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] assert len(v8_accounts) + len(v9_accounts) == 1 - def test_app_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + def test_app_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(328500))) self.v8_client.send.call( AppClientMethodCallParams( @@ -392,7 +392,7 @@ class TestResourcePackerMeta: """Test meta aspects of resource packing""" @pytest.fixture(autouse=True) - def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: config.configure(populate_app_call_resources=True) external_spec = ( @@ -422,7 +422,7 @@ def test_error_during_simulate(self) -> None: ) assert "Error during resource population simulation in transaction 0" in exc_info.value.logic_error_str - def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: Account) -> None: + def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: payment = PaymentTxn( sender=funded_account.address, receiver=funded_account.address, @@ -459,9 +459,9 @@ def test_sender_asset_holding(self) -> None: assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 - def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: Account) -> None: + def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: auth_addr = algorand.account.random() - algorand.account.rekey_account(funded_account, auth_addr) + algorand.account.rekey_account(funded_account.address, auth_addr) self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_001))) @@ -483,7 +483,7 @@ class TestCoverAppCallInnerFees: """Test covering app call inner transaction fees""" @pytest.fixture(autouse=True) - def setup(self, algorand: AlgorandClient, funded_account: Account) -> Generator[None, None, None]: + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: config.configure(populate_app_call_resources=True) # Load inner fee contract spec @@ -789,7 +789,7 @@ def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) - def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: Account) -> None: + def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: SigningAccount) -> None: """Test that fee is not altered when another transaction in group covers inner fees""" expected_fee = 8000 @@ -821,7 +821,7 @@ def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: A # and is probably unlikely to be a common use case assert result.transactions[1].raw.fee == 1000 - def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: Account) -> None: + def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: SigningAccount) -> None: """Test that surplus fees are allocated to the most fee constrained transaction first""" result = ( @@ -858,7 +858,7 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: assert result.transactions[1].raw.fee == 7500 assert result.transactions[2].raw.fee == 0 - def test_handles_nested_abi_method_calls(self, funded_account: Account) -> None: + def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) -> None: """Test fee handling with nested ABI method calls""" # Create nested contract app @@ -942,7 +942,7 @@ def test_throws_when_max_fee_below_calculated(self) -> None: .send({"cover_app_call_inner_txn_fees": True}) ) - def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Account) -> None: + def test_throws_when_nested_max_fee_below_calculated(self, funded_account: SigningAccount) -> None: """Test that error is thrown when nested max fee is below calculated fee""" # Create nested contract app @@ -1016,7 +1016,7 @@ def test_throws_when_static_fee_below_calculated(self) -> None: .send({"cover_app_call_inner_txn_fees": True}) ) - def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Account) -> None: + def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: SigningAccount) -> None: """Test that error is thrown when static fee for non-app-call transaction is too low""" with pytest.raises( diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 20b4442f..473b6b73 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -15,7 +15,7 @@ from algokit_utils._legacy_v2.account import get_account from algokit_utils.algorand import AlgorandClient -from algokit_utils.models.account import Account +from algokit_utils.models.account import MultisigMetadata, SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, @@ -46,7 +46,7 @@ def mock_config() -> Generator[Mock, None, None]: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -57,12 +57,12 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture -def funded_secondary_account(algorand: AlgorandClient) -> Account: +def funded_secondary_account(algorand: AlgorandClient) -> SigningAccount: secondary_name = get_unique_name() return get_account(algorand.client.algod, secondary_name) -def test_add_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_add_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -83,7 +83,7 @@ def test_add_transaction(algorand: AlgorandClient, funded_account: Account) -> N assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos -def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> None: +def test_add_asset_create(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -119,7 +119,9 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> assert txn.asset_name == created_asset["name"] == "Test Asset" -def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account) -> None: +def test_add_asset_config( + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount +) -> None: # First create an asset asset_txn = AssetCreateTxn( sender=funded_account.address, @@ -162,7 +164,7 @@ def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, fun assert updated_asset["manager"] == funded_secondary_account.address -def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> None: +def test_add_app_create(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -187,7 +189,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No composer.send({"max_rounds_to_wait": 20}) -def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Account) -> None: +def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -227,7 +229,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco assert response.returns[-1].value == "Hello, world" -def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: +def test_simulate(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -244,7 +246,7 @@ def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: assert simulate_response -def test_send(algorand: AlgorandClient, funded_account: Account) -> None: +def test_send(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -307,7 +309,7 @@ def test_arc2_note_valid_dapp_names() -> None: def _get_test_transaction( - default_account: Account, amount: AlgoAmount | None = None, sender: Account | None = None + default_account: SigningAccount, amount: AlgoAmount | None = None, sender: SigningAccount | None = None ) -> dict[str, Any]: return { "sender": sender.address if sender else default_account.address, @@ -316,14 +318,16 @@ def _get_test_transaction( } -def test_transaction_is_capped_by_low_min_txn_fee(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transaction_is_capped_by_low_min_txn_fee(algorand: AlgorandClient, funded_account: SigningAccount) -> None: with pytest.raises(ValueError, match="Transaction fee 1000 is greater than max_fee 1 µALGO"): algorand.send.payment( PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1)) ) -def test_transaction_cap_is_ignored_if_higher_than_fee(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transaction_cap_is_ignored_if_higher_than_fee( + algorand: AlgorandClient, funded_account: SigningAccount +) -> None: response = algorand.send.payment( PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1_000_000)) ) @@ -331,7 +335,7 @@ def test_transaction_cap_is_ignored_if_higher_than_fee(algorand: AlgorandClient, assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_micro_algo(1000) -def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account: SigningAccount) -> None: response = algorand.send.payment( PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algos(1)) ) @@ -339,7 +343,7 @@ def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algos(1) -def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: SigningAccount) -> None: composer = TransactionComposer( algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, @@ -367,9 +371,14 @@ def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: Acc ) -def test_multisig_single_account(algorand: AlgorandClient, funded_account: Account) -> None: - multisig = algorand.account.multi_sig( - version=1, threshold=1, addrs=[funded_account.address], signing_accounts=[funded_account] +def test_multisig_single_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + multisig = algorand.account.multisig( + metadata=MultisigMetadata( + version=1, + threshold=1, + addresses=[funded_account.address], + ), + signing_accounts=[funded_account], ) algorand.send.payment( PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) @@ -379,15 +388,17 @@ def test_multisig_single_account(algorand: AlgorandClient, funded_account: Accou ) -def test_multisig_double_account(algorand: AlgorandClient, funded_account: Account) -> None: +def test_multisig_double_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: account2 = algorand.account.random() algorand.account.ensure_funded(account2, funded_account, AlgoAmount.from_algos(10)) # Setup multisig - multisig = algorand.account.multi_sig( - version=1, - threshold=2, - addrs=[funded_account.address, account2.address], + multisig = algorand.account.multisig( + metadata=MultisigMetadata( + version=1, + threshold=2, + addresses=[funded_account.address, account2.address], + ), signing_accounts=[funded_account, account2], ) @@ -403,7 +414,7 @@ def test_multisig_double_account(algorand: AlgorandClient, funded_account: Accou @pytest.mark.usefixtures("mock_config") -def test_transactions_fails_in_debug_mode(algorand: AlgorandClient, funded_account: Account) -> None: +def test_transactions_fails_in_debug_mode(algorand: AlgorandClient, funded_account: SigningAccount) -> None: txn1 = algorand.create_transaction.payment(PaymentParams(**_get_test_transaction(funded_account))) txn2 = algorand.create_transaction.payment( PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_micro_algo(9999999999999))) diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index f68ccddf..e9825a53 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -15,7 +15,7 @@ from algokit_utils._legacy_v2.account import get_account from algokit_utils.algorand import AlgorandClient -from algokit_utils.models.account import Account +from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( AppCallMethodCallParams, @@ -39,7 +39,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -50,7 +50,7 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture -def funded_secondary_account(algorand: AlgorandClient, funded_account: Account) -> Account: +def funded_secondary_account(algorand: AlgorandClient, funded_account: SigningAccount) -> SigningAccount: secondary_name = get_unique_name() account = get_account(algorand.client.algod, secondary_name) algorand.send.payment( @@ -59,7 +59,7 @@ def funded_secondary_account(algorand: AlgorandClient, funded_account: Account) return account -def test_create_payment_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_payment_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: txn = algorand.create_transaction.payment( PaymentParams( sender=funded_account.address, @@ -74,7 +74,7 @@ def test_create_payment_transaction(algorand: AlgorandClient, funded_account: Ac assert txn.amt == AlgoAmount.from_algos(1).micro_algos -def test_create_asset_create_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_asset_create_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: expected_total = 1000 txn = algorand.create_transaction.asset_create( AssetCreateParams( @@ -99,7 +99,7 @@ def test_create_asset_create_transaction(algorand: AlgorandClient, funded_accoun def test_create_asset_config_transaction( - algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount ) -> None: txn = algorand.create_transaction.asset_config( AssetConfigParams( @@ -116,7 +116,7 @@ def test_create_asset_config_transaction( def test_create_asset_freeze_transaction( - algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount ) -> None: txn = algorand.create_transaction.asset_freeze( AssetFreezeParams( @@ -134,7 +134,7 @@ def test_create_asset_freeze_transaction( assert txn.new_freeze_state is True -def test_create_asset_destroy_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_asset_destroy_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: txn = algorand.create_transaction.asset_destroy( AssetDestroyParams( sender=funded_account.address, @@ -148,7 +148,7 @@ def test_create_asset_destroy_transaction(algorand: AlgorandClient, funded_accou def test_create_asset_transfer_transaction( - algorand: AlgorandClient, funded_account: Account, funded_secondary_account: Account + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount ) -> None: expected_amount = 100 txn = algorand.create_transaction.asset_transfer( @@ -167,7 +167,7 @@ def test_create_asset_transfer_transaction( assert txn.receiver == funded_secondary_account.address -def test_create_asset_opt_in_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_asset_opt_in_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: txn = algorand.create_transaction.asset_opt_in( AssetOptInParams( sender=funded_account.address, @@ -182,7 +182,7 @@ def test_create_asset_opt_in_transaction(algorand: AlgorandClient, funded_accoun assert txn.receiver == funded_account.address -def test_create_asset_opt_out_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_asset_opt_out_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: txn = algorand.create_transaction.asset_opt_out( AssetOptOutParams( sender=funded_account.address, @@ -199,7 +199,7 @@ def test_create_asset_opt_out_transaction(algorand: AlgorandClient, funded_accou assert txn.close_assets_to == funded_account.address -def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: approval_program = "#pragma version 6\nint 1" clear_state_program = "#pragma version 6\nint 1" txn = algorand.create_transaction.app_create( @@ -217,7 +217,7 @@ def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: assert txn.clear_program == b"\x06\x81\x01" -def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() @@ -248,7 +248,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde assert result.transactions[0].index == app_id -def test_create_online_key_registration_transaction(algorand: AlgorandClient, funded_account: Account) -> None: +def test_create_online_key_registration_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: sp = algorand.get_suggested_params() expected_dilution = 100 expected_first = sp.first diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 27b5a5c5..0820638d 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -5,7 +5,7 @@ import pytest from algosdk.transaction import OnComplete -from algokit_utils import Account +from algokit_utils import SigningAccount from algokit_utils._legacy_v2.application_specification import ApplicationSpecification from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_manager import AppManager @@ -36,7 +36,7 @@ def algorand() -> AlgorandClient: @pytest.fixture -def funded_account(algorand: AlgorandClient) -> Account: +def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -47,12 +47,12 @@ def funded_account(algorand: AlgorandClient) -> Account: @pytest.fixture -def sender(funded_account: Account) -> Account: +def sender(funded_account: SigningAccount) -> SigningAccount: return funded_account @pytest.fixture -def receiver(algorand: AlgorandClient) -> Account: +def receiver(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( @@ -75,7 +75,7 @@ def test_hello_world_arc32_app_spec() -> ApplicationSpecification: @pytest.fixture def test_hello_world_arc32_app_id( - algorand: AlgorandClient, funded_account: Account, test_hello_world_arc32_app_spec: ApplicationSpecification + algorand: AlgorandClient, funded_account: SigningAccount, test_hello_world_arc32_app_spec: ApplicationSpecification ) -> int: global_schema = test_hello_world_arc32_app_spec.global_state_schema local_schema = test_hello_world_arc32_app_spec.local_state_schema @@ -96,7 +96,7 @@ def test_hello_world_arc32_app_id( @pytest.fixture -def transaction_sender(algorand: AlgorandClient, sender: Account) -> AlgorandClientTransactionSender: +def transaction_sender(algorand: AlgorandClient, sender: SigningAccount) -> AlgorandClientTransactionSender: def new_group() -> TransactionComposer: return TransactionComposer( algod=algorand.client.algod, @@ -111,7 +111,9 @@ def new_group() -> TransactionComposer: ) -def test_payment(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: +def test_payment( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: amount = AlgoAmount.from_algos(1) result = transaction_sender.payment( PaymentParams( @@ -130,7 +132,7 @@ def test_payment(transaction_sender: AlgorandClientTransactionSender, sender: Ac assert txn.amt == amount.micro_algos -def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: +def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: total = 1000 params = AssetCreateParams( sender=sender.address, @@ -156,7 +158,9 @@ def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sende assert txn.url == "https://example.com" -def test_asset_config(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: +def test_asset_config( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: # First create an asset create_result = transaction_sender.asset_create( AssetCreateParams( @@ -191,7 +195,7 @@ def test_asset_config(transaction_sender: AlgorandClientTransactionSender, sende def test_asset_freeze( transaction_sender: AlgorandClientTransactionSender, - sender: Account, + sender: SigningAccount, ) -> None: # First create an asset create_result = transaction_sender.asset_create( @@ -228,7 +232,7 @@ def test_asset_freeze( assert txn.new_freeze_state is True -def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: +def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: # First create an asset create_result = transaction_sender.asset_create( AssetCreateParams( @@ -259,7 +263,7 @@ def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, send def test_asset_transfer( - transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount ) -> None: # First create an asset create_result = transaction_sender.asset_create( @@ -303,7 +307,9 @@ def test_asset_transfer( assert txn.amount == amount -def test_asset_opt_in(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: +def test_asset_opt_in( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: # First create an asset create_result = transaction_sender.asset_create( AssetCreateParams( @@ -335,7 +341,9 @@ def test_asset_opt_in(transaction_sender: AlgorandClientTransactionSender, sende assert txn.receiver == receiver.address -def test_asset_opt_out(transaction_sender: AlgorandClientTransactionSender, sender: Account, receiver: Account) -> None: +def test_asset_opt_out( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: # First create an asset create_result = transaction_sender.asset_create( AssetCreateParams( @@ -377,7 +385,7 @@ def test_asset_opt_out(transaction_sender: AlgorandClientTransactionSender, send assert txn.close_assets_to == sender.address -def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: +def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: approval_program = "#pragma version 6\nint 1" clear_state_program = "#pragma version 6\nint 1" params = AppCreateParams( @@ -399,7 +407,7 @@ def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: def test_app_call( - test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount ) -> None: params = AppCallParams( app_id=test_hello_world_arc32_app_id, @@ -413,7 +421,7 @@ def test_app_call( def test_app_call_method_call( - test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount ) -> None: params = AppCallMethodCallParams( app_id=test_hello_world_arc32_app_id, @@ -431,8 +439,8 @@ def test_app_call_method_call( def test_payment_logging( mock_debug: MagicMock, transaction_sender: AlgorandClientTransactionSender, - sender: Account, - receiver: Account, + sender: SigningAccount, + receiver: SigningAccount, ) -> None: amount = AlgoAmount.from_algos(1) transaction_sender.payment( @@ -450,7 +458,7 @@ def test_payment_logging( assert receiver.address in log_message -def test_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: Account) -> None: +def test_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: sp = transaction_sender._algod.suggested_params() # noqa: SLF001 params = OnlineKeyRegistrationParams( From 0755c87dbd17a74e9b1a3a3caf558420cfd84eeb Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 28 Jan 2025 01:40:31 +0100 Subject: [PATCH 25/31] docs: further refinements in capabilities .md files --- docs/source/capabilities/account.md | 160 +++--- docs/source/capabilities/algorand-client.md | 230 ++++++-- docs/source/capabilities/amount.md | 52 +- docs/source/capabilities/app-client.md | 575 ++++++-------------- docs/source/capabilities/app-deploy.md | 65 ++- docs/source/capabilities/app-manager.md | 9 +- docs/source/capabilities/client.md | 26 +- docs/source/capabilities/debugger.md | 41 +- docs/source/capabilities/transaction.md | 12 +- docs/source/capabilities/transfer.md | 28 +- 10 files changed, 550 insertions(+), 648 deletions(-) diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index cb7190a0..3864be4f 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -4,39 +4,38 @@ Account management is one of the core capabilities provided by AlgoKit Utils. It ## `AccountManager` -The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using transaction composition to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! +The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the `TransactionComposer` to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! -To get an instance of `AccountManager`, you can either use the `AlgorandClient` via `algorand.account` or instantiate it directly: +To get an instance of `AccountManager`, you can use either `AlgorandClient` via `algorand.account` or instantiate it directly: ```python -from algokit_utils import AccountManager +from algokit_utils.accounts.account_manager import AccountManager account_manager = AccountManager(client_manager) ``` -## `Account` and Transaction Signing +## `TransactionSignerAccount` -The core type that holds information about a signer/sender pair for a transaction in Python is the `Account` class, which represents both the signing capability and sender address in one object. This is different from the TypeScript implementation which uses `TransactionSignerAccount` interface that combines an `algosdk.TransactionSigner` with a sender address. +The core internal type that holds information about a signer/sender pair for a transaction is `TransactionSignerAccount`, which represents an `algosdk.TransactionSigner` (`signer`) along with a sender address (`addr`) as the encoded string address. -The Python `Account` class provides: - -- `address` - The encoded string address -- `private_key` - The private key for signing -- `signer` - An `AccountTransactionSigner` that can sign transactions -- `public_key` - The public key associated with this account +Many methods in `AccountManager` expose a `TransactionSignerAccount`. `TransactionSignerAccount` can be used with `AtomicTransactionComposer`, `TransactionComposer` and other Algorand SDK tools. ## Registering a signer -The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by the transaction composition functionality to automatically sign transactions by that sender. Any of the [methods](#accounts) within `AccountManager` that return an account will automatically register the signer with the sender. +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by `AlgorandClient` to automatically sign transactions by that sender. Any of the methods within `AccountManager` that return an account will automatically register the signer with the sender. If however, you are creating a signer external to the `AccountManager`, then you need to register the signer with the `AccountManager` if you want it to be able to automatically sign transactions from that sender. -There are two methods that can be used for this: +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of account based objects that combine signer and sender (`TransactionSignerAccount`, `SigningAccount`, `LogicSigAccount`, `MultiSigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: ```python -# Register an account object that has both signer and sender -account_manager.set_signer_from_account(account) - -# Register just a signer for a given sender address -account_manager.set_signer("SENDER_ADDRESS", transaction_signer) +algorand.account\ + .set_signer_from_account(SigningAccount.new_account())\ + .set_signer_from_account(LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)))\ + .set_signer_from_account(MultiSigAccount( + MultisigMetadata(version=1, threshold=1, addresses=["ADDRESS1...", "ADDRESS2..."]), + [account1, account2] + ))\ + .set_signer_from_account(TransactionSignerAccount(address="SENDERADDRESS", signer=transaction_signer))\ + .set_signer("SENDERADDRESS", transaction_signer) ``` ## Default signer @@ -44,103 +43,95 @@ account_manager.set_signer("SENDER_ADDRESS", transaction_signer) If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can register a default signer: ```python -account_manager.set_default_signer(my_default_signer) +algorand.account.set_default_signer(my_default_signer) ``` ## Get a signer -The library will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: +`AlgorandClient` will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: ```python -signer = account_manager.get_signer("SENDER_ADDRESS") +signer = algorand.account.get_signer("SENDER_ADDRESS") ``` -If there is no signer registered for that sender address it will either return the default signer (if registered) or raise an exception. +If there is no signer registered for that sender address it will either return the default signer (if registered) or raise a `ValueError`. ## Accounts In order to get/register accounts for signing operations you can use the following methods on `AccountManager`: -- `from_environment(name: str, fund_with: AlgoAmount | None = None) -> Account` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `{NAME}_MNEMONIC` and (optionally) `{NAME}_SENDER` (if account is rekeyed) - - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code - - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD -- `from_mnemonic(mnemonic_secret: str) -> Account` - Registers and returns an account with secret key loaded by taking the mnemonic secret -- `multisig(version: int, threshold: int, addrs: list[str], signing_accounts: list[Account]) -> MultisigAccount` - Registers and returns a multisig account with one or more signing keys loaded -- `rekeyed(sender: Account | str, account: Account) -> Account` - Registers and returns an account representing the given rekeyed sender/signer combination -- `random() -> Account` - Returns a new, cryptographically randomly generated account with private key loaded -- `from_kmd(name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an account with private key loaded from the given KMD wallet -- `logic_sig(program: bytes, args: list[bytes] | None = None) -> LogicSigAccount` - Returns an account that represents a logic signature +- `algorand.account.from_environment(name, fund_with)` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `os.getenv('{NAME}_MNEMONIC')` and (optionally) `os.getenv('{NAME}_SENDER')` (if account is rekeyed) +- `algorand.account.from_mnemonic(mnemonic=mnemonic, sender=None)` - Registers and returns an account with secret key loaded by taking the mnemonic secret +- `algorand.account.multisig(metadata, signing_accounts)` - Registers and returns a multisig account with one or more signing keys loaded +- `algorand.account.rekeyed(sender=sender, account=account)` - Registers and returns an account representing the given rekeyed sender/signer combination +- `algorand.account.random()` - Returns a new, cryptographically randomly generated account with private key loaded +- `algorand.account.from_kmd(name, predicate=None, sender=None)` - Returns an account with private key loaded from the given KMD wallet (identified by name) +- `algorand.account.logicsig(program, args=None)` - Returns an account that represents a logic signature ### Underlying account classes -While `Account` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer: +While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. -- `Account` - The main account class that combines address and private key -- `LogicSigAccount` - An in-built algosdk `LogicSigAccount` object for logic signature accounts -- `MultisigAccount` - An abstraction around multisig accounts that supports multisig accounts with one or more signers present +- `SigningAccount` - A class that holds the private key and address for an account, with support for rekeyed accounts +- `LogicSigAccount` - A wrapper around `algosdk.transaction.LogicSigAccount` object +- `MultiSigAccount` - A wrapper around Algorand SDK's multisig functionality that supports multisig accounts with one or more signers present +- `MultisigMetadata` - A dataclass containing the version, threshold and addresses for a multisig account ### Dispenser -- `dispenser_from_environment() -> Account` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present -- `localnet_dispenser() -> Account` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account +- `algorand.account.dispenser_from_environment()` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- `algorand.account.localnet_dispenser()` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account ## Rekey account One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). -```{warning} -Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. -``` +> [!WARNING] +> Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. -You can issue a transaction to rekey an account by using the `rekey_account` method: +You can issue a transaction to rekey an account by using the `algorand.account.rekey_account(account, rekey_to, **kwargs)` function: -```python -account_manager.rekey_account( - account="ACCOUNTADDRESS", # str | Account - rekey_to="NEWADDRESS", # str | Account - # Optional parameters - signer=None, # TransactionSigner - note=None, # bytes - lease=None, # bytes - static_fee=None, # AlgoAmount - extra_fee=None, # AlgoAmount - max_fee=None, # AlgoAmount - validity_window=None, # int - first_valid_round=None, # int - last_valid_round=None, # int - suppress_log=None # bool -) -``` - -You can also pass in `rekey_to` as a common transaction parameter to any transaction. +- `account: str | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekey_to: str | TransactionSignerAccountProtocol` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- Optional keyword arguments: + - Common transaction parameters + - Execution parameters ### Examples ```python # Basic example (with string addresses) -account_manager.rekey_account(account="ACCOUNTADDRESS", rekey_to="NEWADDRESS") +algorand.account.rekey_account( + account="ACCOUNTADDRESS", + rekey_to="NEWADDRESS" +) # Basic example (with signer accounts) -account_manager.rekey_account(account=account1, rekey_to=new_signer_account) +algorand.account.rekey_account( + account=account1, + rekey_to=new_signer_account +) # Advanced example -account_manager.rekey_account( +algorand.account.rekey_account( account="ACCOUNTADDRESS", rekey_to="NEWADDRESS", - lease="lease", - note="note", + lease=b"lease", + note=b"note", first_valid_round=1000, validity_window=10, - extra_fee=1000, # microAlgos - static_fee=1000, # microAlgos - max_fee=3000, # microAlgos - max_rounds_to_wait_for_confirmation=5, - suppress_log=True, + extra_fee=AlgoAmount.from_micro_algos(1000), + static_fee=AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extraFee AND staticFee + # already specified, but here for completeness + max_fee=AlgoAmount.from_micro_algos(3000), + suppress_log=True ) # Using a rekeyed account -# Note: if a signing account is passed into account_manager.rekey_account then you don't need to call rekeyed_account to register the new signer -rekeyed_account = account_manager.rekeyed(account, new_account) +# Note: if a signing account is passed into algorand.account.rekey_account +# then you don't need to call rekeyed to register the new signer +rekeyed_account = algorand.account.rekeyed(sender=account, account=new_account) # rekeyed_account can be used to sign transactions on behalf of account... ``` @@ -153,10 +144,10 @@ When running LocalNet, you have an instance of the [Key Management Daemon](https The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that's needed. This code has been abstracted away into the `KmdAccountManager` class. -To get an instance of the `KmdAccountManager` class you can access it from `AccountManager` via `account_manager.kmd` or instantiate it directly (passing in a `ClientManager`): +To get an instance of the `KmdAccountManager` class you can access it from `AlgorandClient` via `algorand.account.kmd` or instantiate it directly: ```python -from algokit_utils import KmdAccountManager +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager # Algod client only kmd_account_manager = KmdAccountManager(client_manager) @@ -164,32 +155,35 @@ kmd_account_manager = KmdAccountManager(client_manager) The methods that are available are: -- `get_wallet_account(wallet_name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). -- `get_or_create_wallet_account(name: str, fund_with: AlgoAmount | None = None) -> Account` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. -- `get_localnet_dispenser_account() -> Account` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) +- `get_wallet_account(wallet_name, predicate=None, sender=None)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- `get_or_create_wallet_account(name, fund_with=None)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- `get_localnet_dispenser_account()` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) ```python # Get a wallet account that seeded the LocalNet network default_dispenser_account = kmd_account_manager.get_wallet_account( "unencrypted-default-wallet", - lambda a: a.status != "Offline" and a.amount > 1_000_000_000, + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 ) # Same as above, but dedicated method call for convenience localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() # Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD # if creating it then fund it with 2 ALGO from the default dispenser account -new_account = kmd_account_manager.get_or_create_wallet_account("account1", AlgoAmount.from_algo(2)) +new_account = kmd_account_manager.get_or_create_wallet_account( + "account1", + AlgoAmount.from_algos(2) +) # This will return the same account as above since the name matches existing_account = kmd_account_manager.get_or_create_wallet_account("account1") ``` -Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions: +Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via `AlgorandClient`: ```python # Get and register LocalNet dispenser -localnet_dispenser = account_manager.localnet_dispenser() +localnet_dispenser = algorand.account.localnet_dispenser() # Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD -dispenser = account_manager.dispenser_from_environment() +dispenser = algorand.account.dispenser_from_environment() # Get / create and register account from KMD idempotently by name -account1 = account_manager.from_kmd("account1", AlgoAmount.from_algo(2)) +account1 = algorand.account.from_kmd("account1", fund_with=AlgoAmount.from_algos(2)) ``` diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md index 181751dc..3e7c517f 100644 --- a/docs/source/capabilities/algorand-client.md +++ b/docs/source/capabilities/algorand-client.md @@ -1,36 +1,31 @@ # Algorand client -`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the default entrypoint into AlgoKit Utils functionality. +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the [default entrypoint](../../../README.md) into AlgoKit Utils functionality. -The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](todo_paste_url), e.g.: ```python # Point to the network configured through environment variables or -# if no environment variables it will point to the default LocalNet configuration +# if no environment variables it will point to the default LocalNet +# configuration algorand = AlgorandClient.from_environment() # Point to default LocalNet configuration -algorand = AlgorandClient.default_localnet() +algorand = AlgorandClient.default_local_net() # Point to TestNet using AlgoNode free tier algorand = AlgorandClient.testnet() # Point to MainNet using AlgoNode free tier algorand = AlgorandClient.mainnet() -# Point to a pre-created algod client(s) -algorand = AlgorandClient.from_clients( - AlgoSdkClients( - algod=..., - indexer=..., - kmd=..., - ) -) -# Point to custom configuration +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=algod) +# Point to pre-created algod, indexer and kmd clients +algorand = AlgorandClient.from_clients(algod=algod, indexer=indexer, kmd=kmd) +# Point to custom configuration for algod +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod, indexer and kmd algorand = AlgorandClient.from_config( - AlgoClientConfigs( - algod_config=AlgoClientConfig( - server="http://localhost:4001", token="my-token", port=4001 - ), - indexer_config=None, - kmd_config=None, - ) + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config, ) ``` @@ -39,7 +34,7 @@ algorand = AlgorandClient.from_config( Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. ```python -algorand = AlgorandClient.default_localnet() +algorand = AlgorandClient.default_local_net() algod_client = algorand.client.algod indexer_client = algorand.client.indexer @@ -48,38 +43,195 @@ kmd_client = algorand.client.kmd ## Accessing manager class instances -The `AlgorandClient` has several manager class instances that help you quickly access advanced functionality: +The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. -- `AccountManager` via `algorand.account`, with chainable convenience methods: - - `algorand.set_default_signer(signer)` +- [`AccountManager`](todo_paste_url) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.set_default_signer(signer)` - + - `algorand.set_signer_from_account(account)` - - `algorand.set_signer(sender, signer)` -- `AssetManager` via `algorand.asset` -- `ClientManager` via `algorand.client` -- `AppManager` via `algorand.app` -- `AppDeployer` via `algorand.app_deployer` +- [`AssetManager`](todo_paste_url) via `algorand.asset` +- [`ClientManager`](todo_paste_url) via `algorand.client` ## Creating and issuing transactions -`AlgorandClient` exposes methods to create, execute, and compose groups of transactions via the `TransactionComposer`. +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](todo_paste_url)). -### Transaction configuration +### Creating transactions + +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](todo_paste_url) class. Intellisense will guide you on the different options. + +The signature for the calls to send a single transaction usually look like: + +```python +def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> Transaction: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonTransactionParams - Common transaction parameters + returns: Transaction - An unsigned algosdk.Transaction object, ready to be signed and sent + """ +``` + +- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space or cmd+space). +- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) +- [`CommonTransactionParams`](todo_paste_url) are the [common transaction parameters](todo_paste_url) that can be specified for every single transaction +- `Transaction` is an unsigned `algosdk.Transaction` object, ready to be signed and sent + +The return type for the ABI method call methods are slightly different: + +```python +def app_call_type_method_call(self, *, params: ComposerTransactionTypeParams, **common_params) -> BuiltTransactions: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonTransactionParams - Common transaction parameters + returns: BuiltTransactions - Container for transactions, method calls and signers + """ +``` -AlgorandClient caches network provided transaction values automatically to reduce network traffic. You can configure this behavior: +Where `BuiltTransactions` looks like this: -- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds the transaction will be valid). Defaults to 10. -- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) -- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout for caching suggested network parameters (default 3 seconds) -- `algorand.get_suggested_params()` - Get current suggested network parameters +```python +@dataclass +class BuiltTransactions: + """Container for built transactions and associated metadata""" + # The built transactions + transactions: list[Transaction] + # Any ABIMethod objects associated with any of the transactions in a dict keyed by transaction index + method_calls: dict[int, ABIMethod] + # Any TransactionSigner objects associated with any of the transactions in a dict keyed by transaction index + signers: dict[int, TransactionSigner] +``` + +This signifies the fact that an ABI method call can actually result in multiple transactions (which in turn may have different signers), that you need ABI metadata to be able to extract the return value from the transaction result. + +### Sending a single transaction + +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`](todo_paste_url) class. Intellisense will guide you on the different options. + +Further documentation is present in the related capabilities: + +- [App management](todo_paste_url) +- [Asset management](todo_paste_url) +- [Algo transfers](todo_paste_url) -### Creating transaction groups +The signature for the calls to send a single transaction usually look like: -You can compose a group of transactions using the `new_group()` method which returns a `TransactionComposer` instance: +```python +def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> SingleSendTransactionResult: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonAppCallParams & SendParams - Common parameters for app calls and transaction sending + returns: SingleSendTransactionResult - Result of sending a single transaction + """ +``` + +- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space). +- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) +- [`CommonAppCallParams`](todo_paste_url) are the [common app call transaction parameters](todo_paste_url) that can be specified for every single app transaction +- [`SendParams`](todo_paste_url) are the [parameters](todo_paste_url) that control execution semantics when sending transactions to the network +- [`SendSingleTransactionResult`](todo_paste_url) is all of the information that is relevant when [sending a single transaction to the network](todo_paste_url) + +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppress_log=True`. + +### Composing a group of transactions + +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{type}()` methods on [`TransactionComposer`](todo_paste_url) to add a series of transactions. ```python result = ( - algorand.new_group() - .add_payment(sender="SENDERADDRESS", receiver="RECEIVERADDRESS", amount=1_000) + algorand + .new_group() + .add_payment( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=microalgos(1) + ) .add_asset_opt_in(sender="SENDERADDRESS", asset_id=12345) .send() ) ``` + +`new_group()` returns a new [`TransactionComposer`](todo_paste_url) instance, which can also return the group of transactions, simulate them and other things. + +### Transaction parameters + +To create a transaction you define a set of parameters as a Python params dataclass instance. + +The type [`TxnParams`](todo_paste_url) is a union type representing all of the transaction parameters that can be specified for constructing any Algorand transaction type. + +- `sender: str` - The address of the account sending the transaction +- `signer: TransactionSigner | TransactionSignerAccountProtocol | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured) +- `rekey_to: Optional[str]` - Change the signing key of the sender to the given address. **Warning:** Please be careful and read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/) +- `note: Optional[bytes | str]` - Note to attach to transaction (UTF-8 encoded if string). Max 1000 bytes +- `lease: Optional[bytes | str]` - Prevent duplicate transactions with same lease (max 32 bytes). [Lease documentation](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) +- Fee management: + - `static_fee: Optional[AlgoAmount]` - Fixed transaction fee (use `extra_fee` instead unless setting to 0) + - `extra_fee: Optional[AlgoAmount]` - Additional fee to cover inner transactions + - `max_fee: Optional[AlgoAmount]` - Maximum allowed fee (prevents overspending during congestion) +- Validity management: + + - `validity_window: Optional[int]` - Number of rounds transaction is valid (default: 10) + - `first_valid_round: Optional[int]` - Explicit first valid round (use with caution) + - `last_valid_round: Optional[int]` - Explicit last valid round (prefer `validity_window`) + +- [`SendParams`](todo_paste_url) + - `max_rounds_to_wait_for_confirmation: Optional[int]` - Maximum rounds to wait for confirmation + - `suppress_log: bool` - Suppress log messages (default: False) + - `populate_app_call_resources: bool` - Auto-populate app call resources using simulation (default: from config) + - `cover_app_call_inner_transaction_fees: bool` - Automatically cover inner transaction fees via simulation + +Some more transaction-specific parameters extend these base types: + +#### Payment Transactions (`PaymentParams`) + +- `receiver: str` - Recipient address +- `amount: AlgoAmount` - Amount to send +- `close_remainder_to: Optional[str]` - Address to send remaining funds to (for account closure) + +#### Asset Transactions + +- `AssetTransferParams`: Asset transfers including opt-in +- `AssetCreateParams`: Asset creation +- `AssetConfigParams`: Asset configuration +- `AssetFreezeParams`: Asset freezing +- `AssetDestroyParams`: Asset destruction + +#### Application Transactions + +- `AppCallParams`: Generic application calls +- `AppCreateParams`: Application creation +- `AppUpdateParams`: Application update +- `AppDeleteParams`: Application deletion + +#### Key Registration + +- `OnlineKeyRegistrationParams`: Register online participation keys +- `OfflineKeyRegistrationParams`: Take account offline + +Usage example with `AlgorandClient`: + +```python +# Create transaction +payment = client.create_transaction.payment( + PaymentParams(sender=account.address, receiver=receiver, amount=AlgoAmount(1)) +) + +# Send transaction +result = client.send.send_transaction(payment, SendParams()) +``` + +These parameters are used with the [`TransactionComposer`](todo_paste_url) class which handles: + +- Automatic fee calculation +- Validity window management +- Transaction grouping +- ABI method handling +- Simulation-based resource population + +### Transaction configuration + +AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it's set to `1000`. +- `algorand.set_suggested_params(suggested_params, until=None)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) +- `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md index e1c3a7be..4478c918 100644 --- a/docs/source/capabilities/amount.md +++ b/docs/source/capabilities/amount.md @@ -2,39 +2,37 @@ Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. -Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function you can safely and explicitly convert to microAlgo or Algo. +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the [modularity principle](todo_paste_url)) you can safely and explicitly convert to microAlgo or Algo. -To see some usage examples check out the automated tests in the repository. Alternatively, you can refer to the reference documentation for `AlgoAmount`. +To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. ## `AlgoAmount` -The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or exiting the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or existing the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). To import the AlgoAmount class you can access it via: ```python -from algokit_utils.models import AlgoAmount +from algokit_utils import AlgoAmount ``` ### Creating an `AlgoAmount` -There are several ways to create an `AlgoAmount`: +There are a few ways to create an `AlgoAmount`: - Algo - - Constructor: `AlgoAmount({"algo": 10})` - - Static helper: `AlgoAmount.from_algo(10)` - - Static helper (plural): `AlgoAmount.from_algos(10)` + - Constructor: `AlgoAmount({"algo": 10})` or `AlgoAmount({"algos": 10})` + - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` - microAlgo - - Constructor: `AlgoAmount({"microAlgo": 10_000})` - - Static helper: `AlgoAmount.from_micro_algo(10_000)` - - Static helper (plural): `AlgoAmount.from_micro_algos(10_000)` + - Constructor: `AlgoAmount({"microAlgo": 10_000})` or `AlgoAmount({"microAlgos": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` ### Extracting a value from `AlgoAmount` The `AlgoAmount` class has properties to return Algo and microAlgo: -- `amount.algo` or `amount.algos` - Returns the value in Algo -- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo +- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer `AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. @@ -42,28 +40,16 @@ You can also call `str(amount)` or use an `AlgoAmount` directly in string interp ### Additional Features -The `AlgoAmount` class also supports: +The `AlgoAmount` class supports arithmetic operations: -- Arithmetic operations (`+`, `-`) with other `AlgoAmount` objects or integers -- Comparison operations (`<`, `<=`, `>`, `>=`, `==`, `!=`) -- In-place arithmetic (`+=`, `-=`) +- Addition: `amount1 + amount2` +- Subtraction: `amount1 - amount2` +- Comparison operations: `<`, `<=`, `>`, `>=`, `==`, `!=` -Example usage: +Example: ```python -from algokit_utils.models import AlgoAmount - -# Create amounts -amount1 = AlgoAmount.from_algo(1.5) # 1.5 Algos -amount2 = AlgoAmount.from_micro_algos(500_000) # 0.5 Algos - -# Arithmetic -total = amount1 + amount2 # 2 Algos -difference = amount1 - amount2 # 1 Algo - -# Comparisons -is_greater = amount1 > amount2 # True - -# String representation -print(amount1) # "1,500,000 µALGO" +amount1 = AlgoAmount({"algo": 1}) +amount2 = AlgoAmount({"microAlgo": 500_000}) +total = amount1 + amount2 # Results in 1.5 Algo ``` diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 91a9982e..4d43e02d 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -1,9 +1,9 @@ -# App Client and App Factory +# App client and App factory > [!NOTE] > This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](./app-deploy.md) and [App management](./app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](../../markdown/capabilities/app-deploy.md) and [App management](../../markdown/capabilities/app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. > [!NOTE] > If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don't know the app ID (deferred knowledge or the instance doesn't exist yet on the blockchain) or you have multiple app IDs @@ -15,26 +15,22 @@ The `AppFactory` is a class that, for a given app spec, allows you to create and To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: ```python -from algokit_utils.clients import AlgorandClient - -# Create an Algorand client -algorand = AlgorandClient.from_environment() - # Minimal example factory = algorand.get_app_factory( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", ) # Advanced example factory = algorand.get_app_factory( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec - app_name="MyApp", - default_sender="SENDER_ADDRESS", - default_signer=signer, - version="1.0.0", - updatable=True, - deletable=True, - deploy_time_params={"TMPL_VALUE": "value"}, + app_spec=parsed_arc32_or_arc56_app_spec, + default_sender="SENDERADDRESS", + app_name="OverriddenAppName", + version="2.0.0", + compilation_params={ + "updatable": True, + "deletable": False, + "deploy_time_params": { "ONE": 1, "TWO": "value" }, + } ) ``` @@ -42,201 +38,125 @@ factory = algorand.get_app_factory( The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). -To get an instance of `AppClient` you can use `AlgorandClient` via `get_app_client_by_id` or use the factory methods: +To get an instance of `AppClient` you can use either `AlgorandClient` or instantiate it directly: ```python -from algokit_utils.clients import AlgorandClient - -# Create an Algorand client -algorand = AlgorandClient.from_environment() - -# Get client by ID -client = algorand.get_app_client_by_id( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec - app_id=existing_app_id, # Use 0 for new app - app_name="MyApp", # Optional: Name of the app - default_sender="SENDER_ADDRESS", # Optional: Default sender address - default_signer=signer, # Optional: Default signer for transactions - approval_source_map=approval_map, # Optional: Source map for approval program - clear_source_map=clear_map, # Optional: Source map for clear program +# Minimal examples +app_client = AppClient.from_creator_and_name( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + creator_address="CREATORADDRESS", + algorand=algorand, +) + +app_client = AppClient( + AppClientParams( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + app_id=12345, + algorand=algorand, + ) ) -# Get client by creator and name using factory -factory = algorand.get_app_factory(app_spec=app_spec) -client = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", - app_name="MyApp", - ignore_cache=False, # Optional: Whether to ignore app lookup cache +app_client = AppClient.from_network( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + algorand=algorand, +) + +# Advanced example +app_client = AppClient( + AppClientParams( + app_spec=parsed_app_spec, + app_id=12345, + algorand=algorand, + app_name="OverriddenAppName", + default_sender="SENDERADDRESS", + approval_source_map=approval_teal_source_map, + clear_source_map=clear_teal_source_map, + ) ) ``` -You can get the `app_id` and `app_address` at any time as properties on the `AppClient` along with `app_name` and `app_spec`. +You can access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. ## Dynamically creating clients for a given app spec -As well as allowing you to control creation and deployment of apps, the `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. +The `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. This is possible via two methods on the app factory: -- `factory.get_app_client_by_id` - Returns a new `AppClient` client for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. -- `factory.get_app_client_by_creator_and_name` - Returns a new `AppClient` client, resolving the app by creator address and name using AlgoKit app deployment semantics (i.e. looking for the app creation transaction note). Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. +- `factory.get_app_client_by_id(app_id, ...)` - Returns a new `AppClient` for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified. +- `factory.get_app_client_by_creator_and_name(creator_address, app_name, ...)` - Returns a new `AppClient`, resolving the app by creator address and name using AlgoKit app deployment semantics. Automatically populates app_name, default_sender and source maps from the factory if not specified. ```python -# Get clients by ID -client1 = factory.get_app_client_by_id(app_id=12345) -client2 = factory.get_app_client_by_id(app_id=12346) -client3 = factory.get_app_client_by_id( +app_client1 = factory.get_app_client_by_id(app_id=12345) +app_client2 = factory.get_app_client_by_id(app_id=12346) +app_client3 = factory.get_app_client_by_id( app_id=12345, - default_sender="SENDER2_ADDRESS" + default_sender="SENDER2ADDRESS" ) -# Get clients by creator and name -client4 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" ) -client5 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", - app_name="NonDefaultAppName", +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" ) -client6 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", +app_client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", app_name="NonDefaultAppName", ignore_cache=True, # Perform fresh indexer lookups - default_sender="SENDER2_ADDRESS", + default_sender="SENDER2ADDRESS" ) ``` ## Creating and deploying an app -Once you have an [app factory](#appfactory) you can perform the following actions: +Once you have an app factory you can perform the following actions: -- `factory.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.send.bare.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app - `factory.deploy(params)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app ### Create The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: -- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec (noting you can override the `schema`) -- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) -- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control; these values can also be passed into the `AppFactory` constructor instead and if so will be used if not defined in the params to the create call +- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control ```python # Use no-argument bare-call -create_response = factory.send.bare.create() +result, app_client = factory.send.bare.create() # Specify parameters for bare-call and override other parameters -create_response = factory.send.bare.create( - params=factory.params.bare.create( - args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptInOC, - deploy_time_params={ - "ONE": 1, - "TWO": "two", - }, - updatable=True, - deletable=False, - populate_app_call_resources=True, - ) -) - -## Or passing params directly -create_response = factory.send.bare.create( - AppFactoryCreateWithSendParams( +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptInOC, - deploy_time_params={ + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { "ONE": 1, "TWO": "two", }, - updatable=True, - deletable=False, - populate_app_call_resources=True, - ) + "updatable": True, + "deletable": False, + } ) # Specify parameters for ABI method call -create_response = factory.send.create( - params=factory.params.create( +result, app_client = factory.send.create( + AppClientMethodCallParams( method="create_application", - args=[1, "something"], + args=[1, "something"] ) ) ``` -If you want to construct a custom create call, you can get params objects: - -- `factory.params.create(params)` - ABI method create call for deploy method -- `factory.params.bare.create(params)` - Bare create call for deploy method - -### Deploy - -The deploy method is a wrapper over the `AppDeployer`'s `deploy` method, with the following differences: - -- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` in the `create_params` because these are all specified or calculated from the app spec (noting you can override the `schema`) -- `sender` is optional for `create_params`, `update_params` and `delete_params` and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) -- You don't need to pass in `metadata` to the deploy params - it's calculated from: - - `updatable` and `deletable`, which you can optionally pass in directly to the method params - - `version` and `name`, which are optionally passed into the `AppFactory` constructor -- `deploy_time_params`, `updatable` and `deletable` can all be passed into the `AppFactory` and if so will be used if not defined in the params to the deploy call for deploy-time parameter replacements and deploy-time immutability and permanence control -- `create_params`, `update_params` and `delete_params` are optional, if they aren't specified then default values are used for everything and a no-argument bare call will be made for any create/update/delete calls -- If you want to call an ABI method for create/update/delete calls then you can pass in a string for `method`, which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature - -```python -# Use no-argument bare-calls to deploy with default behaviour -# for when update or schema break detected (fail the deployment) -client, response = factory.deploy({}) - -# Specify parameters for bare-calls and override the schema break behaviour -client, response = factory.deploy( - create_params=factory.params.bare.create( - args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptIn, - ), - update_params=factory.params.bare.deploy_update( - args=[bytes([1, 2, 3])], - ), - delete_params=factory.params.bare.deploy_delete( - args=[bytes([1, 2])], - ), - deploy_time_params={ - "ONE": 1, - "TWO": "two", - }, - on_update=OnUpdate.UpdateApp, - on_schema_break=OnSchemaBreak.ReplaceApp, - updatable=True, - deletable=True, -) - -# Specify parameters for ABI method calls -client, response = factory.deploy( - create_params=factory.params.create( - method="create_application", - args=[1, "something"], - ), - update_params=factory.params.deploy_update( - method="update", - ), - delete_params=factory.params.deploy_delete( - method="delete_app(uint64,uint64,uint64)uint64", - args=[1, 2, 3], - ), -) -``` - -If you want to construct a custom deploy call, you can get params objects for the `create_params`, `update_params` and `delete_params`: - -- `factory.params.create(params)` - ABI method create call for deploy method -- `factory.params.deploy_update(params)` - ABI method update call for deploy method -- `factory.params.deploy_delete(params)` - ABI method delete call for deploy method -- `factory.params.bare.create(params)` - Bare create call for deploy method -- `factory.params.bare.deploy_update(params)` - Bare update call for deploy method -- `factory.params.bare.deploy_delete(params)` - Bare delete call for deploy method - ## Updating and deleting an app -Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than [other calls](#calling-the-app), with the caveat that the update call is a bit different to the others since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. ## Calling the app @@ -244,14 +164,14 @@ You can construct a params object, transaction(s) and sign and send a transactio This is done via the following properties: -- `client.params.{on_complete}(params)` - Params for an ABI method call -- `client.params.bare.{on_complete}(params)` - Params for a bare call -- `client.create_transaction.{on_complete}(params)` - Transaction(s) for an ABI method call -- `client.create_transaction.bare.{on_complete}(params)` - Transaction for a bare call -- `client.send.{on_complete}(params)` - Sign and send an ABI method call -- `client.send.bare.{on_complete}(params)` - Sign and send a bare call +- `app_client.params.{method}(params)` - Params for an ABI method call +- `app_client.params.bare.{method}(params)` - Params for a bare call +- `app_client.create_transaction.{method}(params)` - Transaction(s) for an ABI method call +- `app_client.create_transaction.bare.{method}(params)` - Transaction for a bare call +- `app_client.send.{method}(params)` - Sign and send an ABI method call +- `app_client.send.bare.{method}(params)` - Sign and send a bare call -To make one of these calls `{on_complete}` needs to be swapped with the [on complete action](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#the-lifecycle-of-a-smart-contract) that should be made: +Where `{method}` is one of: - `update` - An update call - `opt_in` - An opt-in call @@ -260,115 +180,66 @@ To make one of these calls `{on_complete}` needs to be swapped with the [on comp - `close_out` - A close-out call - `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) -The input payload for all of these calls is the same as the underlying app methods with the caveat that the `app_id` is not passed in (since the `AppClient` already knows the app ID), `sender` is optional (it uses `default_sender` from the `AppClient` constructor if it was specified) and `method` (for ABI method calls) is a string rather than an `ABIMethod` object (which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature). - ```python -update_call = client.send.update( - params=client.params.update( +call1 = app_client.send.update( + AppClientMethodCallParams( method="update_abi", args=["string_io"], - deploy_time_params=deploy_time_params, - ) + ), + compilation_params={"deploy_time_params": deploy_time_params} ) -delete_call = client.send.delete( - params=client.params.delete( + +call2 = app_client.send.delete( + AppClientMethodCallParams( method="delete_abi", - args=["string_io"], - ) -) -opt_in_call = client.send.opt_in( - params=client.params.opt_in( - method="opt_in" + args=["string_io"] ) ) -clear_state_call = client.send.bare.clear_state() -transaction = client.create_transaction.bare.close_out( - params=client.params.bare.close_out( - args=[bytes([1, 2, 3])], - ) +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") ) -params = client.params.opt_in(method="optin") -``` - -### Nested ABI Method Call Transactions - -The ARC4 ABI specification supports ABI method calls as arguments to other ABI method calls, enabling some interesting use cases. While this conceptually resembles a function call hierarchy, in practice, the transactions are organized as a flat, ordered transaction group. Unfortunately, this logically hierarchical structure cannot always be correctly represented as a flat transaction group, making some scenarios impossible. - -To illustrate this, let's consider an example of two ABI methods with the following signatures: - -- `myMethod(pay,appl)void` -- `myOtherMethod(pay)void` - -These signatures are compatible, so `myOtherMethod` can be passed as an ABI method call argument to `myMethod`, which would look like: +call4 = app_client.send.bare.clear_state() -Hierarchical method call - -``` -myMethod(pay, myOtherMethod(pay)) -``` - -Flat transaction group - -``` -pay (pay) -appl (myOtherMethod) -appl (myMethod) -``` - -An important limitation to note is that the flat transaction group representation does not allow having two different pay transactions. This invariant is represented in the hierarchical call interface of the app client by passing `None` for the value. This acts as a placeholder and tells the app client that another ABI method call argument will supply the value for this argument. For example: - -```python -payment = client.algorand.create_transaction.payment( - sender="SENDER_ADDRESS", - receiver="RECEIVER_ADDRESS", - amount=1_000_000, # 1 Algo -) - -my_other_method_call = client.params.call( - method="myOtherMethod", - args=[payment], +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) ) -my_method_call = client.send.call( - params=client.params.call( - method="myMethod", - args=[None, my_other_method_call], - ) +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") ) ``` -`my_other_method_call` supplies the pay transaction to the transaction group and, by association, `my_other_method_call` has access to it as defined in its signature. -To ensure the app client builds the correct transaction group, you must supply a value for every argument in a method call signature. - ## Funding the app account -Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you `fund_app_account(params)`. +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you via `fund_app_account(params)`. The input parameters are: -- A `FundAppParams`, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). +- A `FundAppAccountParams` object, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you'll want to get the funding call as a transaction, e.g.: ```python -result = client.send.call( - params=client.params.call( +result = app_client.send.call( + AppClientMethodCallParams( method="bootstrap", args=[ - client.create_transaction.fund_app_account( - params=client.params.fund_app_account( - amount=200_000, # microAlgos + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) ) - ), + ) ], - box_references=["Box1"], + box_references=["Box1"] ) ) ``` -You can also get the funding call as a params object via `client.params.fund_app_account(params)`. +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. ## Reading state @@ -376,52 +247,58 @@ You can also get the funding call as a params object via `client.params.fund_app ### App spec methods -The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the [generic methods](#generic-methods) give you. +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the generic methods give you. You can access this functionality via: -- `client.state.global_state.{method}()` - Global state -- `client.state.local_state(address).{method}()` - Local state -- `client.state.box.{method}()` - Box storage +- `app_client.state.global_state.{method}()` - Global state +- `app_client.state.local_state(address).{method}()` - Local state +- `app_client.state.box.{method}()` - Box storage Where `{method}` is one of: -- `get_all()` - Returns all single-key state values in a record keyed by the key name and the value a decoded ABI value. +- `get_all()` - Returns all single-key state values in a dict keyed by the key name and the value a decoded ABI value. - `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. -- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be a `bytes` with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` -- `get_map(map_name)` - Returns all map values for the given map in a key=>value record. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be bytes with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value dict. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. ```python -values = client.state.global_state.get_all() -value = client.state.local_state("ADDRESS").get_value("value1") -map_value = client.state.box.get_map_value("map1", "mapKey") -map_values = client.state.global_state.get_map("myMap") +values = app_client.state.global_state.get_all() +value = app_client.state.local_state("ADDRESS").get_value("value1") +map_value = app_client.state.box.get_map_value("map1", "mapKey") +map_dict = app_client.state.global_state.get_map("myMap") ``` ### Generic methods There are various methods defined that let you read state from the smart contract app: -- `get_global_state()` - Gets the current global state -- `get_local_state(address: str)` - Gets the current local state for the given account address -- `get_box_names()` - Gets the current box names -- `get_box_value(name)` - Gets the current value of the given box -- `get_box_value_from_abi_type(name, abi_type)` - Gets the current value of the given box decoded using the specified ABI type -- `get_box_values(filter)` - Gets the current values of the boxes -- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes decoded using the specified ABI type +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](todo_paste_url) +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](todo_paste_url). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](todo_paste_url). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](todo_paste_url). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](todo_paste_url). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](todo_paste_url). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](todo_paste_url). ```python -global_state = client.get_global_state() -local_state = client.get_local_state("ACCOUNT_ADDRESS") - -box_name = "my-box" -box_name2 = "my-box2" - -box_names = client.get_box_names() -box_value = client.get_box_value(box_name) -box_values = client.get_box_values([box_name, box_name2]) -box_abi_value = client.get_box_value_from_abi_type(box_name, algosdk.ABIStringType()) -box_abi_values = client.get_box_values_from_abi_type([box_name, box_name2], algosdk.ABIStringType()) +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = "my-box" +box_name2: BoxReference = "my-box2" + +box_names = app_client.get_box_names() +box_value = app_client.get_box_value(box_name) +box_values = app_client.get_box_values([box_name, box_name2]) +box_abi_value = app_client.get_box_value_from_abi_type( + box_name, + algosdk.ABIStringType +) +box_abi_values = app_client.get_box_values_from_abi_type( + [box_name, box_name2], + algosdk.ABIStringType +) ``` ## Handling logic errors and diagnosing errors @@ -430,160 +307,38 @@ Often when calling a smart contract during development you will get logic errors When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the source map from compilation you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. +The information in that error message can be parsed and when combined with the [source map from compilation](todo_paste_url) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. -The app client and app factory automatically provide this functionality for all smart contract calls. When an error is thrown then the resulting error that is re-thrown will be a `LogicError` object, which has the following fields: +The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -- `logic_error_str: str` - The original error message -- `program: str` - The TEAL program -- `source_map: AlgoSourceMap | None` - The source map if available -- `transaction_id: str` - The transaction ID that triggered the error -- `message: str` - The error message -- `pc: int` - The program counter value -- `traces: list[SimulationTrace] | None` - Any traces that were included in the error -- `line_no: int | None` - The line number in the TEAL program that triggered the error -- `lines: list[str]` - The TEAL program split into lines +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](todo_paste_url), which has the following fields: + +- `message: str` - The formatted error message `{ERROR_MESSAGE}. at:{TEAL_LINE}. {ERROR_DESCRIPTION}` +- `stack: str` - A stack trace of the TEAL code showing where the error was with the 5 lines either side of it +- `led: LogicErrorDetails` - The parsed [logic error details](todo_paste_url) from the error message, with the following properties: + - `tx_id: str` - The transaction ID that triggered the error + - `pc: int` - The program counter + - `msg: str` - The raw error message + - `desc: str` - The full error description + - `traces: List[Dict[str, Any]]` - Any traces that were included in the error +- `program: List[str]` - The TEAL program split by line +- `teal_line: int` - The line number in the TEAL program that triggered the error Note: This information will only show if the app client / app factory has a source map. This will occur if: - You have called `create`, `update` or `deploy` - You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) -- You had source maps present in an app factory and then used it to create an app client (they are automatically passed through) +- You had source maps present in an app factory and then used it to [create an app client](todo_paste_url) (they are automatically passed through) -If you want to go a step further and automatically issue a simulated transaction and get trace information when there is an error when an ABI method is called you can turn on debug mode: +If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: ```python -from algokit_utils.config import config - -config.configure(debug=True) +Config.configure({"debug": True}) ``` If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. -When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the project root is also configured. - -Example error handling: - -```python -from algokit_utils.config import config - -# Enable debug mode for detailed error information -config.configure(debug=True) - -try: - client.send.call( - params=client.params.call( - method="will_fail", - args=["test"] - ) - ) -except algokit_utils.LogicError as e: - print(f"Error at line {e.value.line_no}") # Access via value property - print(f"Error message: {e.value.message}") - print(f"Transaction ID: {e.value.transaction_id}") - print(e.value.trace()) # Shows TEAL execution trace with source mapping - - if e.value.traces: # Available when debug mode is active - for trace in e.value.traces: - print(f"PC: {trace['pc']}, Stack: {trace['stack']}") -``` - -## Best Practices - -1. Use typed ABI methods when possible for better type safety -2. Always handle potential logic errors with proper error handling -3. Use transaction composition for atomic operations -4. Leverage source maps and debug mode for development -5. Use idempotent deployment patterns with versioning -6. Properly manage box references to avoid transaction failures -7. Use template values for flexible application deployment -8. Implement proper state management with type safety -9. Use the client's parameter builders for type-safe transaction creation -10. Leverage the state accessor patterns for cleaner state management - -## Common Patterns - -### Idempotent Deployment - -```python -# Deploy with idempotency and version tracking -client, response = factory.deploy( - version="1.0.0", - deploy_time_params={"TMPL_VALUE": "value"}, - on_update=algokit_utils.OnUpdate.UpdateApp, - on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, - create_params=factory.params.create( - method="create", - args=["initial_value"], - ), -) - -if response.app.app_id != 0: - print(f"Deployed app ID: {response.app.app_id}") - if response.operation_performed == algokit_utils.OperationPerformed.Create: - print("New application deployed") - else: - print("Existing application found") -``` - -### Application State Migration - -```python -# Deploy with state migration -client, response = factory.deploy( - version="2.0.0", - on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, - on_update=algokit_utils.OnUpdate.UpdateApp, - create_params=factory.params.create( - method="create", - args=["initial_value"], - schema={ - "global_ints": 1, - "global_byte_slices": 1, - "local_ints": 0, - "local_byte_slices": 0, - }, - ), -) - -if response.operation_performed == algokit_utils.OperationPerformed.Replace: - # Migrate state from old to new app - # Note: Migration logic should be implemented in the smart contract - client.send.call( - params=client.params.call( - method="migrate_state", - args=[response.old_app_id], - ) - ) -``` - -### Opt-in Management - -```python -# Create opt-in parameters -opt_in_params = client.params.opt_in( - method="initialize", # Optional: Method to call during opt-in - args=["initial_value"], # Optional: Arguments for initialization - boxes=[("user_data", "ACCOUNT_ADDRESS")], # Optional: Box allocation -) - -# Create and send opt-in transaction -transaction = client.create_transaction.opt_in(opt_in_params) -result = client.send.opt_in(opt_in_params) - -# Check if account is opted in -is_opted_in = client.is_opted_in("ACCOUNT_ADDRESS") - -# Create close-out parameters -close_out_params = client.params.close_out( - method="cleanup", # Optional: Method to call during close-out - args=["cleanup_value"], # Optional: Arguments for cleanup -) - -# Create and send close-out transaction -transaction = client.create_transaction.close_out(close_out_params) -result = client.send.close_out(close_out_params) -``` +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](todo_paste_url). ## Default arguments diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index d041576e..cd95d4e7 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -8,8 +8,6 @@ TEAL template substitution. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -(design)= - ## Design The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). @@ -46,8 +44,7 @@ Furthermore, the implementation contains the following implementation characteri ## Finding apps by creator -There is a method `algokit.get_creator_apps(creatorAccount, indexer)`, which performs a series of indexer lookups that return all apps created by the given creator. These are indexed by the name it -was deployed under if the creation transaction contained the following payload in the transaction note field: +The `AppDeployer.get_creator_apps_by_name()` method performs indexer lookups to find all apps created by an account that were deployed using this framework. The results are cached in an `ApplicationLookup` object. ``` ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} @@ -61,39 +58,45 @@ fresh version. ## Deploying an application -The method that performs the deployment logic is the instance method `ApplicationClient.deploy`. It performs an idempotent (safely retryable) deployment. It will detect if the app already -exists and if it doesn't it will create it. If the app does already exist then it will: +The class that performs the deployment logic is `AppDeployer` with the `deploy` method. It performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn't it will create it. If the app does already exist then it will: - Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the - deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the deployment configuration. It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. -This metadata works in concert with `get_creator_apps` to allow the app to be reliably retrieved against that creator in it's currently deployed state. `deploy` automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability. ### Input parameters -The following inputs are used when deploying an App +The `AppDeployParams` dataclass accepts these key parameters: + +- `metadata`: Required AppDeploymentMetaData containing name, version, deletable and updatable flags +- `deploy_time_params`: Optional TealTemplateParams for TEAL template substitution +- `on_schema_break`: Optional behavior for schema breaks - can be string literal "replace", "fail", "append" or OnSchemaBreak enum +- `on_update`: Optional behavior for updates - can be string literal "update", "replace", "fail", "append" or OnUpdate enum +- `create_params`: AppCreateParams or AppCreateMethodCallParams specifying app creation parameters +- `update_params`: AppUpdateParams or AppUpdateMethodCallParams specifying app update parameters +- `delete_params`: AppDeleteParams or AppDeleteMethodCallParams specifying app deletion parameters +- `existing_deployments`: Optional ApplicationLookup to cache and reduce indexer calls +- `ignore_cache`: When true, forces fresh indexer lookup even if creator apps are cached +- `max_fee`: Maximum microalgos to spend on any single transaction +- `send_params`: Additional transaction sending parameters (fee, signer, etc.) + +### Error Handling -- `version`: The version string for the app defined in app_spec, if not specified the version will automatically increment for existing apps that are updated, and set to 1.0 for new apps -- `signer`, `sender`: Optional signer and sender for deployment operations, sender must be the same as the creator specified -- `allow_update`, `allow_delete`: Control the updatability and deletability of the app, used to populate `TMPL_UPDATABLE` and `TMPL_DELETABLE` template values -- `on_update`: Determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment) -- `on_schema_break`: Determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created) -- `create_args`: Args to use if a create operation is performed -- `update_args`: Args to use if an update operation is performed -- `delete_args`: Args to use if a delete operation is performed -- `template_values`: Values to use for automatic substitution of [deploy-time parameter values](#design) is mapping of `key: value` that will result in `TMPL_{key}` being replaced with `value` +Specific error cases that throw ValueError: + +- Schema break with on_schema_break=fail +- Update attempt on non-updatable app +- Replacement attempt on non-deletable app +- Invalid `existing_deployments` cache provided ### Idempotency `deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. -(compilation-and-template-substitution)= - ### Compilation and template substitution When compiling TEAL template code, the capabilities described in the [design above](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. @@ -104,18 +107,14 @@ In order for a smart contract to be able to use this functionality, it must have - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn't (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn't (permanent) -If you are building a smart contract using the [beaker_production AlgoKit template](https://github.com/algorandfoundation/algokit-beaker-default-template) if provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the [Python AlgoKit template](https://github.com/algorandfoundation/algokit-python-template) it provides a reference implementation out of the box for the deploy-time immutability and permanence control. ### Return value -`deploy` returns a `DeployResponse` object, that describes the action taken. - -- `action_taken`: Describes what happened during deployment - - `Create` - The smart contract app is created. - - `Update` - The smart contract app is updated - - `Replace` - The smart contract app was deleted and created again (in an atomic transaction) - - `Nothing` - Nothing was done since an existing up-to-date app was found -- `create_response`: If action taken was `Create` or `Replace`, the result of the create transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `update_response`: If action taken was `Update`, the result of the update transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `delete_response`: If action taken was `Replace`, the result of the delete transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `app`: An `AppMetaData` object, describing the final app state +`deploy` returns an `AppDeployResult` object containing: + +- `operation_performed`: Enum indicating action taken (Create/Update/Replace/Nothing) +- `app`: ApplicationMetaData with final app state +- `create_result`: Transaction result if creation occurred +- `update_result`: Transaction result if update occurred +- `delete_result`: Transaction result if replacement occurred diff --git a/docs/source/capabilities/app-manager.md b/docs/source/capabilities/app-manager.md index 3fcfd4fa..55d30b17 100644 --- a/docs/source/capabilities/app-manager.md +++ b/docs/source/capabilities/app-manager.md @@ -38,10 +38,13 @@ compilation_result = app_manager.compile_teal_template( template_params=template_params ) -# Compile with deployment metadata (for updatable/deletable control) +# Compile with deployment control (updatable/deletable) +control_template = f"""#pragma version 8 +int {UPDATABLE_TEMPLATE_NAME} +int {DELETABLE_TEMPLATE_NAME}""" deployment_metadata = {"updatable": True, "deletable": True} compilation_result = app_manager.compile_teal_template( - template_code, + control_template, deployment_metadata=deployment_metadata ) ``` @@ -134,7 +137,7 @@ box_ref = b"my_box" box_ref = account_signer # Box reference with app ID -box_ref = BoxReference(app_id=123, name="my_box") +box_ref = BoxReference(app_id=123, name=b"my_box") ``` ## Common app parameters diff --git a/docs/source/capabilities/client.md b/docs/source/capabilities/client.md index 08614682..206c3c47 100644 --- a/docs/source/capabilities/client.md +++ b/docs/source/capabilities/client.md @@ -14,15 +14,23 @@ To get an instance of `ClientManager` you can instantiate it directly: ```python from algokit_utils import ClientManager - -# Algod client only -client_manager = ClientManager(algod=algod_client) -# All clients -client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) -# Algod config only -client_manager = ClientManager(algod_config=algod_config) -# All client configs -client_manager = ClientManager(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) +from algosdk.v2client.algod import AlgodClient + +algod_client = AlgodClient(...) +algorand_client = ... # Get AlgorandClient instance from somewhere + +# Using existing client instances +client_manager = ClientManager( + {"algod": algod_client, "indexer": indexer_client, "kmd": kmd_client}, + algorand_client=algorand_client +) + +# Using configs +algod_config = {"server": "https://..."} +client_manager = ClientManager( + {"algod_config": algod_config}, + algorand_client=algorand_client +) ``` ## Network configuration diff --git a/docs/source/capabilities/debugger.md b/docs/source/capabilities/debugger.md index a6274393..948fed17 100644 --- a/docs/source/capabilities/debugger.md +++ b/docs/source/capabilities/debugger.md @@ -48,24 +48,29 @@ config.configure( When debug mode is enabled, AlgoKit Utils will automatically: -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. - -The following methods are provided for scenarios where you want to manually persist sourcemaps and traces: - -- `persist_sourcemaps`: This method persists the sourcemaps for the - given sources as AVM Debugger compliant artifacts. It takes a list of - `PersistSourceMapInput` objects, a `Path` object representing the root - directory of the project, an `AlgodClient` object for interacting with the - Algorand blockchain, and a boolean indicating whether to dump teal source - files along with sourcemaps. -- `simulate_and_persist_response`: This method simulates the atomic - transactions using the provided `AtomicTransactionComposer` object and - `AlgodClient` object, and persists the simulation response to an AVM - Debugger compliant JSON file. It takes an `AtomicTransactionComposer` - object representing the atomic transactions to be simulated and persisted, - a `Path` object representing the root directory of the project, an - `AlgodClient` object representing the Algorand client, and a float - representing the size of the trace buffer in megabytes. +- Generate transaction traces compatible with the AVM Debugger +- Manage trace file storage with automatic cleanup +- Provide source map generation for TEAL contracts + +The following methods are provided for manual debugging operations: + +- `persist_sourcemaps`: Persists sourcemaps for given TEAL contracts as AVM Debugger-compliant artifacts. Parameters: + + - `sources`: List of TEAL sources to generate sourcemaps for + - `project_root`: Project root directory for storage + - `client`: AlgodClient instance + - `with_sources`: Whether to include TEAL source files (default: True) + +- `simulate_and_persist_response`: Simulates transactions and persists debug traces. Parameters: + - `atc`: AtomicTransactionComposer containing transactions + - `project_root`: Project root directory for storage + - `algod_client`: AlgodClient instance + - `buffer_size_mb`: Maximum trace storage in MB (default: 256) + - `allow_empty_signatures`: Allow unsigned transactions (default: True) + - `allow_unnamed_resources`: Allow unnamed resources (default: True) + - `extra_opcode_budget`: Additional opcode budget + - `exec_trace_config`: Custom trace configuration + - `simulation_round`: Specific round to simulate ### Trace filename format diff --git a/docs/source/capabilities/transaction.md b/docs/source/capabilities/transaction.md index 34f3c45d..e10a8e7b 100644 --- a/docs/source/capabilities/transaction.md +++ b/docs/source/capabilities/transaction.md @@ -16,11 +16,11 @@ class SendSingleTransactionResult: transaction: TransactionWrapper # Last transaction confirmation: AlgodResponseType # Last confirmation group_id: str - tx_id: str | None = None - tx_ids: list[str] # Full array of transaction IDs + tx_id: str | None = None # Transaction ID of the last transaction + tx_ids: list[str] # All transaction IDs in the group transactions: list[TransactionWrapper] confirmations: list[AlgodResponseType] - returns: list[ABIReturn] | None = None + returns: list[ABIReturn] | None = None # ABI returns if applicable ``` Common variations include: @@ -72,9 +72,9 @@ Different interfaces return different result types: - `.send.payment()` → `SendSingleTransactionResult` - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` - - `.send.app_call()` → `SendAppTransactionResult` - - `.send.app_create()` → `SendAppCreateTransactionResult` - - `.send.app_update()` → `SendAppUpdateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` (contains raw ABI return) + - `.send.app_create()` → `SendAppCreateTransactionResult` (with app ID/address) + - `.send.app_update()` → `SendAppUpdateTransactionResult` (with compilation info) 3. **AppClient Methods** diff --git a/docs/source/capabilities/transfer.md b/docs/source/capabilities/transfer.md index 0e965981..f2299170 100644 --- a/docs/source/capabilities/transfer.md +++ b/docs/source/capabilities/transfer.md @@ -16,7 +16,7 @@ The base type for specifying a payment transaction is `PaymentParams`, which has ```python # Minimal example -result = algod.send.payment( +result = algorand_client.send.payment( PaymentParams( sender="SENDERADDRESS", receiver="RECEIVERADDRESS", @@ -25,7 +25,7 @@ result = algod.send.payment( ) # Advanced example -result2 = algod.send.payment( +result2 = algorand_client.send.payment( PaymentParams( sender="SENDERADDRESS", receiver="RECEIVERADDRESS", @@ -33,7 +33,7 @@ result2 = algod.send.payment( close_remainder_to="CLOSEREMAINDERTOADDRESS", lease="lease", note=b"note", - # Use this with caution, it's generally better to use algod.account.rekey_account + # Use this with caution, it's generally better to use algorand_client.account.rekey_account rekey_to="REKEYTOADDRESS", # You wouldn't normally set this field first_valid_round=1000, @@ -59,12 +59,12 @@ The `ensure_funded` function automatically funds an account to maintain a minimu There are 3 variants of this function: -- `algod.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). -- `algod.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). - **Note:** requires environment variables to be set. - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` if it's a rekeyed account, or against default LocalNet if no environment variables present. -- `algod.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). The general structure of these calls is similar, they all take: @@ -85,9 +85,9 @@ The general structure of these calls is similar, they all take: # From account # Basic example -algod.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +algorand_client.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) # With configuration -algod.account.ensure_funded( +algorand_client.account.ensure_funded( "ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo"), @@ -99,9 +99,9 @@ algod.account.ensure_funded( # From environment # Basic example -algod.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +algorand_client.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) # With configuration -algod.account.ensure_funded_from_environment( +algorand_client.account.ensure_funded_from_environment( "ACCOUNTADDRESS", AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), @@ -112,15 +112,15 @@ algod.account.ensure_funded_from_environment( # TestNet Dispenser API # Basic example -algod.account.ensure_funded_from_testnet_dispenser_api( +algorand_client.account.ensure_funded_from_testnet_dispenser_api( "ACCOUNTADDRESS", - algod.client.get_testnet_dispenser_from_environment(), + algorand_client.client.get_testnet_dispenser_from_environment(), AlgoAmount(1, "algo") ) # With configuration -algod.account.ensure_funded_from_testnet_dispenser_api( +algorand_client.account.ensure_funded_from_testnet_dispenser_api( "ACCOUNTADDRESS", - algod.client.get_testnet_dispenser_from_environment(), + algorand_client.client.get_testnet_dispenser_from_environment(), AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), ) From 1bf49e3d1f0e4b357b4fb016f1aeb9a602f69059 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 28 Jan 2025 01:42:03 +0100 Subject: [PATCH 26/31] refactor: addressing pr comments --- src/algokit_utils/models/transaction.py | 2 +- .../transactions/transaction_composer.py | 38 +++++++-------- tests/transactions/test_resource_packing.py | 46 +++++++++---------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py index 68e23153..e0c07cda 100644 --- a/src/algokit_utils/models/transaction.py +++ b/src/algokit_utils/models/transaction.py @@ -97,4 +97,4 @@ class SendParams(TypedDict, total=False): max_rounds_to_wait: int | None suppress_log: bool | None populate_app_call_resources: bool | None - cover_app_call_inner_txn_fees: bool | None + cover_app_call_inner_transaction_fees: bool | None diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 2ae654cb..1fcf4125 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -647,7 +647,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912 atc: AtomicTransactionComposer, algod: AlgodClient, populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None, ) -> ExecutionInfo: # Create simulation request @@ -670,9 +670,9 @@ def _get_group_execution_info( # noqa: C901, PLR0912 for i, txn in enumerate(empty_signer_atc.txn_list): txn_with_signer = TransactionWithSigner(txn=txn.txn, signer=NULL_SIGNER) - if cover_app_call_inner_txn_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn): + if cover_app_call_inner_transaction_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn): if not suggested_params: - raise ValueError("suggested_params required when cover_app_call_inner_txn_fees enabled") + raise ValueError("suggested_params required when cover_app_call_inner_transaction_fees enabled") max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] if max_fee is None: @@ -680,9 +680,9 @@ def _get_group_execution_info( # noqa: C901, PLR0912 else: txn_with_signer.txn.fee = max_fee - if cover_app_call_inner_txn_fees and app_call_indexes_without_max_fees: + if cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees: raise ValueError( - f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_txn_fees` is enabled. " # noqa: E501 + f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_transaction_fees` is enabled. " # noqa: E501 f"Required for transactions: {', '.join(str(i) for i in app_call_indexes_without_max_fees)}" ) @@ -697,7 +697,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912 if group_response.get("failure-message"): msg = group_response["failure-message"] - if cover_app_call_inner_txn_fees and "fee too small" in msg: + if cover_app_call_inner_transaction_fees and "fee too small" in msg: raise ValueError( "Fees were too small to resolve execution info via simulate. " "You may need to increase an app call transaction maxFee." @@ -718,7 +718,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912 original_txn = atc.build_group()[i].txn required_fee_delta = 0 - if cover_app_call_inner_txn_fees: + if cover_app_call_inner_transaction_fees: # Calculate parent transaction fee parent_per_byte_fee = per_byte_txn_fee * (original_txn.estimate_size() + 75) parent_min_fee = max(parent_per_byte_fee, min_txn_fee) @@ -810,7 +810,7 @@ def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 atc: AtomicTransactionComposer, algod: AlgodClient, populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None, ) -> AtomicTransactionComposer: """Prepare a transaction group for sending by handling execution info and resources. @@ -818,20 +818,20 @@ def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 :param atc: The AtomicTransactionComposer containing transactions :param algod: Algod client for simulation :param populate_app_call_resources: Whether to populate app call resources - :param cover_app_call_inner_txn_fees: Whether to cover inner txn fees + :param cover_app_call_inner_transaction_fees: Whether to cover inner txn fees :param additional_atc_context: Additional context for the AtomicTransactionComposer :return: Modified AtomicTransactionComposer ready for sending """ # Get execution info via simulation execution_info = _get_group_execution_info( - atc, algod, populate_app_call_resources, cover_app_call_inner_txn_fees, additional_atc_context + atc, algod, populate_app_call_resources, cover_app_call_inner_transaction_fees, additional_atc_context ) max_fees = additional_atc_context.max_fees if additional_atc_context else None group = atc.build_group() # Handle transaction fees if needed - if cover_app_call_inner_txn_fees: + if cover_app_call_inner_transaction_fees: # Sort transactions by fee priority txns_with_priority: list[_TransactionWithPriority] = [] for i, txn_info in enumerate(execution_info.txns or []): @@ -1082,7 +1082,7 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: app_txn.boxes = boxes # type: ignore[attr-defined] # Update fees if needed - if cover_app_call_inner_txn_fees and i in additional_fees: + if cover_app_call_inner_transaction_fees and i in additional_fees: cur_txn = group[i].txn additional_fee = additional_fees[i] if not isinstance(cur_txn, algosdk.transaction.ApplicationCallTxn): @@ -1165,7 +1165,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 skip_waiting: bool = False, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, - cover_app_call_inner_txn_fees: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None, ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group. @@ -1178,7 +1178,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 :param skip_waiting: If True, don't wait for transaction confirmation, defaults to False :param suppress_log: If True, suppress logging, defaults to None :param populate_app_call_resources: If True, populate app call resources, defaults to None - :param cover_app_call_inner_txn_fees: If True, cover app call inner transaction fees, defaults to None + :param cover_app_call_inner_transaction_fees: If True, cover app call inner transaction fees, defaults to None :param additional_atc_context: Additional context for the AtomicTransactionComposer :return: Results from sending the transaction group :raises Exception: If there is an error sending the transactions @@ -1196,14 +1196,14 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 else config.populate_app_call_resource ) - if (populate_app_call_resources or cover_app_call_inner_txn_fees) and any( + if (populate_app_call_resources or cover_app_call_inner_transaction_fees) and any( isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer ): atc = prepare_group_for_sending( atc, algod, populate_app_call_resources, - cover_app_call_inner_txn_fees, + cover_app_call_inner_transaction_fees, additional_atc_context, ) @@ -1623,10 +1623,10 @@ def send( has_app_call = any(isinstance(txn.txn, ApplicationCallTxn) for txn in group) params = SendParams() if has_app_call else SendParams() - cover_app_call_inner_txn_fees = params.get("cover_app_call_inner_txn_fees") + cover_app_call_inner_transaction_fees = params.get("cover_app_call_inner_transaction_fees") populate_app_call_resources = params.get("populate_app_call_resources") wait_rounds = params.get("max_rounds_to_wait") - sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_txn_fees else None + sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_transaction_fees else None if wait_rounds is None: last_round = max(txn.txn.last_valid_round for txn in group) @@ -1641,7 +1641,7 @@ def send( max_rounds_to_wait=wait_rounds, suppress_log=params.get("suppress_log"), populate_app_call_resources=populate_app_call_resources, - cover_app_call_inner_txn_fees=cover_app_call_inner_txn_fees, + cover_app_call_inner_transaction_fees=cover_app_call_inner_transaction_fees, additional_atc_context=AdditionalAtcContext( suggested_params=sp, max_fees=self._txn_max_fees, diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index 5413a4ed..cb277d26 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -514,7 +514,7 @@ def test_throws_when_no_max_fee(self) -> None: method="no_op", ), send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -532,7 +532,7 @@ def test_throws_when_inner_fees_not_covered(self) -> None: self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": False, + "cover_app_call_inner_transaction_fees": False, }, ) @@ -547,7 +547,7 @@ def test_does_not_alter_fee_without_inners(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -568,7 +568,7 @@ def test_throws_when_max_fee_too_small(self) -> None: self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -586,7 +586,7 @@ def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -602,7 +602,7 @@ def test_alters_fee_handling_when_no_itxns_covered(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -621,7 +621,7 @@ def test_alters_fee_handling_when_all_inners_covered(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -640,7 +640,7 @@ def test_alters_fee_handling_when_some_inners_covered(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -659,7 +659,7 @@ def test_alters_fee_when_some_inners_have_surplus(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) assert result.transaction.raw.fee == expected_fee @@ -688,7 +688,7 @@ def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fe self.app_client1.algorand.new_group() .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) assert result.transactions[0].raw.fee == txn_1_expected_fee @@ -708,7 +708,7 @@ def test_does_not_alter_static_fee_with_surplus(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -726,7 +726,7 @@ def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -745,7 +745,7 @@ def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -764,7 +764,7 @@ def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: result = self.app_client1.send.call( params, send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -784,7 +784,7 @@ def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) ], max_fee=AlgoAmount.from_micro_algos(expected_fee), ) - result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) assert result.transaction.raw.fee == expected_fee self._assert_min_fee(self.app_client1, params, expected_fee) @@ -813,7 +813,7 @@ def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: S ) ) ) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) assert result.transactions[0].raw.fee == expected_fee @@ -851,7 +851,7 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: static_fee=AlgoAmount.from_micro_algos(0), ) ) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) assert result.transactions[0].raw.fee == 1500 @@ -896,7 +896,7 @@ def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) - ], static_fee=AlgoAmount.from_micro_algos(expected_fee), ) - result = nested_client.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) + result = nested_client.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) assert len(result.transactions) == 3 assert result.transactions[0].raw.fee == 1500 @@ -939,7 +939,7 @@ def test_throws_when_max_fee_below_calculated(self) -> None: ) ) ) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) def test_throws_when_nested_max_fee_below_calculated(self, funded_account: SigningAccount) -> None: @@ -982,7 +982,7 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Signi max_fee=AlgoAmount.from_micro_algos(10_000), ), send_params={ - "cover_app_call_inner_txn_fees": True, + "cover_app_call_inner_transaction_fees": True, }, ) @@ -1013,7 +1013,7 @@ def test_throws_when_static_fee_below_calculated(self) -> None: ) ) ) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: SigningAccount) -> None: @@ -1051,7 +1051,7 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Signi static_fee=AlgoAmount.from_micro_algos(500), ) ) - .send({"cover_app_call_inner_txn_fees": True}) + .send({"cover_app_call_inner_transaction_fees": True}) ) def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: @@ -1063,7 +1063,7 @@ def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: args=[6200], max_fee=AlgoAmount.from_micro_algos(12_000), ) - result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_txn_fees": True}) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) assert result.transaction.raw.fee == expected_fee assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] From 719e140815657aa439469ed3003387394312486b Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 28 Jan 2025 01:48:25 +0100 Subject: [PATCH 27/31] docs: typo in logic error description --- docs/source/capabilities/app-client.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 4d43e02d..4c446ca3 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -313,16 +313,16 @@ The app client and app factory automatically provide this functionality for all When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](todo_paste_url), which has the following fields: -- `message: str` - The formatted error message `{ERROR_MESSAGE}. at:{TEAL_LINE}. {ERROR_DESCRIPTION}` -- `stack: str` - A stack trace of the TEAL code showing where the error was with the 5 lines either side of it -- `led: LogicErrorDetails` - The parsed [logic error details](todo_paste_url) from the error message, with the following properties: - - `tx_id: str` - The transaction ID that triggered the error - - `pc: int` - The program counter - - `msg: str` - The raw error message - - `desc: str` - The full error description - - `traces: List[Dict[str, Any]]` - Any traces that were included in the error -- `program: List[str]` - The TEAL program split by line -- `teal_line: int` - The line number in the TEAL program that triggered the error +- `logic_error: Exception` - The original logic error exception +- `logic_error_str: str` - The string representation of the logic error +- `program: str` - The TEAL program source code +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - Combined error message with debugging information +- `pc: int` - The program counter value where error occurred +- `traces: list[SimulationTrace] | None` - Simulation traces if debug enabled +- `line_no: int | None` - The line number in the TEAL source code +- `lines: list[str]` - The TEAL program split into individual lines Note: This information will only show if the app client / app factory has a source map. This will occur if: From cf14b5c86982354aba60aedb1cb39f7b9ca0292a Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 28 Jan 2025 19:52:49 +0100 Subject: [PATCH 28/31] fix: improving regex usage in logic error message parser; bumping pytest; adding extra deprecation warning --- poetry.lock | 745 +++++++++--------- pyproject.toml | 19 +- .../application_specification.py | 18 +- src/algokit_utils/applications/app_client.py | 9 +- src/algokit_utils/applications/app_factory.py | 2 +- src/algokit_utils/clients/client_manager.py | 2 +- .../transactions/test_transaction_creator.py | 5 +- 7 files changed, 426 insertions(+), 374 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7fb81d7e..4fc38827 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.7.0" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [package.dependencies] @@ -30,18 +30,18 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "3.3.6" +version = "3.3.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, - {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] [package.dependencies] @@ -104,13 +104,13 @@ files = [ [[package]] name = "cachecontrol" -version = "0.14.1" +version = "0.14.2" description = "httplib2 caching for requests" optional = false python-versions = ">=3.8" files = [ - {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, - {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, + {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, + {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, ] [package.dependencies] @@ -125,13 +125,13 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -226,127 +226,114 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -379,73 +366,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.9" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -635,29 +622,29 @@ tests = ["asttokens", "littleutils", "pytest", "rich"] [[package]] name = "filelock" -version = "3.16.1" +version = "3.17.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "gitdb" -version = "4.0.11" +version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] @@ -665,20 +652,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.43" +version = "3.1.44" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, + {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, + {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] @@ -715,57 +702,58 @@ lxml = ["lxml"] [[package]] name = "httpcore" -version = "0.16.3" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] -anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.28.1" description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" +httpcore = "==1.*" +idna = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.3" +version = "2.6.6" description = "File identification library for Python" optional = false python-versions = ">=3.9" files = [ - {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, - {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, + {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, ] [package.extras] @@ -798,13 +786,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.6.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [package.dependencies] @@ -816,7 +804,7 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -933,17 +921,17 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.5.0" +version = "25.6.0" description = "Store and access your passwords safely." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, - {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, + {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, + {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" "jaraco.context" = "*" "jaraco.functools" = "*" @@ -962,13 +950,13 @@ type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "license-expression" -version = "30.4.0" +version = "30.4.1" description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." optional = false python-versions = ">=3.9" files = [ - {file = "license_expression-30.4.0-py3-none-any.whl", hash = "sha256:7c8f240c6e20d759cb8455e49cb44a923d9e25c436bf48d7e5b8eea660782c04"}, - {file = "license_expression-30.4.0.tar.gz", hash = "sha256:6464397f8ed4353cc778999caec43b099f8d8d5b335f282e26a9eb9435522f05"}, + {file = "license_expression-30.4.1-py3-none-any.whl", hash = "sha256:679646bc3261a17690494a3e1cada446e5ee342dbd87dcfa4a0c24cc5dce13ee"}, + {file = "license_expression-30.4.1.tar.gz", hash = "sha256:9f02105f9e0fcecba6a85dfbbed7d94ea1c3a70cf23ddbfb5adf3438a6f6fce0"}, ] [package.dependencies] @@ -1124,13 +1112,13 @@ files = [ [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.6.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, - {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, ] [[package]] @@ -1208,49 +1196,55 @@ files = [ [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -1298,35 +1292,35 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, [[package]] name = "nh3" -version = "0.2.19" -description = "Python bindings to the ammonia HTML sanitization library." +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, - {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, - {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, - {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, - {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, - {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, - {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, - {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, - {file = "nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804"}, + {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, ] [[package]] @@ -1381,13 +1375,13 @@ files = [ [[package]] name = "pip" -version = "24.3.1" +version = "25.0" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" files = [ - {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, - {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, + {file = "pip-25.0-py3-none-any.whl", hash = "sha256:b6eb97a803356a52b2dd4bb73ba9e65b2ba16caa6bcb25a7497350a4e5859b65"}, + {file = "pip-25.0.tar.gz", hash = "sha256:8e0a97f7b4c47ae4a494560da84775e9e2f671d415d8d828e052efefb206b30b"}, ] [[package]] @@ -1534,13 +1528,13 @@ virtualenv = ">=20.10.0" [[package]] name = "py-algorand-sdk" -version = "2.6.1" +version = "2.7.0" description = "Algorand SDK in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "py-algorand-sdk-2.6.1.tar.gz", hash = "sha256:9223929d05f532a9295711c5ff945aa8aa854bc5efedb37b821f15335106ea14"}, - {file = "py_algorand_sdk-2.6.1-py3-none-any.whl", hash = "sha256:1257b0999f4c67dd66e0517da5081e014953d0a7d14edecc45d53b8aba1b7328"}, + {file = "py-algorand-sdk-2.7.0.tar.gz", hash = "sha256:9bb20d794aa4c67452330ad76fcd016195241c7bee2a39720cea688df6620a1b"}, + {file = "py_algorand_sdk-2.7.0-py3-none-any.whl", hash = "sha256:4e04e8705ac65b38adcd14ccfc39d21406019ddbd6ae5b3aba287471d139be34"}, ] [package.dependencies] @@ -1635,13 +1629,13 @@ flake8 = ["flake8 (>=4)"] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1675,13 +1669,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyparsing" -version = "3.2.0" +version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" files = [ - {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, - {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, ] [package.extras] @@ -1707,13 +1701,13 @@ tabulate = ">=0.9.0,<0.10.0" [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1721,47 +1715,47 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-httpx" -version = "0.21.3" +version = "0.35.0" description = "Send responses to httpx." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest_httpx-0.21.3-py3-none-any.whl", hash = "sha256:50b52b910f6f6cfb0aa65039d6f5bedb6ae3a0c02a98c4a7187543fe437c428a"}, - {file = "pytest_httpx-0.21.3.tar.gz", hash = "sha256:edcb62baceffbd57753c1a7afc4656b0e71e91c7a512e143c0adbac762d979c1"}, + {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, + {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, ] [package.dependencies] -httpx = "==0.23.*" -pytest = ">=6.0,<8.0" +httpx = "==0.28.*" +pytest = "==8.*" [package.extras] -testing = ["pytest-asyncio (==0.20.*)", "pytest-cov (==4.*)"] +testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==6.*)"] [[package]] name = "pytest-mock" @@ -1780,6 +1774,25 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +files = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + [[package]] name = "pytest-xdist" version = "3.6.1" @@ -1993,18 +2006,15 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rfc3986" -version = "1.5.0" +version = "2.0.0" description = "Validating URI References per RFC 3986" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, ] -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - [package.extras] idna2008 = ["idna"] @@ -2097,23 +2107,23 @@ files = [ [[package]] name = "setuptools" -version = "75.6.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2128,13 +2138,13 @@ files = [ [[package]] name = "smmap" -version = "5.0.1" +version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] @@ -2246,13 +2256,13 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] [[package]] name = "sphinx-markdown-builder" -version = "0.6.7" +version = "0.6.8" description = "A Sphinx extension to add markdown generation support." optional = false python-versions = ">=3.7" files = [ - {file = "sphinx_markdown_builder-0.6.7-py3-none-any.whl", hash = "sha256:6d52b63d2b7b3504ca664773e805b0ee8957239f2ca86103e793d96103970839"}, - {file = "sphinx_markdown_builder-0.6.7.tar.gz", hash = "sha256:9623c8d5963e18b3733ec8335a48b58c3e556a96529b73e4c65113cabd8e8591"}, + {file = "sphinx_markdown_builder-0.6.8-py3-none-any.whl", hash = "sha256:f04ab42d52449363228b9104569c56b778534f9c41a168af8cfc721a1e0e3edc"}, + {file = "sphinx_markdown_builder-0.6.8.tar.gz", hash = "sha256:6141b566bf18dd1cd515a0a90efd91c6c4d10fc638554fab2fd19cba66543dd7"}, ] [package.dependencies] @@ -2404,6 +2414,20 @@ files = [ [package.extras] widechars = ["wcwidth"] +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "toml" version = "0.10.2" @@ -2511,6 +2535,17 @@ rfc3986 = ">=1.4.0" tqdm = ">=4.14" urllib3 = ">=1.26.0" +[[package]] +name = "types-deprecated" +version = "1.2.15.20241117" +description = "Typing stubs for Deprecated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e"}, + {file = "types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -2538,13 +2573,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -2555,13 +2590,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.28.0" +version = "20.29.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, - {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, ] [package.dependencies] @@ -2620,4 +2655,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "be28f13fd9fa25c4a7204d6888efd33ecbed11c7fa903768d423fa737dd4e169" +content-hash = "55003b10efa72f9205b31cf879b74048b695d8d58edad56d19b6057018c9211e" diff --git a/pyproject.toml b/pyproject.toml index ddcaa820..33a7517f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,17 +9,17 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" py-algorand-sdk = "^2.4.0" -httpx = "^0.23.1" +httpx = "^0.28" typing-extensions = ">=4.6.0" # Add this line [tool.poetry.group.dev.dependencies] -pytest = "^7.2.0" +pytest = "^8" ruff = ">=0.1.6,<=0.8.3" pip-audit = "^2.5.6" -pytest-mock = "^3.11.1" +pytest-mock = "^3.14" mypy = "^1.5.1" python-semantic-release = "^7.34.3" -pytest-cov = "^4.1.0" +pytest-cov = "^6" pre-commit = "^3.4.0" python-dotenv = "^1.0.0" sphinx = "^6.1.3" @@ -29,12 +29,14 @@ sphinx-rtd-theme = "^1.2.0" sphinx-autodoc2 = ">=0.4.2,<0.6.0" poethepoet = ">=0.19,<0.26" beaker-pyteal = "^1.1.1" -pytest-httpx = "^0.21.3" -pytest-xdist = "^3.4.0" +pytest-httpx = "^0.35" +pytest-xdist = "^3.6.1" sphinx-markdown-builder = "^0.6.6" linkify-it-py = "^2.0.3" setuptools = "^75.2.0" pydoclint = "^0.6.0" +pytest-sugar = "^1.0.0" +types-deprecated = "^1.2.15.20241117" [build-system] requires = ["poetry-core"] @@ -139,6 +141,11 @@ docstrings-check = "pydoclint src --style sphinx --arg-type-hints-in-docstring f [tool.pytest.ini_options] pythonpath = ["src", "tests"] +norecursedirs = ["src"] # Ignore test collection in source directory, otherwise picks up TestNet* prefixed abstractions +filterwarnings = [ + # Ignore deprecations in utils legacy v2 is removed + "ignore::DeprecationWarning", +] [tool.mypy] files = ["src", "tests"] diff --git a/src/algokit_utils/application_specification.py b/src/algokit_utils/application_specification.py index dcd73e21..8b38160d 100644 --- a/src/algokit_utils/application_specification.py +++ b/src/algokit_utils/application_specification.py @@ -1,5 +1,7 @@ import warnings +from deprecated import deprecated + warnings.warn( """The legacy v2 application_specification module is deprecated and will be removed in a future version. Use `from algokit_utils.applications.app_spec.arc32 import ...` to access Arc32 app spec instead. @@ -11,8 +13,9 @@ stacklevel=2, ) -from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 +from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 # noqa: E402 AppSpecStateDict, + Arc32Contract, CallConfig, DefaultArgumentDict, DefaultArgumentType, @@ -20,9 +23,18 @@ MethodHints, OnCompleteActionName, ) -from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 - Arc32Contract as ApplicationSpecification, + + +@deprecated( + "Use `Arc32Contract` from algokit_utils.applications instead. Example:\n" + "```python\n" + "from algokit_utils.applications import Arc32Contract\n" + "app_spec = Arc32Contract.from_json(app_spec_json)\n" + "```" ) +class ApplicationSpecification(Arc32Contract): + """Deprecated class for ARC-0032 application specification""" + __all__ = [ "AppSpecStateDict", diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 6d797ec5..406da6a8 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -1688,12 +1688,13 @@ def get_line_for_pc(input_pc: int) -> int | None: if error_message: import re - app_id = re.search(r"(?<=app=)\d+", str(e)) - tx_id = re.search(r"(?<=transaction )\S+(?=:)", str(e)) + message = e.logic_error_str if isinstance(e, LogicError) else str(e) + app_id = re.search(r"(?<=app=)\d+", message) + tx_id = re.search(r"(?<=transaction )\S+(?=:)", message) error = Exception( f"Runtime error when executing {app_spec.name} " - f"(appId: {app_id.group() if app_id else ''}) in transaction " - f"{tx_id.group() if tx_id else ''}: {error_message}" + f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction " + f"{tx_id.group() if tx_id else 'N/A'}: {error_message}" ) error.__cause__ = e return error diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index ffcd3339..3b0fb423 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -747,7 +747,7 @@ def _get_deploy_time_control(self, control: str) -> bool | None: on_complete in m.actions.call for m in self._app_spec.methods if m.actions and m.actions.call ) - def _get_sender(self, sender: str | bytes | None) -> str: + def _get_sender(self, sender: str | None) -> str: if not sender and not self._default_sender: raise Exception( f"No sender provided and no default sender present in app client for call to app {self._app_name}" diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 9cd029f5..283ff6c1 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -528,7 +528,7 @@ def get_typed_app_factory( typed_factory: type[TypedFactoryT], *, app_name: str | None = None, - default_sender: str | bytes | None = None, + default_sender: str | None = None, default_signer: TransactionSigner | None = None, version: str | None = None, compilation_params: AppClientCompilationParams | None = None, diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index e9825a53..a9916f96 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -13,7 +13,6 @@ PaymentTxn, ) -from algokit_utils._legacy_v2.account import get_account from algokit_utils.algorand import AlgorandClient from algokit_utils.models.account import SigningAccount from algokit_utils.models.amount import AlgoAmount @@ -30,7 +29,6 @@ OnlineKeyRegistrationParams, PaymentParams, ) -from legacy_v2_tests.conftest import get_unique_name @pytest.fixture @@ -51,8 +49,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: @pytest.fixture def funded_secondary_account(algorand: AlgorandClient, funded_account: SigningAccount) -> SigningAccount: - secondary_name = get_unique_name() - account = get_account(algorand.client.algod, secondary_name) + account = algorand.account.random() algorand.send.payment( PaymentParams(sender=funded_account.address, receiver=account.address, amount=AlgoAmount.from_algos(1)) ) From f8cb58f54155a12eb504a7b2b13c9874c775375a Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 29 Jan 2025 15:11:12 +0100 Subject: [PATCH 29/31] docs: refreshing documentation; - excluding html folder from generation (until produciton release is out) -> as this will refresh self hosted docs --- .github/workflows/check-python.yaml | 14 +- docs/.nojekyll | 0 docs/Makefile | 20 + docs/make.bat | 35 + .../apidocs/algokit_utils/algokit_utils.md | 1095 ----------------- .../markdown/autoapi/account_manager/index.md | 1 + .../accounts/account_manager/index.md | 603 +++++++++ .../autoapi/algokit_utils/accounts/index.md | 6 + .../accounts/kmd_account_manager/index.md | 71 ++ .../autoapi/algokit_utils/algorand/index.md | 156 +++ .../algokit_utils/applications/abi/index.md | 161 +++ .../applications/app_client/index.md | 578 +++++++++ .../applications/app_deployer/index.md | 128 ++ .../applications/app_factory/index.md | 138 +++ .../applications/app_manager/index.md | 202 +++ .../applications/app_spec/arc32/index.md | 157 +++ .../applications/app_spec/arc56/index.md | 681 ++++++++++ .../applications/app_spec/index.md | 6 + .../algokit_utils/applications/enums/index.md | 72 ++ .../algokit_utils/applications/index.md | 11 + .../assets/asset_manager/index.md | 173 +++ .../autoapi/algokit_utils/assets/index.md | 5 + .../clients/client_manager/index.md | 367 ++++++ .../clients/dispenser_api_client/index.md | 82 ++ .../autoapi/algokit_utils/clients/index.md | 6 + .../autoapi/algokit_utils/config/index.md | 102 ++ .../autoapi/algokit_utils/errors/index.md | 5 + .../algokit_utils/errors/logic_error/index.md | 76 ++ docs/markdown/autoapi/algokit_utils/index.md | 24 + .../algokit_utils/models/account/index.md | 120 ++ .../algokit_utils/models/amount/index.md | 108 ++ .../algokit_utils/models/application/index.md | 72 ++ .../autoapi/algokit_utils/models/index.md | 11 + .../algokit_utils/models/network/index.md | 32 + .../algokit_utils/models/simulate/index.md | 18 + .../algokit_utils/models/state/index.md | 55 + .../algokit_utils/models/transaction/index.md | 89 ++ .../algokit_utils/protocols/account/index.md | 23 + .../autoapi/algokit_utils/protocols/index.md | 6 + .../protocols/typed_clients/index.md | 97 ++ .../algokit_utils/transactions/index.md | 7 + .../transaction_composer/index.md | 841 +++++++++++++ .../transactions/transaction_creator/index.md | 90 ++ .../transactions/transaction_sender/index.md | 278 +++++ .../markdown/autoapi/algorand_client/index.md | 1 + docs/markdown/autoapi/client_manager/index.md | 1 + docs/markdown/autoapi/composer/index.md | 1 + docs/markdown/autoapi/index.md | 48 + docs/markdown/capabilities/account.md | 225 +++- docs/markdown/capabilities/algorand-client.md | 191 +++ docs/markdown/capabilities/amount.md | 55 + docs/markdown/capabilities/app-client.md | 391 ++++-- docs/markdown/capabilities/app-deploy.md | 217 +++- docs/markdown/capabilities/app.md | 163 +++ docs/markdown/capabilities/asset.md | 134 ++ docs/markdown/capabilities/client.md | 108 +- docs/markdown/capabilities/debugger.md | 45 - .../capabilities/debugging.md} | 44 +- .../markdown/capabilities/dispenser-client.md | 63 +- docs/markdown/capabilities/testing.md | 204 +++ .../capabilities/transaction-composer.md | 228 ++++ docs/markdown/capabilities/transaction.md | 135 ++ docs/markdown/capabilities/transfer.md | 199 ++- .../capabilities/typed-app-clients.md | 194 +++ docs/markdown/index.md | 251 ++-- docs/markdown/v3-migration-guide.md | 304 +++++ docs/source/capabilities/account.md | 182 +-- docs/source/capabilities/algorand-client.md | 226 ++-- docs/source/capabilities/amount.md | 2 +- docs/source/capabilities/app-client.md | 38 +- docs/source/capabilities/app-deploy.md | 210 +++- .../capabilities/{app-manager.md => app.md} | 0 docs/source/capabilities/asset.md | 134 ++ docs/source/capabilities/client.md | 46 +- .../{debugger.md => debugging.md} | 20 +- docs/source/capabilities/testing.md | 204 +++ .../capabilities/transaction-composer.md | 5 +- docs/source/capabilities/transfer.md | 14 +- docs/source/capabilities/typed-app-clients.md | 200 +++ docs/source/conf.py | 174 +-- docs/source/index.md | 146 +-- docs/source/migration-guide.md | 252 ---- docs/source/v3-migration-guide.md | 314 +++++ poetry.lock | 455 +++++-- pyproject.toml | 13 +- src/algokit_utils/accounts/account_manager.py | 2 +- src/algokit_utils/beta/__init__.py | 72 -- src/algokit_utils/beta/_utils.py | 36 + src/algokit_utils/beta/account_manager.py | 9 + src/algokit_utils/beta/algorand_client.py | 9 + src/algokit_utils/beta/client_manager.py | 9 + src/algokit_utils/beta/composer.py | 9 + src/algokit_utils/models/account.py | 8 +- 93 files changed, 10314 insertions(+), 2499 deletions(-) delete mode 100644 docs/.nojekyll create mode 100644 docs/Makefile create mode 100644 docs/make.bat delete mode 100644 docs/markdown/apidocs/algokit_utils/algokit_utils.md create mode 100644 docs/markdown/autoapi/account_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/accounts/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/algorand/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/abi/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_client/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/enums/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/applications/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/assets/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/clients/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/config/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/errors/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/account/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/amount/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/application/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/network/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/simulate/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/state/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/models/transaction/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/protocols/account/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/protocols/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/transactions/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md create mode 100644 docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md create mode 100644 docs/markdown/autoapi/algorand_client/index.md create mode 100644 docs/markdown/autoapi/client_manager/index.md create mode 100644 docs/markdown/autoapi/composer/index.md create mode 100644 docs/markdown/autoapi/index.md create mode 100644 docs/markdown/capabilities/algorand-client.md create mode 100644 docs/markdown/capabilities/amount.md create mode 100644 docs/markdown/capabilities/app.md create mode 100644 docs/markdown/capabilities/asset.md delete mode 100644 docs/markdown/capabilities/debugger.md rename docs/{html/_sources/capabilities/debugger.md.txt => markdown/capabilities/debugging.md} (63%) create mode 100644 docs/markdown/capabilities/testing.md create mode 100644 docs/markdown/capabilities/transaction-composer.md create mode 100644 docs/markdown/capabilities/transaction.md create mode 100644 docs/markdown/capabilities/typed-app-clients.md create mode 100644 docs/markdown/v3-migration-guide.md rename docs/source/capabilities/{app-manager.md => app.md} (100%) create mode 100644 docs/source/capabilities/asset.md rename docs/source/capabilities/{debugger.md => debugging.md} (75%) create mode 100644 docs/source/capabilities/testing.md create mode 100644 docs/source/capabilities/typed-app-clients.md delete mode 100644 docs/source/migration-guide.md create mode 100644 docs/source/v3-migration-guide.md delete mode 100644 src/algokit_utils/beta/__init__.py create mode 100644 src/algokit_utils/beta/_utils.py create mode 100644 src/algokit_utils/beta/account_manager.py create mode 100644 src/algokit_utils/beta/algorand_client.py create mode 100644 src/algokit_utils/beta/client_manager.py create mode 100644 src/algokit_utils/beta/composer.py diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index e8464b76..0a473b53 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -45,13 +45,7 @@ jobs: - name: Check types with mypy run: poetry run mypy - # TODO: uncomment after bulk of feature parity with ts is addressed - # - name: Check docs are up to date - # run: | - # poetry run poe docs - # git diff --quiet --exit-code \ - # ':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \ - # ':!docs/html/apidocs/algokit_utils/algokit_utils.html' \ - # ':!docs/html/searchindex.js' \ - # ':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \ - # docs/ + - name: Check docs are up to date + run: | + poetry run poe docs-md-only + git diff --exit-code ':!docs/markdown/autoapi/index.md' docs diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..dc1312ab --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/markdown/apidocs/algokit_utils/algokit_utils.md b/docs/markdown/apidocs/algokit_utils/algokit_utils.md deleted file mode 100644 index 8c991230..00000000 --- a/docs/markdown/apidocs/algokit_utils/algokit_utils.md +++ /dev/null @@ -1,1095 +0,0 @@ -# [`algokit_utils`](#module-algokit_utils) - -## Data - -### algokit_utils.AppSpecStateDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Type defining Application Specification state entries - -### algokit_utils.DELETABLE_TEMPLATE_NAME - -None - -Template variable name used to control if a smart contract is deletable or not at deployment - -### algokit_utils.DefaultArgumentType *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Literal values describing the types of default argument sources - -### algokit_utils.MethodConfigDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type - -### algokit_utils.NOTE_PREFIX - -‘ALGOKIT_DEPLOYER:j’ - -ARC-0002 compliant note prefix for algokit_utils deployed applications - -### algokit_utils.OnCompleteActionName *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -String literals representing on completion transaction types - -### algokit_utils.TemplateValueDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Dictionary of `dict[str, int | str | bytes]` representing template variable names and values - -### algokit_utils.TemplateValueMapping *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Mapping of `str` to `int | str | bytes` representing template variable names and values - -### algokit_utils.UPDATABLE_TEMPLATE_NAME - -None - -Template variable name used to control if a smart contract is updatable or not at deployment - -## Classes - -### *class* algokit_utils.ABICallArgs - -Bases: [`algokit_utils.deploy.DeployCallArgs`](#algokit_utils.DeployCallArgs), `algokit_utils.deploy.ABICall` - -ABI Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.ABICallArgsDict - -Bases: [`algokit_utils.deploy.DeployCallArgsDict`](#algokit_utils.DeployCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -ABI Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.ABICreateCallArgs - -Bases: [`algokit_utils.deploy.DeployCreateCallArgs`](#algokit_utils.DeployCreateCallArgs), `algokit_utils.deploy.ABICall` - -ABI Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.ABICreateCallArgsDict - -Bases: [`algokit_utils.deploy.DeployCreateCallArgsDict`](#algokit_utils.DeployCreateCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -ABI Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.ABITransactionResponse - -Bases: [`algokit_utils.models.TransactionResponse`](#algokit_utils.TransactionResponse), [`typing.Generic`](https://docs.python.org/3/library/typing.html#typing.Generic)[`algokit_utils.models.ReturnType`] - -Response for an ABI call - -#### decode_error *: [Exception](https://docs.python.org/3/library/exceptions.html#Exception) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Details of error that occurred when attempting to decode raw_value - -#### method *: algosdk.abi.Method* - -None - -ABI method used to make call - -#### raw_value *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)* - -None - -The raw response before ABI decoding - -#### return_value *: algokit_utils.models.ReturnType* - -None - -Decoded ABI result - -#### tx_info *: [dict](https://docs.python.org/3/library/stdtypes.html#dict)* - -None - -Details of transaction - -### *class* algokit_utils.Account - -Holds the private_key and address for an account - -#### address *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -‘field(…)’ - -Address for this account - -#### private_key *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Base64 encoded private key - -#### *property* public_key *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)* - -The public key for this account - -#### *property* signer *: [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner)* - -An AccountTransactionSigner for this account - -### *class* algokit_utils.AlgoClientConfig - -Connection details for connecting to an [`algosdk.v2client.algod.AlgodClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) or -[`algosdk.v2client.indexer.IndexerClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) - -#### server *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud` - -#### token *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -API Token to authenticate with the service - -### *class* algokit_utils.AppDeployMetaData - -Metadata about an application stored in a transaction note during creation. - -The note is serialized as JSON and prefixed with [`NOTE_PREFIX`](#algokit_utils.NOTE_PREFIX) and stored in the transaction note field -as part of [`ApplicationClient.deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.AppLookup - -Cache of [`AppMetaData`](#algokit_utils.AppMetaData) for a specific `creator` - -Can be used as an argument to [`ApplicationClient`](#algokit_utils.ApplicationClient) to reduce the number of calls when deploying multiple -apps or discovering multiple app_ids - -### *class* algokit_utils.AppMetaData - -Bases: [`algokit_utils.deploy.AppReference`](#algokit_utils.AppReference), [`algokit_utils.deploy.AppDeployMetaData`](#algokit_utils.AppDeployMetaData) - -Metadata about a deployed app - -### *class* algokit_utils.AppReference - -Information about an Algorand app - -### *class* algokit_utils.ApplicationClient(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), app_spec: [algokit_utils.application_specification.ApplicationSpecification](#algokit_utils.ApplicationSpecification) | [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), \*, app_id: [int](https://docs.python.org/3/library/functions.html#int) = 0, creator: [str](https://docs.python.org/3/library/stdtypes.html#str) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, indexer_client: IndexerClient | [None](https://docs.python.org/3/library/constants.html#None) = None, existing_deployments: [algokit_utils.deploy.AppLookup](#algokit_utils.AppLookup) | [None](https://docs.python.org/3/library/constants.html#None) = None, signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, suggested_params: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None) = None, template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_name: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) - -A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app - -### Initialization - -ApplicationClient can be created with an app_id to interact with an existing application, alternatively -it can be created with a creator and indexer_client specified to find existing applications by name and creator. - -* **Parameters:** - * **algod_client** (*AlgodClient*) – AlgoSDK algod client - * **app_spec** ([*ApplicationSpecification*](#algokit_utils.ApplicationSpecification) *|* *Path*) – An Application Specification or the path to one - * **app_id** ([*int*](https://docs.python.org/3/library/functions.html#int)) – The app_id of an existing application, to instead find the application by creator and name - use the creator and indexer_client parameters - * **creator** ([*str*](https://docs.python.org/3/library/stdtypes.html#str) *|* [*Account*](#algokit_utils.Account)) – The address or Account of the app creator to resolve the app_id - * **indexer_client** (*IndexerClient*) – AlgoSDK indexer client, only required if deploying or finding app_id by - creator and app name - * **existing_deployments** ([*AppLookup*](#algokit_utils.AppLookup)) – - * **signer** (*TransactionSigner* *|* [*Account*](#algokit_utils.Account)) – Account or signer to use to sign transactions, if not specified and - creator was passed as an Account will use that. - * **sender** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – Address to use as the sender for all transactions, will use the address associated with the - signer if not specified. - * **template_values** ([*TemplateValueMapping*](#algokit_utils.TemplateValueMapping)) – Values to use for TMPL_\* template variables, dictionary keys should - *NOT* include the TMPL_ prefix - * **app_name** ([*str*](https://docs.python.org/3/library/stdtypes.html#str) *|* [*None*](https://docs.python.org/3/library/constants.html#None)) – Name of application to use when deploying, defaults to name defined on the - Application Specification - -#### add_method_call(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, abi_args: algokit_utils.models.ABIArgsDict | [None](https://docs.python.org/3/library/constants.html#None) = None, app_id: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, on_complete: [algosdk.transaction.OnComplete](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.OnComplete) = transaction.OnComplete.NoOpOC, local_schema: [algosdk.transaction.StateSchema](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.StateSchema) | [None](https://docs.python.org/3/library/constants.html#None) = None, global_schema: [algosdk.transaction.StateSchema](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.StateSchema) | [None](https://docs.python.org/3/library/constants.html#None) = None, approval_program: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None) = None, clear_program: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None) = None, extra_pages: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None, call_config: [algokit_utils.application_specification.CallConfig](#algokit_utils.CallConfig) = au_spec.CallConfig.CALL) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a transaction to the AtomicTransactionComposer passed - -#### call(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.OnCompleteCallParameters](#algokit_utils.OnCompleteCallParameters) | [algokit_utils.models.OnCompleteCallParametersDict](#algokit_utils.OnCompleteCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with specified parameters - -#### clear_state(transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) - -Submits a signed transaction with on_complete=ClearState - -#### close_out(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=CloseOut - -#### compose_call(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.OnCompleteCallParameters](#algokit_utils.OnCompleteCallParameters) | [algokit_utils.models.OnCompleteCallParametersDict](#algokit_utils.OnCompleteCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with specified parameters to atc - -#### compose_clear_state(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=ClearState to atc - -#### compose_close_out(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=CloseOut to ac - -#### compose_create(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.CreateCallParameters](#algokit_utils.CreateCallParameters) | [algokit_utils.models.CreateCallParametersDict](#algokit_utils.CreateCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with application id == 0 and the schema and source of client’s app_spec to atc - -#### compose_delete(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=DeleteApplication to atc - -#### compose_opt_in(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=OptIn to atc - -#### compose_update(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=UpdateApplication to atc - -#### create(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.CreateCallParameters](#algokit_utils.CreateCallParameters) | [algokit_utils.models.CreateCallParametersDict](#algokit_utils.CreateCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with application id == 0 and the schema and source of client’s app_spec - -#### delete(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=DeleteApplication - -#### deploy(version: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, allow_update: [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, allow_delete: [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, on_update: [algokit_utils.deploy.OnUpdate](#algokit_utils.OnUpdate) = au_deploy.OnUpdate.Fail, on_schema_break: [algokit_utils.deploy.OnSchemaBreak](#algokit_utils.OnSchemaBreak) = au_deploy.OnSchemaBreak.Fail, template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping) | [None](https://docs.python.org/3/library/constants.html#None) = None, create_args: [algokit_utils.deploy.ABICreateCallArgs](#algokit_utils.ABICreateCallArgs) | [algokit_utils.deploy.ABICreateCallArgsDict](#algokit_utils.ABICreateCallArgsDict) | [algokit_utils.deploy.DeployCreateCallArgs](#algokit_utils.DeployCreateCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None, update_args: [algokit_utils.deploy.ABICallArgs](#algokit_utils.ABICallArgs) | [algokit_utils.deploy.ABICallArgsDict](#algokit_utils.ABICallArgsDict) | [algokit_utils.deploy.DeployCallArgs](#algokit_utils.DeployCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None, delete_args: [algokit_utils.deploy.ABICallArgs](#algokit_utils.ABICallArgs) | [algokit_utils.deploy.ABICallArgsDict](#algokit_utils.ABICallArgsDict) | [algokit_utils.deploy.DeployCallArgs](#algokit_utils.DeployCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.deploy.DeployResponse](#algokit_utils.DeployResponse) - -Deploy an application and update client to reference it. - -Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator -account, including deploy-time template placeholder substitutions. -To understand the architecture decisions behind this functionality please see -[https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md) - -#### NOTE -If there is a breaking state schema change to an existing app (and `on_schema_break` is set to -‘ReplaceApp’ the existing app will be deleted and re-created. - -#### NOTE -If there is an update (different TEAL code) to an existing app (and `on_update` is set to ‘ReplaceApp’) -the existing app will be deleted and re-created. - -* **Parameters:** - * **version** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – version to use when creating or updating app, if None version will be auto incremented - * **signer** ([*algosdk.atomic_transaction_composer.TransactionSigner*](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner)) – signer to use when deploying app - , if None uses self.signer - * **sender** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – sender address to use when deploying app, if None uses self.sender - * **allow_delete** ([*bool*](https://docs.python.org/3/library/functions.html#bool)) – Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app - can be deleted - * **allow_update** ([*bool*](https://docs.python.org/3/library/functions.html#bool)) – Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app - can be updated - * **on_update** ([*OnUpdate*](#algokit_utils.OnUpdate)) – Determines what action to take if an application update is required - * **on_schema_break** ([*OnSchemaBreak*](#algokit_utils.OnSchemaBreak)) – Determines what action to take if an application schema requirements - has increased beyond the current allocation - * **template_values** ([*dict*](https://docs.python.org/3/library/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/library/stdtypes.html#str) *,* [*int*](https://docs.python.org/3/library/functions.html#int) *|*[*str*](https://docs.python.org/3/library/stdtypes.html#str) *|*[*bytes*](https://docs.python.org/3/library/stdtypes.html#bytes) *]*) – Values to use for `TMPL_*` template variables, dictionary keys - should *NOT* include the TMPL_ prefix - * **create_args** ([*ABICreateCallArgs*](#algokit_utils.ABICreateCallArgs)) – Arguments used when creating an application - * **update_args** ([*ABICallArgs*](#algokit_utils.ABICallArgs) *|* [*ABICallArgsDict*](#algokit_utils.ABICallArgsDict)) – Arguments used when updating an application - * **delete_args** ([*ABICallArgs*](#algokit_utils.ABICallArgs) *|* [*ABICallArgsDict*](#algokit_utils.ABICallArgsDict)) – Arguments used when deleting an application -* **Return DeployResponse:** - details action taken and relevant transactions -* **Raises:** - **DeploymentError** – If the deployment failed - -#### export_source_map() → [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) - -Export approval source map to JSON, can be later re-imported with `import_source_map` - -#### get_global_state(\*, raw: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)] - -Gets the global state info associated with app_id - -#### get_local_state(account: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, raw: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)] - -Gets the local state info for associated app_id and account/sender - -#### get_signer_sender(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None), [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)] - -Return signer and sender, using default values on client if not specified - -Will use provided values if given, otherwise will fall back to values defined on client. -If no sender is specified then will attempt to obtain sender from signer - -#### import_source_map(source_map_json: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [None](https://docs.python.org/3/library/constants.html#None) - -Import approval source from JSON exported by `export_source_map` - -#### opt_in(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=OptIn - -#### prepare(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_id: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, template_values: [algokit_utils.deploy.TemplateValueDict](#algokit_utils.TemplateValueDict) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.application_client.ApplicationClient](#algokit_utils.ApplicationClient) - -Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. -Will also substitute provided template_values into the associated app_spec in the copy - -#### resolve(to_resolve: [algokit_utils.application_specification.DefaultArgumentDict](#algokit_utils.DefaultArgumentDict)) → [int](https://docs.python.org/3/library/functions.html#int) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) - -Resolves the default value for an ABI method, based on app_spec - -#### resolve_signer_sender(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Return signer and sender, using default values on client if not specified - -Will use provided values if given, otherwise will fall back to values defined on client. -If no sender is specified then will attempt to obtain sender from signer - -* **Raises:** - [**ValueError**](https://docs.python.org/3/library/exceptions.html#ValueError) – Raised if a signer or sender is not provided. See `get_signer_sender` - for variant with no exception - -#### update(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=UpdateApplication - -### *class* algokit_utils.ApplicationSpecification - -ARC-0032 application specification - -See [https://github.com/algorandfoundation/ARCs/pull/150](https://github.com/algorandfoundation/ARCs/pull/150) - -#### export(directory: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [None](https://docs.python.org/3/library/constants.html#None) - -write out the artifacts generated by the application to disk - -Args: -directory(optional): path to the directory where the artifacts should be written - -### *class* algokit_utils.CallConfig - -Bases: [`enum.IntFlag`](https://docs.python.org/3/library/enum.html#enum.IntFlag) - -Describes the type of calls a method can be used for based on [`algosdk.transaction.OnComplete`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.OnComplete) type - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -#### ALL - -3 - -Handle the specified on completion type for both create and normal application calls - -#### CALL - -1 - -Only handle the specified on completion type for application calls - -#### CREATE - -2 - -Only handle the specified on completion type for application create calls - -#### NEVER - -0 - -Never handle the specified on completion type - -### *class* algokit_utils.CreateCallParameters - -Bases: [`algokit_utils.models.OnCompleteCallParameters`](#algokit_utils.OnCompleteCallParameters) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.create/compose_create methods - -### *class* algokit_utils.CreateCallParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), [`algokit_utils.models.OnCompleteCallParametersDict`](#algokit_utils.OnCompleteCallParametersDict) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.create/compose_create methods - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.CreateTransactionParameters - -Bases: [`algokit_utils.models.TransactionParameters`](#algokit_utils.TransactionParameters) - -Additional parameters that can be included in a transaction when calling a create method - -### *class* algokit_utils.DefaultArgumentDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -DefaultArgument is a container for any arguments that may -be resolved prior to calling some target method - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployCallArgs - -Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.DeployCallArgsDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployCreateCallArgs - -Bases: [`algokit_utils.deploy.DeployCallArgs`](#algokit_utils.DeployCallArgs) - -Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.DeployCreateCallArgsDict - -Bases: [`algokit_utils.deploy.DeployCallArgsDict`](#algokit_utils.DeployCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployResponse - -Describes the action taken during deployment, related transactions and the [`AppMetaData`](#algokit_utils.AppMetaData) - -### *class* algokit_utils.EnsureBalanceParameters - -Parameters for ensuring an account has a minimum number of µALGOs - -#### account_to_fund *: [algokit_utils.models.Account](#algokit_utils.Account) | [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -The account address that will receive the µALGOs - -#### fee_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call - -#### funding_source *: [algokit_utils.models.Account](#algokit_utils.Account) | [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner) | [algokit_utils.dispenser_api.TestNetDispenserApiClient](#algokit_utils.TestNetDispenserApiClient) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -The account (with private key) or signer that will send the µALGOs, -will use `get_dispenser_account` by default. Alternatively you can pass an instance of [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) -which will allow you to interact with [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md). - -#### max_fee_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional)The maximum fee that you are happy to pay (default: unbounded) - -if this is set it’s possible the transaction could get rejected during network congestion - -#### min_funding_increment_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int)* - -0 - -When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets -called often on an active account) - -#### min_spending_balance_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int)* - -None - -The minimum balance of ALGOs that the account should have available to spend (i.e. on top of -minimum balance requirement) - -#### note *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -The (optional) transaction note, default: “Funding account to meet minimum requirement - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional) transaction parameters - -### *class* algokit_utils.EnsureFundedResponse - -Response for ensuring an account has a minimum number of µALGOs - -#### transaction_id *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -The amount of µALGOs that were funded - -### *class* algokit_utils.MethodHints - -MethodHints provides hints to the caller about how to call the method - -### *class* algokit_utils.OnCompleteCallParameters - -Bases: [`algokit_utils.models.TransactionParameters`](#algokit_utils.TransactionParameters) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.call/compose_call methods - -### *class* algokit_utils.OnCompleteCallParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), [`algokit_utils.models.TransactionParametersDict`](#algokit_utils.TransactionParametersDict) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.call/compose_call methods - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.OnSchemaBreak(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Action to take if an Application’s schema has breaking changes - -### Initialization - -#### AppendApp - -3 - -Create a new Application - -#### Fail - -0 - -Fail the deployment - -#### ReplaceApp - -2 - -Create a new Application and delete the old Application in a single transaction - -### *class* algokit_utils.OnUpdate(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Action to take if an Application has been updated - -### Initialization - -#### AppendApp - -3 - -Create a new application - -#### Fail - -0 - -Fail the deployment - -#### ReplaceApp - -2 - -Create a new Application and delete the old Application in a single transaction - -#### UpdateApp - -1 - -Update the Application with the new approval and clear programs - -### *class* algokit_utils.OperationPerformed(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Describes the actions taken during deployment - -### Initialization - -#### Create - -1 - -No existing Application was found, created a new Application - -#### Nothing - -0 - -An existing Application was found - -#### Replace - -3 - -An existing Application was found, but was out of date, created a new Application and deleted the original - -#### Update - -2 - -An existing Application was found, but was out of date, updated to latest version - -### *class* algokit_utils.Program(program: [str](https://docs.python.org/3/library/stdtypes.html#str), client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) - -A compiled TEAL program - -### Initialization - -Fully compile the program source to binary and generate a -source map for matching pc to line number - -### *class* algokit_utils.TestNetDispenserApiClient(auth_token: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, request_timeout: [int](https://docs.python.org/3/library/functions.html#int) = DISPENSER_REQUEST_TIMEOUT) - -Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). -To get started create a new access token via `algokit dispenser login --ci` -and pass it to the client constructor as `auth_token`. -Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, -and it will be auto loaded. If both are set, the constructor argument takes precedence. - -Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. - -### Initialization - -#### fund(address: [str](https://docs.python.org/3/library/stdtypes.html#str), amount: [int](https://docs.python.org/3/library/functions.html#int), asset_id: [int](https://docs.python.org/3/library/functions.html#int)) → algokit_utils.dispenser_api.DispenserFundResponse - -Fund an account with Algos from the dispenser API - -#### get_limit(address: [str](https://docs.python.org/3/library/stdtypes.html#str)) → algokit_utils.dispenser_api.DispenserLimitResponse - -Get current limit for an account with Algos from the dispenser API - -#### refund(refund_txn_id: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [None](https://docs.python.org/3/library/constants.html#None) - -Register a refund for a transaction with the dispenser API - -### *class* algokit_utils.TransactionParameters - -Additional parameters that can be included in a transaction - -#### accounts *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[str](https://docs.python.org/3/library/stdtypes.html#str)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Accounts to include in transaction - -#### boxes *: [collections.abc.Sequence](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [bytearray](https://docs.python.org/3/library/stdtypes.html#bytearray) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)]] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Box references to include in transaction. A sequence of (app id, box key) tuples - -#### foreign_apps *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -List of foreign apps (by app id) to include in transaction - -#### foreign_assets *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -List of foreign assets (by asset id) to include in transaction - -#### lease *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Lease value for this transaction - -#### note *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Note for this transaction - -#### rekey_to *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Address to rekey to - -#### sender *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Sender of this transaction - -#### signer *: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Signer to use when signing this transaction - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -SuggestedParams to use for this transaction - -### *class* algokit_utils.TransactionParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Additional parameters that can be included in a transaction - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -#### accounts *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[str](https://docs.python.org/3/library/stdtypes.html#str)]* - -None - -Accounts to include in transaction - -#### boxes *: [collections.abc.Sequence](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [bytearray](https://docs.python.org/3/library/stdtypes.html#bytearray) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)]]* - -None - -Box references to include in transaction. A sequence of (app id, box key) tuples - -#### foreign_apps *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]* - -None - -List of foreign apps (by app id) to include in transaction - -#### foreign_assets *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]* - -None - -List of foreign assets (by asset id) to include in transaction - -#### lease *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Lease value for this transaction - -#### note *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Note for this transaction - -#### rekey_to *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Address to rekey to - -#### sender *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Sender of this transaction - -#### signer *: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner)* - -None - -Signer to use when signing this transaction - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams)* - -None - -SuggestedParams to use for this transaction - -### *class* algokit_utils.TransactionResponse - -Response for a non ABI call - -#### confirmed_round *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Round transaction was confirmed, `None` if call was a from a dry-run - -#### *static* from_atr(result: [algosdk.atomic_transaction_composer.AtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionResponse) | [algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse), transaction_index: [int](https://docs.python.org/3/library/functions.html#int) = -1) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) - -Returns either an ABITransactionResponse or a TransactionResponse based on the type of the transaction -referred to by transaction_index - -* **Parameters:** - * **result** (*AtomicTransactionResponse*) – Result containing one or more transactions - * **transaction_index** ([*int*](https://docs.python.org/3/library/functions.html#int)) – Which transaction in the result to return, defaults to -1 (the last transaction) - -#### tx_id *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Transaction Id - -### *class* algokit_utils.TransferAssetParameters - -Bases: `algokit_utils._transfer.TransferParametersBase` - -Parameters for transferring assets between accounts - -Args: -asset_id (int): The asset id that will be transfered -amount (int): The amount to send -clawback_from (str | None): An address of a target account from which to perform a clawback operation. Please -note, in such cases senderAccount must be equal to clawback field on ASA metadata. - -### *class* algokit_utils.TransferParameters - -Bases: `algokit_utils._transfer.TransferParametersBase` - -Parameters for transferring µALGOs between accounts - -## Functions - -### algokit_utils.create_kmd_wallet_account(kmd_client: [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Creates a wallet with specified name - -### algokit_utils.ensure_funded(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._ensure_funded.EnsureBalanceParameters](#algokit_utils.EnsureBalanceParameters)) → [algokit_utils._ensure_funded.EnsureFundedResponse](#algokit_utils.EnsureFundedResponse) | [None](https://docs.python.org/3/library/constants.html#None) - -Funds a given account using a funding source such that it has a certain amount of algos free to spend -(accounting for ALGOs locked in minimum balance requirement) -see [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) - -Args: -client (AlgodClient): An instance of the AlgodClient class from the AlgoSDK library. -parameters (EnsureBalanceParameters): An instance of the EnsureBalanceParameters class that -specifies the account to fund and the minimum spending balance. - -Returns: -PaymentTxn | str | None: If funds are needed, the function returns a payment transaction or a -string indicating that the dispenser API was used. If no funds are needed, the function returns None. - -### algokit_utils.execute_atc_with_logic_error(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), approval_program: [str](https://docs.python.org/3/library/stdtypes.html#str), wait_rounds: [int](https://docs.python.org/3/library/functions.html#int) = 4, approval_source_map: [algosdk.source_map.SourceMap](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/source_map.html#algosdk.source_map.SourceMap) | [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [algosdk.source_map.SourceMap](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/source_map.html#algosdk.source_map.SourceMap) | [None](https://docs.python.org/3/library/constants.html#None)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.atomic_transaction_composer.AtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionResponse) - -Calls `AtomicTransactionComposer.execute()` on provided `atc`, but will parse any errors -and raise a `LogicError` if possible - -#### NOTE -`approval_program` and `approval_source_map` are required to be able to parse any errors into a -`LogicError` - -### algokit_utils.get_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), fund_with_algos: [float](https://docs.python.org/3/library/functions.html#float) = 1000, kmd_client: KMDClient | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns an Algorand account with private key loaded by convention based on the given name identifier. - -### Convention - -**Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret -Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a -secret storage service rather than the file system. - -**LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn’t exist it will -create it and fund the account for you - -This allows you to write code that will work seamlessly in production and local development (LocalNet) without -manual config locally (including when you reset the LocalNet). - -### Example - -If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get -that private key loaded into an account object: - -```python -account = get_account('ACCOUNT', algod) -``` - -If that code runs against LocalNet then a wallet called ‘ACCOUNT’ will automatically be created with an account -that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. - -### algokit_utils.get_account_from_mnemonic(mnemonic: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Convert a mnemonic (25 word passphrase) into an Account - -### algokit_utils.get_algod_client(config: [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) - -Returns an [`algosdk.v2client.algod.AlgodClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) from `config` or environment - -If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN` - -### algokit_utils.get_app_id_from_tx_id(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), tx_id: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [int](https://docs.python.org/3/library/functions.html#int) - -Finds the app_id for provided transaction id - -### algokit_utils.get_creator_apps(indexer: [algosdk.v2client.indexer.IndexerClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient), creator_account: [algokit_utils.models.Account](#algokit_utils.Account) | [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.deploy.AppLookup](#algokit_utils.AppLookup) - -Returns a mapping of Application names to [`AppMetaData`](#algokit_utils.AppMetaData) for all Applications created by specified -creator that have a transaction note containing [`AppDeployMetaData`](#algokit_utils.AppDeployMetaData) - -### algokit_utils.get_default_localnet_config(config: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)[algod, indexer, kmd]) → [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) - -Returns the client configuration to point to the default LocalNet - -### algokit_utils.get_dispenser_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet - -### algokit_utils.get_indexer_client(config: [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.v2client.indexer.IndexerClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) - -Returns an [`algosdk.v2client.indexer.IndexerClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) from `config` or environment. - -If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN` - -### algokit_utils.get_kmd_client_from_algod_client(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient) - -Returns an [`algosdk.kmd.KMDClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient) from supplied `client` - -Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, -or 4002 by default - -### algokit_utils.get_kmd_wallet_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), kmd_client: [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), predicate: Callable[[[dict](https://docs.python.org/3/library/stdtypes.html#dict)[[str](https://docs.python.org/3/library/stdtypes.html#str), Any]], [bool](https://docs.python.org/3/library/functions.html#bool)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) - -Returns wallet matching specified name and predicate or None if not found - -### algokit_utils.get_localnet_default_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns the default Account in a LocalNet instance - -### algokit_utils.get_next_version(current_version: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [str](https://docs.python.org/3/library/stdtypes.html#str) - -Calculates the next version from `current_version` - -Next version is calculated by finding a semver like -version string and incrementing the lower. This function is used by [`ApplicationClient.deploy()`](#algokit_utils.ApplicationClient.deploy) when -a version is not specified, and is intended mostly for convenience during local development. - -* **Params str current_version:** - An existing version string with a semver like version contained within it, - some valid inputs and incremented outputs: - `1` -> `2` - `1.0` -> `1.1` - `v1.1` -> `v1.2` - `v1.1-beta1` -> `v1.2-beta1` - `v1.2.3.4567` -> `v1.2.3.4568` - `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` -* **Raises:** - **DeploymentFailedError** – If `current_version` cannot be parsed - -### algokit_utils.get_or_create_kmd_wallet_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), fund_with_algos: [float](https://docs.python.org/3/library/functions.html#float) = 1000, kmd_client: KMDClient | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns a wallet with specified name, or creates one if not found - -### algokit_utils.get_sender_from_signer(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None)) → [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) - -Returns the associated address of a signer, return None if no address found - -### algokit_utils.is_localnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `devnet-v1` or `sandnet-v1` - -### algokit_utils.is_mainnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `mainnet-v1` - -### algokit_utils.is_testnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `testnet-v1` - -### algokit_utils.num_extra_program_pages(approval: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes), clear: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)) → [int](https://docs.python.org/3/library/functions.html#int) - -Calculate minimum number of extra_pages required for provided approval and clear programs - -### algokit_utils.opt_in(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), account: [algokit_utils.models.Account](#algokit_utils.Account), asset_ids: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[int](https://docs.python.org/3/library/functions.html#int), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, -it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases -its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). - -Args: -algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. -account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. -asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. -Returns: -dict[int, str]: A dictionary where the keys are the asset IDs and the values -are the transaction IDs for opting-in to each asset. - -### algokit_utils.opt_out(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), account: [algokit_utils.models.Account](#algokit_utils.Account), asset_ids: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[int](https://docs.python.org/3/library/functions.html#int), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. -The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) -The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. - -It’s essential to note that an account can only opt_out of an asset if its balance of that asset is zero. - -Args: -algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. -account (Account): An instance of the Account class that holds the private key and address for an account. -asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. -Returns: -dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of -the executed transactions. - -### algokit_utils.persist_sourcemaps(\*, sources: [list](https://docs.python.org/3/library/stdtypes.html#list)[algokit_utils._debugging.PersistSourceMapInput], project_root: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), with_sources: [bool](https://docs.python.org/3/library/functions.html#bool) = True, persist_mappings: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [None](https://docs.python.org/3/library/constants.html#None) - -Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts. -Args: -sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects. -project_root (Path): The root directory of the project. -client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. -with_sources (bool): If True, it will dump teal source files along with sourcemaps. -Default is True, as needed by an AlgoKit AVM debugger. -persist_mappings (bool): Enables legacy behavior of persisting the `sources.avm.json` mappings to -the project root. Default is False, given that the AlgoKit AVM VSCode extension will manage the mappings. - -### algokit_utils.replace_template_variables(program: [str](https://docs.python.org/3/library/stdtypes.html#str), template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping)) → [str](https://docs.python.org/3/library/stdtypes.html#str) - -Replaces `TMPL_*` variables in `program` with `template_values` - -#### NOTE -`template_values` keys should *NOT* be prefixed with `TMPL_` - -### algokit_utils.simulate_and_persist_response(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), project_root: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), buffer_size_mb: [float](https://docs.python.org/3/library/functions.html#float) = 256) → [algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse) - -Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, -and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. - -* **Parameters:** - * **atc** – An `AtomicTransactionComposer` object representing the atomic transactions to be - simulated and persisted. - * **project_root** – A `Path` object representing the root directory of the project. - * **algod_client** – An `AlgodClient` object representing the Algorand client. - * **buffer_size_mb** – The size of the trace buffer in megabytes. Defaults to 256mb. -* **Returns:** - None - -Returns: -SimulateAtomicTransactionResponse: The simulated response after persisting it -for AlgoKit AVM Debugger consumption. - -### algokit_utils.transfer(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._transfer.TransferParameters](#algokit_utils.TransferParameters)) → [algosdk.transaction.PaymentTxn](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.PaymentTxn) - -Transfer µALGOs between accounts - -### algokit_utils.transfer_asset(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._transfer.TransferAssetParameters](#algokit_utils.TransferAssetParameters)) → [algosdk.transaction.AssetTransferTxn](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.AssetTransferTxn) - -Transfer assets between accounts diff --git a/docs/markdown/autoapi/account_manager/index.md b/docs/markdown/autoapi/account_manager/index.md new file mode 100644 index 00000000..afa7273e --- /dev/null +++ b/docs/markdown/autoapi/account_manager/index.md @@ -0,0 +1 @@ +# account_manager diff --git a/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md new file mode 100644 index 00000000..f3707775 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md @@ -0,0 +1,603 @@ +# algokit_utils.accounts.account_manager + +## Classes + +| [`EnsureFundedResult`](#algokit_utils.accounts.account_manager.EnsureFundedResult) | Result from performing an ensure funded call. | +|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [`EnsureFundedFromTestnetDispenserApiResult`](#algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult) | Result from performing an ensure funded call using TestNet dispenser API. | +| [`AccountInformation`](#algokit_utils.accounts.account_manager.AccountInformation) | Information about an Algorand account's current status, balance and other properties. | +| [`AccountManager`](#algokit_utils.accounts.account_manager.AccountManager) | Creates and keeps track of signing accounts that can sign transactions for a sending address. | + +## Module Contents + +### *class* algokit_utils.accounts.account_manager.EnsureFundedResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendSingleTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `_CommonEnsureFundedParams` + +Result from performing an ensure funded call. + +### *class* algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult + +Bases: `_CommonEnsureFundedParams` + +Result from performing an ensure funded call using TestNet dispenser API. + +### *class* algokit_utils.accounts.account_manager.AccountInformation + +Information about an Algorand account’s current status, balance and other properties. + +See https://developer.algorand.org/docs/rest-apis/algod/#account for detailed field descriptions. + +* **Variables:** + * **address** (*str*) – The account’s address + * **amount** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s current balance + * **amount_without_pending_rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s balance without the pending rewards + * **min_balance** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s minimum required balance + * **pending_rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The amount of pending rewards + * **rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The amount of rewards earned + * **round** (*int*) – The round for which this information is relevant + * **status** (*str*) – The account’s status (e.g., ‘Offline’, ‘Online’) + * **total_apps_opted_in** (*int* *|**None*) – Number of applications this account has opted into + * **total_assets_opted_in** (*int* *|**None*) – Number of assets this account has opted into + * **total_box_bytes** (*int* *|**None*) – Total number of box bytes used by this account + * **total_boxes** (*int* *|**None*) – Total number of boxes used by this account + * **total_created_apps** (*int* *|**None*) – Number of applications created by this account + * **total_created_assets** (*int* *|**None*) – Number of assets created by this account + * **apps_local_state** (*list* *[**dict* *]* *|**None*) – Local state of applications this account has opted into + * **apps_total_extra_pages** (*int* *|**None*) – Number of extra pages allocated to applications + * **apps_total_schema** (*dict* *|**None*) – Total schema for all applications + * **assets** (*list* *[**dict* *]* *|**None*) – Assets held by this account + * **auth_addr** (*str* *|**None*) – If rekeyed, the authorized address + * **closed_at_round** (*int* *|**None*) – Round when this account was closed + * **created_apps** (*list* *[**dict* *]* *|**None*) – Applications created by this account + * **created_assets** (*list* *[**dict* *]* *|**None*) – Assets created by this account + * **created_at_round** (*int* *|**None*) – Round when this account was created + * **deleted** (*bool* *|**None*) – Whether this account is deleted + * **incentive_eligible** (*bool* *|**None*) – Whether this account is eligible for incentives + * **last_heartbeat** (*int* *|**None*) – Last heartbeat round for this account + * **last_proposed** (*int* *|**None*) – Last round this account proposed a block + * **participation** (*dict* *|**None*) – Participation information for this account + * **reward_base** (*int* *|**None*) – Base reward for this account + * **sig_type** (*str* *|**None*) – Signature type for this account + +#### address *: str* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### amount_without_pending_rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### min_balance *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### pending_rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### round *: int* + +#### status *: str* + +#### total_apps_opted_in *: int | None* *= None* + +#### total_assets_opted_in *: int | None* *= None* + +#### total_box_bytes *: int | None* *= None* + +#### total_boxes *: int | None* *= None* + +#### total_created_apps *: int | None* *= None* + +#### total_created_assets *: int | None* *= None* + +#### apps_local_state *: list[dict] | None* *= None* + +#### apps_total_extra_pages *: int | None* *= None* + +#### apps_total_schema *: dict | None* *= None* + +#### assets *: list[dict] | None* *= None* + +#### auth_addr *: str | None* *= None* + +#### closed_at_round *: int | None* *= None* + +#### created_apps *: list[dict] | None* *= None* + +#### created_assets *: list[dict] | None* *= None* + +#### created_at_round *: int | None* *= None* + +#### deleted *: bool | None* *= None* + +#### incentive_eligible *: bool | None* *= None* + +#### last_heartbeat *: int | None* *= None* + +#### last_proposed *: int | None* *= None* + +#### participation *: dict | None* *= None* + +#### reward_base *: int | None* *= None* + +#### sig_type *: str | None* *= None* + +### *class* algokit_utils.accounts.account_manager.AccountManager(client_manager: [algokit_utils.clients.client_manager.ClientManager](../../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)) + +Creates and keeps track of signing accounts that can sign transactions for a sending address. + +This class provides functionality to create, track, and manage various types of accounts including +mnemonic-based, rekeyed, multisig, and logic signature accounts. + +* **Parameters:** + **client_manager** – The ClientManager client to use for algod and kmd clients +* **Example:** + +```pycon +>>> account_manager = AccountManager(client_manager) +``` + +#### *property* kmd *: [algokit_utils.accounts.kmd_account_manager.KmdAccountManager](../kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager)* + +#### set_default_signer(signer: algosdk.atomic_transaction_composer.TransactionSigner | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +If this isn’t set and a transaction needs signing for a given sender +then an error will be thrown from get_signer / get_account. + +* **Parameters:** + **signer** – A TransactionSigner signer to use. +* **Returns:** + The AccountManager so method calls can be chained +* **Example:** + +```pycon +>>> signer_account = account_manager.random() +>>> account_manager.set_default_signer(signer_account.signer) +>>> # When signing a transaction, if there is no signer registered for the sender +>>> # then the default signer will be used +>>> signer = account_manager.get_signer("{SENDERADDRESS}") +``` + +#### set_signer(sender: str, signer: algosdk.atomic_transaction_composer.TransactionSigner) → typing_extensions.Self + +Tracks the given TransactionSigner against the given sender address for later signing. + +* **Parameters:** + * **sender** – The sender address to use this signer for + * **signer** – The TransactionSigner to sign transactions with for the given sender +* **Returns:** + The AccountManager instance for method chaining +* **Example:** + +```pycon +>>> account_manager.set_signer("SENDERADDRESS", transaction_signer) +``` + +#### set_signers(\*, another_account_manager: [AccountManager](#algokit_utils.accounts.account_manager.AccountManager), overwrite_existing: bool = True) → typing_extensions.Self + +Merges the given AccountManager into this one. + +* **Parameters:** + * **another_account_manager** – The AccountManager to merge into this one + * **overwrite_existing** – Whether to overwrite existing signers in this manager +* **Returns:** + The AccountManager instance for method chaining + +#### set_signer_from_account(account: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Tracks the given account for later signing. + +Note: If you are generating accounts via the various methods on AccountManager +(like random, from_mnemonic, logic_sig, etc.) then they automatically get tracked. + +* **Parameters:** + **account** – The account to register +* **Returns:** + The AccountManager instance for method chaining +* **Example:** + +```pycon +>>> account_manager = AccountManager(client_manager) +>>> account_manager.set_signer_from_account(SigningAccount.new_account()) +>>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) +>>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) +``` + +#### get_signer(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → algosdk.atomic_transaction_composer.TransactionSigner + +Returns the TransactionSigner for the given sender address. + +If no signer has been registered for that address then the default signer is used if registered. + +* **Parameters:** + **sender** – The sender address or account +* **Returns:** + The TransactionSigner +* **Raises:** + **ValueError** – If no signer is found and no default signer is set +* **Example:** + +```pycon +>>> signer = account_manager.get_signer("SENDERADDRESS") +``` + +#### get_account(sender: str) → [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol) + +Returns the TransactionSignerAccountProtocol for the given sender address. + +* **Parameters:** + **sender** – The sender address +* **Returns:** + The TransactionSignerAccountProtocol +* **Raises:** + **ValueError** – If no account is found or if the account is not a regular account +* **Example:** + +```pycon +>>> sender = account_manager.random() +>>> # ... +>>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered +>>> account = account_manager.get_account(sender) +``` + +#### get_information(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [AccountInformation](#algokit_utils.accounts.account_manager.AccountInformation) + +Returns the given sender account’s current status, balance and spendable amounts. + +See [https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress](https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress) +for response data schema details. + +* **Parameters:** + **sender** – The address or account compliant with TransactionSignerAccountProtocol protocol to look up +* **Returns:** + The account information +* **Example:** + +```pycon +>>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +>>> account_info = account_manager.get_information(address) +``` + +#### from_mnemonic(\*, mnemonic: str, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. + +* **Parameters:** + * **mnemonic** – The mnemonic secret representing the private key of an account + * **sender** – Optional address to use as the sender +* **Returns:** + The account + +#### WARNING +Be careful how the mnemonic is handled. Never commit it into source control and ideally load it +from the environment (ideally via a secret storage service) rather than the file system. + +* **Example:** + +```pycon +>>> account = account_manager.from_mnemonic("mnemonic secret ...") +``` + +#### from_environment(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with private key loaded by convention from environment variables. + +This allows you to write code that will work seamlessly in production and local development (LocalNet) +without manual config locally (including when you reset the LocalNet). + +* **Parameters:** + * **name** – The name identifier of the account + * **fund_with** – Optional amount to fund the account with when it gets created + +(when targeting LocalNet) +:returns: The account +:raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} + +#### NOTE +Convention: +: * **Non-LocalNet:** will load {NAME}_MNEMONIC as a mnemonic secret. + If {NAME}_SENDER is defined then it will use that for the sender address + (i.e. to support rekeyed accounts) + * **LocalNet:** will load the account from a KMD wallet called {NAME} and if that wallet doesn’t exist + it will create it and fund the account for you + +* **Example:** + +```pycon +>>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: +>>> account = account_manager.from_environment('MY_ACCOUNT') +>>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created +>>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser +``` + +#### from_kmd(name: str, predicate: collections.abc.Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with private key loaded from the given KMD wallet. + +* **Parameters:** + * **name** – The name of the wallet to retrieve an account from + * **predicate** – Optional filter to use to find the account + * **sender** – Optional sender address to use this signer for (aka a rekeyed account) +* **Returns:** + The account +* **Raises:** + **ValueError** – If unable to find KMD account with given name and predicate +* **Example:** + +```pycon +>>> # Get default funded account in a LocalNet: +>>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', +... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 +... ) +``` + +#### logicsig(program: bytes, args: list[bytes] | None = None) → algokit_utils.models.account.LogicSigAccount + +Tracks and returns an account that represents a logic signature. + +* **Parameters:** + * **program** – The bytes that make up the compiled logic signature + * **args** – Optional (binary) arguments to pass into the logic signature +* **Returns:** + A logic signature account wrapper +* **Example:** + +```pycon +>>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) +``` + +#### multisig(metadata: [algokit_utils.models.account.MultisigMetadata](../../models/account/index.md#algokit_utils.models.account.MultisigMetadata), signing_accounts: list[[algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount)]) → [algokit_utils.models.account.MultiSigAccount](../../models/account/index.md#algokit_utils.models.account.MultiSigAccount) + +Tracks and returns an account that supports partial or full multisig signing. + +* **Parameters:** + * **metadata** – The metadata for the multisig account + * **signing_accounts** – The signers that are currently present +* **Returns:** + A multisig account wrapper +* **Example:** + +```pycon +>>> account = account_manager.multi_sig( +... version=1, +... threshold=1, +... addrs=["ADDRESS1...", "ADDRESS2..."], +... signing_accounts=[account1, account2] +... ) +``` + +#### random() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns a new, random Algorand account. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.random() +``` + +#### localnet_dispenser() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + +This account can be used to fund other accounts. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.localnet_dispenser() +``` + +#### dispenser_from_environment() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Returns an account (with private key loaded) that can act as a dispenser from environment variables. + +If environment variables are not present, returns the default LocalNet dispenser account. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.dispenser_from_environment() +``` + +#### rekeyed(\*, sender: str, account: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [algokit_utils.models.account.TransactionSignerAccount](../../models/account/index.md#algokit_utils.models.account.TransactionSignerAccount) | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. + +* **Parameters:** + * **sender** – The account or address to use as the sender + * **account** – The account to use as the signer for this new rekeyed account +* **Returns:** + The rekeyed account +* **Example:** + +```pycon +>>> account = account.from_mnemonic("mnemonic secret ...") +>>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") +``` + +#### rekey_account(account: str, rekey_to: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol), \*, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, suppress_log: bool | None = None) → [algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Rekey an account to a new address. + +* **Parameters:** + * **account** – The account to rekey + * **rekey_to** – The address or account to rekey to + * **signer** – Optional transaction signer + * **note** – Optional transaction note + * **lease** – Optional transaction lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional max fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **suppress_log** – Optional flag to suppress logging +* **Returns:** + The result of the transaction and the transaction that was sent + +#### WARNING +Please be careful with this function and be sure to read the +[official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +* **Example:** + +```pycon +>>> # Basic example (with string addresses): +>>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) +>>> # Basic example (with signer accounts): +>>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) +>>> # Advanced example: +>>> algorand.account.rekey_account({ +... account: "ACCOUNTADDRESS", +... rekey_to: "NEWADDRESS", +... lease: 'lease', +... note: 'note', +... first_valid_round: 1000, +... validity_window: 10, +... extra_fee: AlgoAmount.from_micro_algo(1000), +... static_fee: AlgoAmount.from_micro_algo(1000), +... max_fee: AlgoAmount.from_micro_algo(3000), +... suppress_log: True, +... }) +``` + +#### ensure_funded(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_account: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None + +Funds a given account using a dispenser account as a funding source. + +Ensures the given account has a certain amount of Algo free to spend (accounting for +Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **dispenser_account** – The account to use as a dispenser funding source + * **min_spending_balance** – The minimum balance of Algo that the account + +should have available to spend +:param min_funding_increment: Optional minimum funding increment +:param send_params: Parameters for the send operation, defaults to None +:param signer: Optional transaction signer +:param rekey_to: Optional rekey address +:param note: Optional transaction note +:param lease: Optional transaction lease +:param static_fee: Optional static fee +:param extra_fee: Optional extra fee +:param max_fee: Optional maximum fee +:param validity_window: Optional validity window +:param first_valid_round: Optional first valid round +:param last_valid_round: Optional last valid round +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, +or None if no funds were needed + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) +>>> # With configuration: +>>> algorand.account.ensure_funded( +... "ACCOUNTADDRESS", +... "DISPENSERADDRESS", +... algokit.algo(1), +... min_funding_increment=algokit.algo(2), +... fee=AlgoAmount.from_micro_algo(1000), +... suppress_log=True +... ) +``` + +#### ensure_funded_from_environment(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None + +Ensure an account is funded from a dispenser account configured in environment. + +Uses a dispenser account retrieved from the environment, per the dispenser_from_environment method, +as a funding source such that the given account has a certain amount of Algo free to spend +(accounting for Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **min_spending_balance** – The minimum balance of Algo that the account should have available to + +spend +:param min_funding_increment: Optional minimum funding increment +:param send_params: Parameters for the send operation, defaults to None +:param signer: Optional transaction signer +:param rekey_to: Optional rekey address +:param note: Optional transaction note +:param lease: Optional transaction lease +:param static_fee: Optional static fee +:param extra_fee: Optional extra fee +:param max_fee: Optional maximum fee +:param validity_window: Optional validity window +:param first_valid_round: Optional first valid round +:param last_valid_round: Optional last valid round +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or +None if no funds were needed + +#### NOTE +The dispenser account is retrieved from the account mnemonic stored in +process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER +if it’s a rekeyed account, or against default LocalNet if no environment variables present. + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) +>>> # With configuration: +>>> algorand.account.ensure_funded_from_environment( +... "ACCOUNTADDRESS", +... algokit.algo(1), +... min_funding_increment=algokit.algo(2), +... fee=AlgoAmount.from_micro_algo(1000), +... suppress_log=True +... ) +``` + +#### ensure_funded_from_testnet_dispenser_api(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_client: [algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient](../../clients/dispenser_api_client/index.md#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [EnsureFundedFromTestnetDispenserApiResult](#algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult) | None + +Ensure an account is funded using the TestNet Dispenser API. + +Uses the TestNet Dispenser API as a funding source such that the account has a certain amount +of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **dispenser_client** – The TestNet dispenser funding client + * **min_spending_balance** – The minimum balance of Algo that the account should have + +available to spend +:param min_funding_increment: Optional minimum funding increment +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or +None if no funds were needed +:raises ValueError: If attempting to fund on non-TestNet network + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded_from_testnet_dispenser_api( +... "ACCOUNTADDRESS", +... algorand.client.get_testnet_dispenser_from_environment(), +... algokit.algo(1) +... ) +>>> # With configuration: +>>> algorand.account.ensure_funded_from_testnet_dispenser_api( +... "ACCOUNTADDRESS", +... algorand.client.get_testnet_dispenser_from_environment(), +... algokit.algo(1), +... min_funding_increment=algokit.algo(2) +... ) +``` diff --git a/docs/markdown/autoapi/algokit_utils/accounts/index.md b/docs/markdown/autoapi/algokit_utils/accounts/index.md new file mode 100644 index 00000000..97b69c7e --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/index.md @@ -0,0 +1,6 @@ +# algokit_utils.accounts + +## Submodules + +* [algokit_utils.accounts.account_manager](account_manager/index.md) +* [algokit_utils.accounts.kmd_account_manager](kmd_account_manager/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md new file mode 100644 index 00000000..5041cc9b --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md @@ -0,0 +1,71 @@ +# algokit_utils.accounts.kmd_account_manager + +## Classes + +| [`KmdAccount`](#algokit_utils.accounts.kmd_account_manager.KmdAccount) | Account retrieved from KMD with signing capabilities, extending base Account. | +|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [`KmdAccountManager`](#algokit_utils.accounts.kmd_account_manager.KmdAccountManager) | Provides abstractions over KMD that makes it easier to get and manage accounts. | + +## Module Contents + +### *class* algokit_utils.accounts.kmd_account_manager.KmdAccount(private_key: str, address: str | None = None) + +Bases: [`algokit_utils.models.account.SigningAccount`](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Account retrieved from KMD with signing capabilities, extending base Account. + +Provides an account implementation that can be used to sign transactions using keys stored in KMD. + +* **Parameters:** + * **private_key** – Base64 encoded private key + * **address** – Optional address override for rekeyed accounts, defaults to None + +### *class* algokit_utils.accounts.kmd_account_manager.KmdAccountManager(client_manager: [algokit_utils.clients.client_manager.ClientManager](../../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)) + +Provides abstractions over KMD that makes it easier to get and manage accounts. + +#### kmd() → algosdk.kmd.KMDClient + +Returns the KMD client, initializing it if needed. + +* **Raises:** + **Exception** – If KMD client is not configured and not running against LocalNet +* **Returns:** + The KMD client + +#### get_wallet_account(wallet_name: str, predicate: collections.abc.Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) | None + +Returns an Algorand signing account with private key loaded from the given KMD wallet. + +Retrieves an account from a KMD wallet that matches the given predicate, or a random account +if no predicate is provided. + +* **Parameters:** + * **wallet_name** – The name of the wallet to retrieve an account from + * **predicate** – Optional filter to use to find the account (otherwise gets a random account from the wallet) + * **sender** – Optional sender address to use this signer for (aka a rekeyed account) +* **Returns:** + The signing account or None if no matching wallet or account was found + +#### get_or_create_wallet_account(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) + +Gets or creates a funded account in a KMD wallet of the given name. + +Provides idempotent access to accounts from LocalNet without specifying the private key. + +* **Parameters:** + * **name** – The name of the wallet to retrieve / create + * **fund_with** – The number of Algos to fund the account with when created +* **Returns:** + An Algorand account with private key loaded + +#### get_localnet_dispenser_account() → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) + +Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + +Retrieves the default funded account from LocalNet that can be used to fund other accounts. + +* **Raises:** + **Exception** – If not running against LocalNet or dispenser account not found +* **Returns:** + The default LocalNet dispenser account diff --git a/docs/markdown/autoapi/algokit_utils/algorand/index.md b/docs/markdown/autoapi/algokit_utils/algorand/index.md new file mode 100644 index 00000000..3e0f3115 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/algorand/index.md @@ -0,0 +1,156 @@ +# algokit_utils.algorand + +## Classes + +| [`AlgorandClient`](#algokit_utils.algorand.AlgorandClient) | A client that brokers easy access to Algorand functionality. | +|--------------------------------------------------------------|----------------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.algorand.AlgorandClient(config: [algokit_utils.models.network.AlgoClientConfigs](../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) | [algokit_utils.clients.client_manager.AlgoSdkClients](../clients/client_manager/index.md#algokit_utils.clients.client_manager.AlgoSdkClients)) + +A client that brokers easy access to Algorand functionality. + +#### set_default_validity_window(validity_window: int) → typing_extensions.Self + +Sets the default validity window for transactions. + +* **Parameters:** + **validity_window** – The number of rounds between the first and last valid rounds +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_default_signer(signer: algosdk.atomic_transaction_composer.TransactionSigner | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +* **Parameters:** + **signer** – The signer to use, either a TransactionSigner or a TransactionSignerAccountProtocol +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_signer(sender: str, signer: algosdk.atomic_transaction_composer.TransactionSigner) → typing_extensions.Self + +Tracks the given account for later signing. + +* **Parameters:** + * **sender** – The sender address to use this signer for + * **signer** – The signer to sign transactions with for the given sender +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_signer_account(signer: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +* **Parameters:** + **signer** – The signer to use, either a TransactionSigner or a TransactionSignerAccountProtocol +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_suggested_params(suggested_params: algosdk.transaction.SuggestedParams, until: float | None = None) → typing_extensions.Self + +Sets a cache value to use for suggested params. + +* **Parameters:** + * **suggested_params** – The suggested params to use + * **until** – A timestamp until which to cache, or if not specified then the timeout is used +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_suggested_params_timeout(timeout: int) → typing_extensions.Self + +Sets the timeout for caching suggested params. + +* **Parameters:** + **timeout** – The timeout in milliseconds +* **Returns:** + The AlgorandClient so method calls can be chained + +#### get_suggested_params() → algosdk.transaction.SuggestedParams + +Get suggested params for a transaction (either cached or from algod if the cache is stale or empty) + +#### new_group() → [algokit_utils.transactions.transaction_composer.TransactionComposer](../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Start a new TransactionComposer transaction group + +#### *property* client *: [algokit_utils.clients.client_manager.ClientManager](../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)* + +Get clients, including algosdk clients and app clients. + +#### *property* account *: [algokit_utils.accounts.account_manager.AccountManager](../accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager)* + +Get or create accounts that can sign transactions. + +#### *property* asset *: [algokit_utils.assets.asset_manager.AssetManager](../assets/asset_manager/index.md#algokit_utils.assets.asset_manager.AssetManager)* + +Get or create assets. + +#### *property* app *: [algokit_utils.applications.app_manager.AppManager](../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager)* + +#### *property* app_deployer *: [algokit_utils.applications.app_deployer.AppDeployer](../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployer)* + +Get or create applications. + +#### *property* send *: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender)* + +Methods for sending a transaction and waiting for confirmation + +#### *property* create_transaction *: [algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator](../transactions/transaction_creator/index.md#algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator)* + +Methods for building transactions + +#### *static* default_localnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at default LocalNet ports and API token. + +* **Returns:** + The AlgorandClient + +#### *static* testnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at TestNet using AlgoNode. + +* **Returns:** + The AlgorandClient + +#### *static* mainnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at MainNet using AlgoNode. + +* **Returns:** + The AlgorandClient + +#### *static* from_clients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing to the given client(s). + +* **Parameters:** + * **algod** – The algod client to use + * **indexer** – The indexer client to use + * **kmd** – The kmd client to use +* **Returns:** + The AlgorandClient + +#### *static* from_environment() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient loading the configuration from environment variables. + +Retrieve configurations from environment variables when defined or get defaults. + +Expects to be called from a Python environment. + +* **Returns:** + The AlgorandClient + +#### *static* from_config(algod_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig), indexer_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None, kmd_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient from the given config. + +* **Parameters:** + * **algod_config** – The config to use for the algod client + * **indexer_config** – The config to use for the indexer client + * **kmd_config** – The config to use for the kmd client +* **Returns:** + The AlgorandClient diff --git a/docs/markdown/autoapi/algokit_utils/applications/abi/index.md b/docs/markdown/autoapi/algokit_utils/applications/abi/index.md new file mode 100644 index 00000000..94e80d66 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/abi/index.md @@ -0,0 +1,161 @@ +# algokit_utils.applications.abi + +## Attributes + +| [`ABIValue`](#algokit_utils.applications.abi.ABIValue) | | +|--------------------------------------------------------------------------------|----| +| [`ABIStruct`](#algokit_utils.applications.abi.ABIStruct) | | +| [`Arc56ReturnValueType`](#algokit_utils.applications.abi.Arc56ReturnValueType) | | +| [`ABIType`](#algokit_utils.applications.abi.ABIType) | | +| [`ABIArgumentType`](#algokit_utils.applications.abi.ABIArgumentType) | | + +## Classes + +| [`ABIReturn`](#algokit_utils.applications.abi.ABIReturn) | Represents the return value from an ABI method call. | +|--------------------------------------------------------------|--------------------------------------------------------| +| [`BoxABIValue`](#algokit_utils.applications.abi.BoxABIValue) | Represents an ABI value stored in a box. | + +## Functions + +| [`get_arc56_value`](#algokit_utils.applications.abi.get_arc56_value)(→ Arc56ReturnValueType) | Gets the ARC-56 formatted return value from an ABI return. | +|---------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| [`get_abi_encoded_value`](#algokit_utils.applications.abi.get_abi_encoded_value)(→ bytes) | Encodes a value according to its ABI type. | +| [`get_abi_decoded_value`](#algokit_utils.applications.abi.get_abi_decoded_value)(→ ABIValue) | Decodes a value according to its ABI type. | +| [`get_abi_tuple_from_abi_struct`](#algokit_utils.applications.abi.get_abi_tuple_from_abi_struct)(→ list[Any]) | Converts an ABI struct to a tuple representation. | +| [`get_abi_tuple_type_from_abi_struct_definition`](#algokit_utils.applications.abi.get_abi_tuple_type_from_abi_struct_definition)(...) | Creates a TupleType from a struct definition. | +| [`get_abi_struct_from_abi_tuple`](#algokit_utils.applications.abi.get_abi_struct_from_abi_tuple)(→ dict[str, Any]) | Converts a decoded tuple to an ABI struct. | + +## Module Contents + +### algokit_utils.applications.abi.ABIValue *: TypeAlias* *= bool | int | str | bytes | bytearray | list['ABIValue'] | tuple['ABIValue'] | dict[str, 'ABIValue']* + +### algokit_utils.applications.abi.ABIStruct *: TypeAlias* *= dict[str, list[dict[str, 'ABIValue']]]* + +### algokit_utils.applications.abi.Arc56ReturnValueType *: TypeAlias* *= ABIValue | ABIStruct | None* + +### algokit_utils.applications.abi.ABIType *: TypeAlias* *= algosdk.abi.ABIType* + +### algokit_utils.applications.abi.ABIArgumentType *: TypeAlias* *= algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType* + +### *class* algokit_utils.applications.abi.ABIReturn(result: algosdk.atomic_transaction_composer.ABIResult) + +Represents the return value from an ABI method call. + +Wraps the raw return value and decoded value along with any decode errors. + +* **Variables:** + * **result** – The ABIResult object containing the method call results + * **raw_value** – The raw return value from the method call + * **value** – The decoded return value from the method call + * **method** – The ABI method definition + * **decode_error** – The exception that occurred during decoding, if any + +#### raw_value *: bytes | None* *= None* + +#### value *: ABIValue | None* *= None* + +#### method *: algosdk.abi.method.Method | None* *= None* + +#### decode_error *: Exception | None* *= None* + +#### *property* is_success *: bool* + +Returns True if the ABI call was successful (no decode error) + +* **Returns:** + True if no decode error occurred, False otherwise + +#### get_arc56_value(method: [algokit_utils.applications.app_spec.arc56.Method](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Method) | algosdk.abi.method.Method, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → Arc56ReturnValueType + +Gets the ARC-56 formatted return value. + +* **Parameters:** + * **method** – The ABI method definition + * **structs** – Dictionary of struct definitions +* **Returns:** + The decoded return value in ARC-56 format + +### algokit_utils.applications.abi.get_arc56_value(abi_return: [ABIReturn](#algokit_utils.applications.abi.ABIReturn), method: [algokit_utils.applications.app_spec.arc56.Method](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Method) | algosdk.abi.method.Method, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → Arc56ReturnValueType + +Gets the ARC-56 formatted return value from an ABI return. + +* **Parameters:** + * **abi_return** – The ABI return value to decode + * **method** – The ABI method definition + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If there was an error decoding the return value +* **Returns:** + The decoded return value in ARC-56 format + +### algokit_utils.applications.abi.get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → bytes + +Encodes a value according to its ABI type. + +* **Parameters:** + * **value** – The value to encode + * **type_str** – The ABI type string + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If the value cannot be encoded for the given type +* **Returns:** + The ABI encoded bytes + +### algokit_utils.applications.abi.get_abi_decoded_value(value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → ABIValue + +Decodes a value according to its ABI type. + +* **Parameters:** + * **value** – The value to decode + * **type_str** – The ABI type string or type object + * **structs** – Dictionary of struct definitions +* **Returns:** + The decoded ABI value + +### algokit_utils.applications.abi.get_abi_tuple_from_abi_struct(struct_value: dict[str, Any], struct_fields: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → list[Any] + +Converts an ABI struct to a tuple representation. + +* **Parameters:** + * **struct_value** – The struct value as a dictionary + * **struct_fields** – List of struct field definitions + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If a required field is missing from the struct +* **Returns:** + The struct as a tuple + +### algokit_utils.applications.abi.get_abi_tuple_type_from_abi_struct_definition(struct_def: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → algosdk.abi.TupleType + +Creates a TupleType from a struct definition. + +* **Parameters:** + * **struct_def** – The struct field definitions + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If a field type is invalid +* **Returns:** + The TupleType representing the struct + +### algokit_utils.applications.abi.get_abi_struct_from_abi_tuple(decoded_tuple: Any, struct_fields: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → dict[str, Any] + +Converts a decoded tuple to an ABI struct. + +* **Parameters:** + * **decoded_tuple** – The tuple to convert + * **struct_fields** – List of struct field definitions + * **structs** – Dictionary of struct definitions +* **Returns:** + The tuple as a struct dictionary + +### *class* algokit_utils.applications.abi.BoxABIValue + +Represents an ABI value stored in a box. + +* **Variables:** + * **name** – The name of the box + * **value** – The ABI value stored in the box + +#### name *: [algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)* + +#### value *: ABIValue* diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md new file mode 100644 index 00000000..7a135152 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md @@ -0,0 +1,578 @@ +# algokit_utils.applications.app_client + +## Attributes + +| [`CreateOnComplete`](#algokit_utils.applications.app_client.CreateOnComplete) | | +|---------------------------------------------------------------------------------|----| + +## Classes + +| [`AppClientCompilationResult`](#algokit_utils.applications.app_client.AppClientCompilationResult) | Result of compiling an application's TEAL code. | +|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| +| [`AppClientCompilationParams`](#algokit_utils.applications.app_client.AppClientCompilationParams) | Parameters for compiling an application's TEAL code. | +| [`FundAppAccountParams`](#algokit_utils.applications.app_client.FundAppAccountParams) | Parameters for funding an application's account. | +| [`AppClientCallParams`](#algokit_utils.applications.app_client.AppClientCallParams) | Parameters for calling an application. | +| [`BaseAppClientMethodCallParams`](#algokit_utils.applications.app_client.BaseAppClientMethodCallParams) | Base parameters for application method calls. | +| [`AppClientMethodCallParams`](#algokit_utils.applications.app_client.AppClientMethodCallParams) | Parameters for application method calls. | +| [`AppClientBareCallParams`](#algokit_utils.applications.app_client.AppClientBareCallParams) | Parameters for bare application calls. | +| [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema) | Schema for application creation. | +| [`AppClientBareCallCreateParams`](#algokit_utils.applications.app_client.AppClientBareCallCreateParams) | Parameters for creating application with bare call. | +| [`AppClientMethodCallCreateParams`](#algokit_utils.applications.app_client.AppClientMethodCallCreateParams) | Parameters for creating application with method call. | +| [`AppClientParams`](#algokit_utils.applications.app_client.AppClientParams) | Full parameters for creating an app client | +| [`AppClient`](#algokit_utils.applications.app_client.AppClient) | A client for interacting with an Algorand smart contract application. | + +## Functions + +| [`get_constant_block_offset`](#algokit_utils.applications.app_client.get_constant_block_offset)(→ int) | Calculate the offset after constant blocks in TEAL program. | +|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| + +## Module Contents + +### algokit_utils.applications.app_client.get_constant_block_offset(program: bytes) → int + +Calculate the offset after constant blocks in TEAL program. + +Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations. + +* **Parameters:** + **program** – The compiled TEAL program as bytes +* **Returns:** + The maximum offset position after any constant block operations + +### algokit_utils.applications.app_client.CreateOnComplete + +### *class* algokit_utils.applications.app_client.AppClientCompilationResult + +Result of compiling an application’s TEAL code. + +Contains the compiled approval and clear state programs along with optional compilation artifacts. + +* **Variables:** + * **approval_program** – The compiled approval program bytes + * **clear_state_program** – The compiled clear state program bytes + * **compiled_approval** – Optional compilation artifacts for approval program + * **compiled_clear** – Optional compilation artifacts for clear state program + +#### approval_program *: bytes* + +#### clear_state_program *: bytes* + +#### compiled_approval *: [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None* *= None* + +#### compiled_clear *: [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCompilationParams + +Bases: `TypedDict` + +Parameters for compiling an application’s TEAL code. + +* **Variables:** + * **deploy_time_params** – Optional template parameters to use during compilation + * **updatable** – Optional flag indicating if app should be updatable + * **deletable** – Optional flag indicating if app should be deletable + +#### deploy_time_params *: algokit_utils.models.state.TealTemplateParams | None* + +#### updatable *: bool | None* + +#### deletable *: bool | None* + +### *class* algokit_utils.applications.app_client.FundAppAccountParams + +Parameters for funding an application’s account. + +* **Variables:** + * **sender** – Optional sender address + * **signer** – Optional transaction signer + * **rekey_to** – Optional address to rekey to + * **note** – Optional transaction note + * **lease** – Optional lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window in rounds + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **amount** – Amount to fund + * **close_remainder_to** – Optional address to close remainder to + * **on_complete** – Optional on complete action + +#### sender *: str | None* *= None* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### note *: bytes | None* *= None* + +#### lease *: bytes | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### close_remainder_to *: str | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCallParams + +Parameters for calling an application. + +* **Variables:** + * **method** – Optional ABI method name or signature + * **args** – Optional arguments to pass to method + * **boxes** – Optional box references to load + * **accounts** – Optional account addresses to load + * **apps** – Optional app IDs to load + * **assets** – Optional asset IDs to load + * **lease** – Optional lease + * **sender** – Optional sender address + * **note** – Optional transaction note + * **send_params** – Optional parameters to control transaction sending + +#### method *: str | None* *= None* + +#### args *: list | None* *= None* + +#### boxes *: list | None* *= None* + +#### accounts *: list[str] | None* *= None* + +#### apps *: list[int] | None* *= None* + +#### assets *: list[int] | None* *= None* + +#### lease *: str | bytes | None* *= None* + +#### sender *: str | None* *= None* + +#### note *: bytes | dict | str | None* *= None* + +#### send_params *: dict | None* *= None* + +### *class* algokit_utils.applications.app_client.BaseAppClientMethodCallParams + +Bases: `Generic`[`ArgsT`, `MethodT`] + +Base parameters for application method calls. + +* **Variables:** + * **method** – Method to call + * **args** – Optional arguments to pass to method + * **account_references** – Optional account references + * **app_references** – Optional application references + * **asset_references** – Optional asset references + * **box_references** – Optional box references + * **extra_fee** – Optional extra fee + * **first_valid_round** – Optional first valid round + * **lease** – Optional lease + * **max_fee** – Optional maximum fee + * **note** – Optional note + * **rekey_to** – Optional rekey to address + * **sender** – Optional sender address + * **signer** – Optional transaction signer + * **static_fee** – Optional static fee + * **validity_window** – Optional validity window + * **last_valid_round** – Optional last valid round + * **on_complete** – Optional on complete action + +#### method *: MethodT* + +#### args *: ArgsT | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: collections.abc.Sequence[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### lease *: bytes | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### note *: bytes | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### sender *: str | None* *= None* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientMethodCallParams + +Bases: [`BaseAppClientMethodCallParams`](#algokit_utils.applications.app_client.BaseAppClientMethodCallParams)[`collections.abc.Sequence`[`algokit_utils.applications.abi.ABIValue | algokit_utils.applications.abi.ABIStruct | algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument | None`], `str`] + +Parameters for application method calls. + +### *class* algokit_utils.applications.app_client.AppClientBareCallParams + +Parameters for bare application calls. + +* **Variables:** + * **signer** – Optional transaction signer + * **rekey_to** – Optional rekey to address + * **lease** – Optional lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **sender** – Optional sender address + * **note** – Optional note + * **args** – Optional arguments + * **account_references** – Optional account references + * **app_references** – Optional application references + * **asset_references** – Optional asset references + * **box_references** – Optional box references + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### lease *: bytes | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### sender *: str | None* *= None* + +#### note *: bytes | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCreateSchema + +Schema for application creation. + +* **Variables:** + * **extra_program_pages** – Optional number of extra program pages + * **schema** – Optional application creation schema + +#### extra_program_pages *: int | None* *= None* + +#### schema *: [algokit_utils.transactions.transaction_composer.AppCreateSchema](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientBareCallCreateParams + +Bases: [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema), [`AppClientBareCallParams`](#algokit_utils.applications.app_client.AppClientBareCallParams) + +Parameters for creating application with bare call. + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientMethodCallCreateParams + +Bases: [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema), [`AppClientMethodCallParams`](#algokit_utils.applications.app_client.AppClientMethodCallParams) + +Parameters for creating application with method call. + +#### on_complete *: CreateOnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientParams + +Full parameters for creating an app client + +#### app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str* + +#### algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### app_id *: int* + +#### app_name *: str | None* *= None* + +#### default_sender *: str | None* *= None* + +#### default_signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### approval_source_map *: algosdk.source_map.SourceMap | None* *= None* + +#### clear_source_map *: algosdk.source_map.SourceMap | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClient(params: [AppClientParams](#algokit_utils.applications.app_client.AppClientParams)) + +A client for interacting with an Algorand smart contract application. + +Provides a high-level interface for interacting with Algorand smart contracts, including +methods for calling application methods, managing state, and handling transactions. + +* **Parameters:** + **params** – Parameters for creating the app client + +#### *property* algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +Get the Algorand client instance. + +* **Returns:** + The Algorand client used by this app client + +#### *property* app_id *: int* + +Get the application ID. + +* **Returns:** + The ID of the Algorand application + +#### *property* app_address *: str* + +Get the application’s Algorand address. + +* **Returns:** + The Algorand address associated with this application + +#### *property* app_name *: str* + +Get the application name. + +* **Returns:** + The name of the application + +#### *property* app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract)* + +Get the application specification. + +* **Returns:** + The ARC-56 contract specification for this application + +#### *property* state *: \_StateAccessor* + +Get the state accessor. + +* **Returns:** + The state accessor for this application + +#### *property* params *: \_MethodParamsBuilder* + +Get the method parameters builder. + +* **Returns:** + The method parameters builder for this application + +#### *property* send *: \_TransactionSender* + +Get the transaction sender. + +* **Returns:** + The transaction sender for this application + +#### *property* create_transaction *: \_TransactionCreator* + +Get the transaction creator. + +* **Returns:** + The transaction creator for this application + +#### *static* normalise_app_spec(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str) → [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +Normalize an application specification to ARC-56 format. + +* **Parameters:** + **app_spec** – The application specification to normalize +* **Returns:** + The normalized ARC-56 contract specification +* **Raises:** + **ValueError** – If the app spec format is invalid + +#### *static* from_network(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create an AppClient instance from network information. + +* **Parameters:** + * **app_spec** – The application specification + * **algorand** – The Algorand client instance + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Returns:** + A new AppClient instance +* **Raises:** + **Exception** – If no app ID is found for the network + +#### *static* from_creator_and_name(creator_address: str, app_name: str, app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create an AppClient instance from creator address and application name. + +* **Parameters:** + * **creator_address** – The address of the application creator + * **app_name** – The name of the application + * **app_spec** – The application specification + * **algorand** – The Algorand client instance + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache +* **Returns:** + A new AppClient instance +* **Raises:** + **ValueError** – If the app is not found for the creator and name + +#### *static* compile(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract), app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), compilation_params: [AppClientCompilationParams](#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [AppClientCompilationResult](#algokit_utils.applications.app_client.AppClientCompilationResult) + +Compile the application’s TEAL code. + +* **Parameters:** + * **app_spec** – The application specification + * **app_manager** – The application manager instance + * **compilation_params** – Optional compilation parameters +* **Returns:** + The compilation result +* **Raises:** + **ValueError** – If attempting to compile without source or byte code + +#### compile_app(compilation_params: [AppClientCompilationParams](#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [AppClientCompilationResult](#algokit_utils.applications.app_client.AppClientCompilationResult) + +Compile the application’s TEAL code. + +* **Parameters:** + **compilation_params** – Optional compilation parameters +* **Returns:** + The compilation result + +#### clone(app_name: str | None = \_MISSING, default_sender: str | None = \_MISSING, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = \_MISSING, approval_source_map: algosdk.source_map.SourceMap | None = \_MISSING, clear_source_map: algosdk.source_map.SourceMap | None = \_MISSING) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create a cloned AppClient instance with optionally overridden parameters. + +* **Parameters:** + * **app_name** – Optional new application name + * **default_sender** – Optional new default sender + * **default_signer** – Optional new default signer + * **approval_source_map** – Optional new approval source map + * **clear_source_map** – Optional new clear source map +* **Returns:** + A new AppClient instance with the specified parameters + +#### export_source_maps() → [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps) + +Export the application’s source maps. + +* **Returns:** + The application’s source maps +* **Raises:** + **ValueError** – If source maps haven’t been loaded + +#### import_source_maps(source_maps: [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps)) → None + +Import source maps for the application. + +* **Parameters:** + **source_maps** – The source maps to import +* **Raises:** + **ValueError** – If source maps are invalid or missing + +#### get_local_state(address: str) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get local state for an account. + +* **Parameters:** + **address** – The account address +* **Returns:** + The account’s local state for this application + +#### get_global_state() → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the application’s global state. + +* **Returns:** + The application’s global state + +#### get_box_names() → list[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)] + +Get all box names for the application. + +* **Returns:** + List of box names + +#### get_box_value(name: algokit_utils.models.state.BoxIdentifier) → bytes + +Get the value of a box. + +* **Parameters:** + **name** – The box identifier +* **Returns:** + The box value as bytes + +#### get_box_value_from_abi_type(name: algokit_utils.models.state.BoxIdentifier, abi_type: algokit_utils.applications.abi.ABIType) → algokit_utils.applications.abi.ABIValue + +Get a box value decoded according to an ABI type. + +* **Parameters:** + * **name** – The box identifier + * **abi_type** – The ABI type to decode as +* **Returns:** + The decoded box value + +#### get_box_values(filter_func: collections.abc.Callable[[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)], bool] | None = None) → list[[algokit_utils.models.state.BoxValue](../../models/state/index.md#algokit_utils.models.state.BoxValue)] + +Get values for multiple boxes. + +* **Parameters:** + **filter_func** – Optional function to filter box names +* **Returns:** + List of box values + +#### get_box_values_from_abi_type(abi_type: algokit_utils.applications.abi.ABIType, filter_func: collections.abc.Callable[[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)], bool] | None = None) → list[[algokit_utils.applications.abi.BoxABIValue](../abi/index.md#algokit_utils.applications.abi.BoxABIValue)] + +Get multiple box values decoded according to an ABI type. + +* **Parameters:** + * **abi_type** – The ABI type to decode as + * **filter_func** – Optional function to filter box names +* **Returns:** + List of decoded box values + +#### fund_app_account(params: [FundAppAccountParams](#algokit_utils.applications.app_client.FundAppAccountParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [algokit_utils.transactions.transaction_sender.SendSingleTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Fund the application’s account. + +* **Parameters:** + * **params** – The funding parameters + * **send_params** – Send parameters, defaults to None +* **Returns:** + The transaction result diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md new file mode 100644 index 00000000..0719b1a3 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md @@ -0,0 +1,128 @@ +# algokit_utils.applications.app_deployer + +## Attributes + +| [`APP_DEPLOY_NOTE_DAPP`](#algokit_utils.applications.app_deployer.APP_DEPLOY_NOTE_DAPP) | | +|-------------------------------------------------------------------------------------------|----| + +## Classes + +| [`AppDeploymentMetaData`](#algokit_utils.applications.app_deployer.AppDeploymentMetaData) | Metadata about an application stored in a transaction note during creation. | +|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| [`ApplicationReference`](#algokit_utils.applications.app_deployer.ApplicationReference) | Information about an Algorand app | +| [`ApplicationMetaData`](#algokit_utils.applications.app_deployer.ApplicationMetaData) | Complete metadata about a deployed app | +| [`ApplicationLookup`](#algokit_utils.applications.app_deployer.ApplicationLookup) | Cache of {py:class}\`ApplicationMetaData\` for a specific creator | +| [`AppDeployParams`](#algokit_utils.applications.app_deployer.AppDeployParams) | Parameters for deploying an app | +| [`AppDeployResult`](#algokit_utils.applications.app_deployer.AppDeployResult) | | +| [`AppDeployer`](#algokit_utils.applications.app_deployer.AppDeployer) | Manages deployment and deployment metadata of applications | + +## Module Contents + +### algokit_utils.applications.app_deployer.APP_DEPLOY_NOTE_DAPP *: str* *= 'ALGOKIT_DEPLOYER'* + +### *class* algokit_utils.applications.app_deployer.AppDeploymentMetaData + +Metadata about an application stored in a transaction note during creation. + +#### name *: str* + +#### version *: str* + +#### deletable *: bool | None* + +#### updatable *: bool | None* + +#### dictify() → dict[str, str | bool] + +### *class* algokit_utils.applications.app_deployer.ApplicationReference + +Information about an Algorand app + +#### app_id *: int* + +#### app_address *: str* + +### *class* algokit_utils.applications.app_deployer.ApplicationMetaData + +Complete metadata about a deployed app + +#### reference *: [ApplicationReference](#algokit_utils.applications.app_deployer.ApplicationReference)* + +#### deploy_metadata *: [AppDeploymentMetaData](#algokit_utils.applications.app_deployer.AppDeploymentMetaData)* + +#### created_round *: int* + +#### updated_round *: int* + +#### deleted *: bool* *= False* + +#### *property* app_id *: int* + +#### *property* app_address *: str* + +#### *property* name *: str* + +#### *property* version *: str* + +#### *property* deletable *: bool | None* + +#### *property* updatable *: bool | None* + +### *class* algokit_utils.applications.app_deployer.ApplicationLookup + +Cache of {py:class}\`ApplicationMetaData\` for a specific creator + +Can be used as an argument to {py:class}\`ApplicationClient\` to reduce the number of calls when deploying multiple +apps or discovering multiple app_ids + +#### creator *: str* + +#### apps *: dict[str, [ApplicationMetaData](#algokit_utils.applications.app_deployer.ApplicationMetaData)]* + +### *class* algokit_utils.applications.app_deployer.AppDeployParams + +Parameters for deploying an app + +#### metadata *: [AppDeploymentMetaData](#algokit_utils.applications.app_deployer.AppDeploymentMetaData)* + +#### deploy_time_params *: algokit_utils.models.state.TealTemplateParams | None* *= None* + +#### on_schema_break *: Literal['replace', 'fail', 'append'] | [algokit_utils.applications.enums.OnSchemaBreak](../enums/index.md#algokit_utils.applications.enums.OnSchemaBreak) | None* *= None* + +#### on_update *: Literal['update', 'replace', 'fail', 'append'] | [algokit_utils.applications.enums.OnUpdate](../enums/index.md#algokit_utils.applications.enums.OnUpdate) | None* *= None* + +#### create_params *: [algokit_utils.transactions.transaction_composer.AppCreateParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams) | [algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)* + +#### update_params *: [algokit_utils.transactions.transaction_composer.AppUpdateParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams) | [algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)* + +#### delete_params *: [algokit_utils.transactions.transaction_composer.AppDeleteParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams) | [algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)* + +#### existing_deployments *: [ApplicationLookup](#algokit_utils.applications.app_deployer.ApplicationLookup) | None* *= None* + +#### ignore_cache *: bool* *= False* + +#### max_fee *: int | None* *= None* + +#### send_params *: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None* *= None* + +### *class* algokit_utils.applications.app_deployer.AppDeployResult + +#### app *: [ApplicationMetaData](#algokit_utils.applications.app_deployer.ApplicationMetaData)* + +#### operation_performed *: [algokit_utils.applications.enums.OperationPerformed](../enums/index.md#algokit_utils.applications.enums.OperationPerformed)* + +#### create_result *: [algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### update_result *: [algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### delete_result *: [algokit_utils.transactions.transaction_sender.SendAppTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +### *class* algokit_utils.applications.app_deployer.AppDeployer(app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), transaction_sender: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender), indexer: algosdk.v2client.indexer.IndexerClient | None = None) + +Manages deployment and deployment metadata of applications + +#### deploy(deployment: [AppDeployParams](#algokit_utils.applications.app_deployer.AppDeployParams)) → [AppDeployResult](#algokit_utils.applications.app_deployer.AppDeployResult) + +#### get_creator_apps_by_name(\*, creator_address: str, ignore_cache: bool = False) → [ApplicationLookup](#algokit_utils.applications.app_deployer.ApplicationLookup) + +Get apps created by an account diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md new file mode 100644 index 00000000..e0002b87 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md @@ -0,0 +1,138 @@ +# algokit_utils.applications.app_factory + +## Classes + +| [`AppFactoryParams`](#algokit_utils.applications.app_factory.AppFactoryParams) | | +|--------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------| +| [`AppFactoryCreateParams`](#algokit_utils.applications.app_factory.AppFactoryCreateParams) | Schema for application creation. | +| [`AppFactoryCreateMethodCallParams`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams) | Schema for application creation. | +| [`AppFactoryCreateMethodCallResult`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult) | Base class for transaction results. | +| [`SendAppFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | Result of an application transaction. | +| [`SendAppUpdateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | Result of updating an application. | +| [`SendAppCreateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | Result of creating a new application. | +| [`AppFactoryDeployResult`](#algokit_utils.applications.app_factory.AppFactoryDeployResult) | Result from deploying an application via AppFactory | +| [`AppFactory`](#algokit_utils.applications.app_factory.AppFactory) | | + +## Module Contents + +### *class* algokit_utils.applications.app_factory.AppFactoryParams + +#### algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str* + +#### app_name *: str | None* *= None* + +#### default_sender *: str | None* *= None* + +#### default_signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### version *: str | None* *= None* + +#### compilation_params *: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None* *= None* + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateParams + +Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientBareCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) + +Schema for application creation. + +* **Variables:** + * **extra_program_pages** – Optional number of extra program pages + * **schema** – Optional application creation schema + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams + +Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientMethodCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) + +Schema for application creation. + +* **Variables:** + * **extra_program_pages** – Optional number of extra program pages + * **schema** – Optional application creation schema + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendSingleTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `Generic`[`ABIReturnT`] + +Base class for transaction results. + +Represents the result of sending a single transaction. + +#### app_id *: int* + +#### app_address *: str* + +#### compiled_approval *: Any | None* *= None* + +#### compiled_clear *: Any | None* *= None* + +#### abi_return *: ABIReturnT | None* *= None* + +### *class* algokit_utils.applications.app_factory.SendAppFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +Result of an application transaction. + +Contains the ABI return value if applicable. + +### *class* algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +Result of updating an application. + +Contains the compiled approval and clear programs. + +### *class* algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +Result of creating a new application. + +Contains the app ID and address of the newly created application. + +### *class* algokit_utils.applications.app_factory.AppFactoryDeployResult + +Result from deploying an application via AppFactory + +#### app *: [algokit_utils.applications.app_deployer.ApplicationMetaData](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationMetaData)* + +#### operation_performed *: algokit_utils.applications.app_deployer.OperationPerformed* + +#### create_result *: [SendAppCreateFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | None* *= None* + +#### update_result *: [SendAppUpdateFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | None* *= None* + +#### delete_result *: [SendAppFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | None* *= None* + +#### *classmethod* from_deploy_result(response: [algokit_utils.applications.app_deployer.AppDeployResult](../app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployResult), deploy_params: [algokit_utils.applications.app_deployer.AppDeployParams](../app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployParams), app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract), app_compilation_data: [algokit_utils.applications.app_client.AppClientCompilationResult](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationResult) | None = None) → typing_extensions.Self + +### *class* algokit_utils.applications.app_factory.AppFactory(params: [AppFactoryParams](#algokit_utils.applications.app_factory.AppFactoryParams)) + +#### *property* app_name *: str* + +#### *property* app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract)* + +#### *property* algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### *property* params *: \_MethodParamsBuilder* + +#### *property* send *: \_TransactionSender* + +#### *property* create_transaction *: \_TransactionCreator* + +#### deploy(\*, on_update: algokit_utils.applications.app_deployer.OnUpdate | None = None, on_schema_break: algokit_utils.applications.app_deployer.OnSchemaBreak | None = None, create_params: [algokit_utils.applications.app_client.AppClientMethodCallCreateParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallCreateParams) | [algokit_utils.applications.app_client.AppClientBareCallCreateParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallCreateParams) | None = None, update_params: [algokit_utils.applications.app_client.AppClientMethodCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) | [algokit_utils.applications.app_client.AppClientBareCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) | None = None, delete_params: [algokit_utils.applications.app_client.AppClientMethodCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) | [algokit_utils.applications.app_client.AppClientBareCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) | None = None, existing_deployments: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, ignore_cache: bool = False, app_name: str | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → tuple[[algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient), [AppFactoryDeployResult](#algokit_utils.applications.app_factory.AppFactoryDeployResult)] + +Deploy the application with the specified parameters. + +#### get_app_client_by_id(app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient) + +#### get_app_client_by_creator_and_name(creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient) + +#### export_source_maps() → [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps) + +#### import_source_maps(source_maps: [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps)) → None + +#### compile(compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [algokit_utils.applications.app_client.AppClientCompilationResult](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationResult) diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md new file mode 100644 index 00000000..a29a20ec --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md @@ -0,0 +1,202 @@ +# algokit_utils.applications.app_manager + +## Attributes + +| [`UPDATABLE_TEMPLATE_NAME`](#algokit_utils.applications.app_manager.UPDATABLE_TEMPLATE_NAME) | The name of the TEAL template variable for deploy-time immutability control. | +|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [`DELETABLE_TEMPLATE_NAME`](#algokit_utils.applications.app_manager.DELETABLE_TEMPLATE_NAME) | The name of the TEAL template variable for deploy-time permanence control. | + +## Classes + +| [`AppManager`](#algokit_utils.applications.app_manager.AppManager) | A manager class for interacting with Algorand applications. | +|----------------------------------------------------------------------|---------------------------------------------------------------| + +## Module Contents + +### algokit_utils.applications.app_manager.UPDATABLE_TEMPLATE_NAME *= 'TMPL_UPDATABLE'* + +The name of the TEAL template variable for deploy-time immutability control. + +### algokit_utils.applications.app_manager.DELETABLE_TEMPLATE_NAME *= 'TMPL_DELETABLE'* + +The name of the TEAL template variable for deploy-time permanence control. + +### *class* algokit_utils.applications.app_manager.AppManager(algod_client: algosdk.v2client.algod.AlgodClient) + +A manager class for interacting with Algorand applications. + +Provides functionality for compiling TEAL code, managing application state, +and interacting with application boxes. + +* **Parameters:** + **algod_client** – The Algorand client instance to use for interacting with the network + +#### compile_teal(teal_code: str) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) + +Compile TEAL source code. + +* **Parameters:** + **teal_code** – The TEAL source code to compile +* **Returns:** + The compiled TEAL code and associated metadata + +#### compile_teal_template(teal_template_code: str, template_params: algokit_utils.models.state.TealTemplateParams | None = None, deployment_metadata: collections.abc.Mapping[str, bool | None] | None = None) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) + +Compile a TEAL template with parameters. + +* **Parameters:** + * **teal_template_code** – The TEAL template code to compile + * **template_params** – Parameters to substitute in the template + * **deployment_metadata** – Deployment control parameters +* **Returns:** + The compiled TEAL code and associated metadata + +#### get_compilation_result(teal_code: str) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None + +Get cached compilation result for TEAL code if available. + +* **Parameters:** + **teal_code** – The TEAL source code +* **Returns:** + The cached compilation result if available, None otherwise + +#### get_by_id(app_id: int) → [algokit_utils.models.application.AppInformation](../../models/application/index.md#algokit_utils.models.application.AppInformation) + +Get information about an application by ID. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + Information about the application + +#### get_global_state(app_id: int) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the global state of an application. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + The application’s global state + +#### get_local_state(app_id: int, address: str) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the local state for an account in an application. + +* **Parameters:** + * **app_id** – The application ID + * **address** – The account address +* **Returns:** + The account’s local state for the application +* **Raises:** + **ValueError** – If local state is not found + +#### get_box_names(app_id: int) → list[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)] + +Get names of all boxes for an application. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + List of box names + +#### get_box_value(app_id: int, box_name: algokit_utils.models.state.BoxIdentifier) → bytes + +Get the value stored in a box. + +* **Parameters:** + * **app_id** – The application ID + * **box_name** – The box identifier +* **Returns:** + The box value as bytes + +#### get_box_values(app_id: int, box_names: list[algokit_utils.models.state.BoxIdentifier]) → list[bytes] + +Get values for multiple boxes. + +* **Parameters:** + * **app_id** – The application ID + * **box_names** – List of box identifiers +* **Returns:** + List of box values as bytes + +#### get_box_value_from_abi_type(app_id: int, box_name: algokit_utils.models.state.BoxIdentifier, abi_type: algokit_utils.applications.abi.ABIType) → algokit_utils.applications.abi.ABIValue + +Get and decode a box value using an ABI type. + +* **Parameters:** + * **app_id** – The application ID + * **box_name** – The box identifier + * **abi_type** – The ABI type to decode with +* **Returns:** + The decoded box value +* **Raises:** + **ValueError** – If decoding fails + +#### get_box_values_from_abi_type(app_id: int, box_names: list[algokit_utils.models.state.BoxIdentifier], abi_type: algokit_utils.applications.abi.ABIType) → list[algokit_utils.applications.abi.ABIValue] + +Get and decode multiple box values using an ABI type. + +* **Parameters:** + * **app_id** – The application ID + * **box_names** – List of box identifiers + * **abi_type** – The ABI type to decode with +* **Returns:** + List of decoded box values + +#### *static* get_box_reference(box_id: algokit_utils.models.state.BoxIdentifier | [algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference)) → tuple[int, bytes] + +Get standardized box reference from various identifier types. + +* **Parameters:** + **box_id** – The box identifier +* **Returns:** + Tuple of (app_id, box_name_bytes) +* **Raises:** + **ValueError** – If box identifier type is invalid + +#### *static* get_abi_return(confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None) → [algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn) | None + +Get the ABI return value from a transaction confirmation. + +* **Parameters:** + * **confirmation** – The transaction confirmation + * **method** – The ABI method +* **Returns:** + The parsed ABI return value, or None if not available + +#### *static* decode_app_state(state: list[dict[str, Any]]) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Decode application state from raw format. + +* **Parameters:** + **state** – The raw application state +* **Returns:** + Decoded application state +* **Raises:** + **ValueError** – If unknown state data type is encountered + +#### *static* replace_template_variables(program: str, template_values: algokit_utils.models.state.TealTemplateParams) → str + +Replace template variables in TEAL code. + +* **Parameters:** + * **program** – The TEAL program code + * **template_values** – Template variable values to substitute +* **Returns:** + TEAL code with substituted values +* **Raises:** + **ValueError** – If template value type is unexpected + +#### *static* replace_teal_template_deploy_time_control_params(teal_template_code: str, params: collections.abc.Mapping[str, bool | None]) → str + +Replace deploy-time control parameters in TEAL template. + +* **Parameters:** + * **teal_template_code** – The TEAL template code + * **params** – The deploy-time control parameters +* **Returns:** + TEAL code with substituted control parameters +* **Raises:** + **ValueError** – If template variables not found in code + +#### *static* strip_teal_comments(teal_code: str) → str diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md new file mode 100644 index 00000000..8ad088c2 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md @@ -0,0 +1,157 @@ +# algokit_utils.applications.app_spec.arc32 + +## Attributes + +| [`AppSpecStateDict`](#algokit_utils.applications.app_spec.arc32.AppSpecStateDict) | Type defining Application Specification state entries | +|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| [`OnCompleteActionName`](#algokit_utils.applications.app_spec.arc32.OnCompleteActionName) | String literals representing on completion transaction types | +| [`MethodConfigDict`](#algokit_utils.applications.app_spec.arc32.MethodConfigDict) | Dictionary of dict[OnCompletionActionName, CallConfig] representing allowed actions for each on completion type | +| [`DefaultArgumentType`](#algokit_utils.applications.app_spec.arc32.DefaultArgumentType) | Literal values describing the types of default argument sources | +| [`StateDict`](#algokit_utils.applications.app_spec.arc32.StateDict) | | + +## Classes + +| [`CallConfig`](#algokit_utils.applications.app_spec.arc32.CallConfig) | Describes the type of calls a method can be used for based on {py:class}\`algosdk.transaction.OnComplete\` type | +|-----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| [`StructArgDict`](#algokit_utils.applications.app_spec.arc32.StructArgDict) | dict() -> new empty dictionary | +| [`DefaultArgumentDict`](#algokit_utils.applications.app_spec.arc32.DefaultArgumentDict) | DefaultArgument is a container for any arguments that may | +| [`MethodHints`](#algokit_utils.applications.app_spec.arc32.MethodHints) | MethodHints provides hints to the caller about how to call the method | +| [`Arc32Contract`](#algokit_utils.applications.app_spec.arc32.Arc32Contract) | ARC-0032 application specification | + +## Module Contents + +### algokit_utils.applications.app_spec.arc32.AppSpecStateDict *: TypeAlias* *= dict[str, dict[str, dict]]* + +Type defining Application Specification state entries + +### *class* algokit_utils.applications.app_spec.arc32.CallConfig + +Bases: `enum.IntFlag` + +Describes the type of calls a method can be used for based on {py:class}\`algosdk.transaction.OnComplete\` type + +#### NEVER *= 0* + +Never handle the specified on completion type + +#### CALL *= 1* + +Only handle the specified on completion type for application calls + +#### CREATE *= 2* + +Only handle the specified on completion type for application create calls + +#### ALL *= 3* + +Handle the specified on completion type for both create and normal application calls + +### *class* algokit_utils.applications.app_spec.arc32.StructArgDict + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### name *: str* + +#### elements *: list[list[str]]* + +### algokit_utils.applications.app_spec.arc32.OnCompleteActionName *: TypeAlias* *= Literal['no_op', 'opt_in', 'close_out', 'clear_state', 'update_application', 'delete_application']* + +String literals representing on completion transaction types + +### algokit_utils.applications.app_spec.arc32.MethodConfigDict *: TypeAlias* *= dict[OnCompleteActionName, CallConfig]* + +Dictionary of dict[OnCompletionActionName, CallConfig] representing allowed actions for each on completion type + +### algokit_utils.applications.app_spec.arc32.DefaultArgumentType *: TypeAlias* *= Literal['abi-method', 'local-state', 'global-state', 'constant']* + +Literal values describing the types of default argument sources + +### *class* algokit_utils.applications.app_spec.arc32.DefaultArgumentDict + +Bases: `TypedDict` + +DefaultArgument is a container for any arguments that may +be resolved prior to calling some target method + +#### source *: DefaultArgumentType* + +#### data *: int | str | bytes | algosdk.abi.method.MethodDict* + +### algokit_utils.applications.app_spec.arc32.StateDict + +### *class* algokit_utils.applications.app_spec.arc32.MethodHints + +MethodHints provides hints to the caller about how to call the method + +#### read_only *: bool* *= False* + +#### structs *: dict[str, [StructArgDict](#algokit_utils.applications.app_spec.arc32.StructArgDict)]* + +#### default_arguments *: dict[str, [DefaultArgumentDict](#algokit_utils.applications.app_spec.arc32.DefaultArgumentDict)]* + +#### call_config *: MethodConfigDict* + +#### empty() → bool + +#### dictify() → dict[str, Any] + +#### *static* undictify(data: dict[str, Any]) → [MethodHints](#algokit_utils.applications.app_spec.arc32.MethodHints) + +### *class* algokit_utils.applications.app_spec.arc32.Arc32Contract + +ARC-0032 application specification + +See <[https://github.com/algorandfoundation/ARCs/pull/150](https://github.com/algorandfoundation/ARCs/pull/150)> + +#### approval_program *: str* + +#### clear_program *: str* + +#### contract *: algosdk.abi.Contract* + +#### hints *: dict[str, [MethodHints](#algokit_utils.applications.app_spec.arc32.MethodHints)]* + +#### schema *: StateDict* + +#### global_state_schema *: algosdk.transaction.StateSchema* + +#### local_state_schema *: algosdk.transaction.StateSchema* + +#### bare_call_config *: MethodConfigDict* + +#### dictify() → dict + +#### to_json(indent: int | None = None) → str + +#### *static* from_json(application_spec: str) → [Arc32Contract](#algokit_utils.applications.app_spec.arc32.Arc32Contract) + +#### export(directory: pathlib.Path | str | None = None) → None + +Write out the artifacts generated by the application to disk. + +Writes the approval program, clear program, contract specification and application specification +to files in the specified directory. + +* **Parameters:** + **directory** – Path to the directory where the artifacts should be written. If not specified, + uses the current working directory diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md new file mode 100644 index 00000000..f6262e7a --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md @@ -0,0 +1,681 @@ +# algokit_utils.applications.app_spec.arc56 + +## Classes + +| [`StructField`](#algokit_utils.applications.app_spec.arc56.StructField) | Represents a field in a struct type. | +|-------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| [`CallEnum`](#algokit_utils.applications.app_spec.arc56.CallEnum) | Enum representing different call types for application transactions. | +| [`CreateEnum`](#algokit_utils.applications.app_spec.arc56.CreateEnum) | Enum representing different create types for application transactions. | +| [`BareActions`](#algokit_utils.applications.app_spec.arc56.BareActions) | Represents bare call and create actions for an application. | +| [`ByteCode`](#algokit_utils.applications.app_spec.arc56.ByteCode) | Represents the approval and clear program bytecode. | +| [`Compiler`](#algokit_utils.applications.app_spec.arc56.Compiler) | Enum representing different compiler types. | +| [`CompilerVersion`](#algokit_utils.applications.app_spec.arc56.CompilerVersion) | Represents compiler version information. | +| [`CompilerInfo`](#algokit_utils.applications.app_spec.arc56.CompilerInfo) | Information about the compiler used. | +| [`Network`](#algokit_utils.applications.app_spec.arc56.Network) | Network-specific application information. | +| [`ScratchVariables`](#algokit_utils.applications.app_spec.arc56.ScratchVariables) | Information about scratch space variables. | +| [`Source`](#algokit_utils.applications.app_spec.arc56.Source) | Source code for approval and clear programs. | +| [`Global`](#algokit_utils.applications.app_spec.arc56.Global) | Global state schema. | +| [`Local`](#algokit_utils.applications.app_spec.arc56.Local) | Local state schema. | +| [`Schema`](#algokit_utils.applications.app_spec.arc56.Schema) | Application state schema. | +| [`TemplateVariables`](#algokit_utils.applications.app_spec.arc56.TemplateVariables) | Template variable information. | +| [`EventArg`](#algokit_utils.applications.app_spec.arc56.EventArg) | Event argument information. | +| [`Event`](#algokit_utils.applications.app_spec.arc56.Event) | Event information. | +| [`Actions`](#algokit_utils.applications.app_spec.arc56.Actions) | Method actions information. | +| [`DefaultValue`](#algokit_utils.applications.app_spec.arc56.DefaultValue) | Default value information for method arguments. | +| [`MethodArg`](#algokit_utils.applications.app_spec.arc56.MethodArg) | Method argument information. | +| [`Boxes`](#algokit_utils.applications.app_spec.arc56.Boxes) | Box storage requirements. | +| [`Recommendations`](#algokit_utils.applications.app_spec.arc56.Recommendations) | Method execution recommendations. | +| [`Returns`](#algokit_utils.applications.app_spec.arc56.Returns) | Method return information. | +| [`Method`](#algokit_utils.applications.app_spec.arc56.Method) | Method information. | +| [`PcOffsetMethod`](#algokit_utils.applications.app_spec.arc56.PcOffsetMethod) | PC offset method types. | +| [`SourceInfo`](#algokit_utils.applications.app_spec.arc56.SourceInfo) | Source code location information. | +| [`StorageKey`](#algokit_utils.applications.app_spec.arc56.StorageKey) | Storage key information. | +| [`StorageMap`](#algokit_utils.applications.app_spec.arc56.StorageMap) | Storage map information. | +| [`Keys`](#algokit_utils.applications.app_spec.arc56.Keys) | Storage keys for different storage types. | +| [`Maps`](#algokit_utils.applications.app_spec.arc56.Maps) | Storage maps for different storage types. | +| [`State`](#algokit_utils.applications.app_spec.arc56.State) | Application state information. | +| [`ProgramSourceInfo`](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo) | Program source information. | +| [`SourceInfoModel`](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) | Source information for approval and clear programs. | +| [`Arc56Contract`](#algokit_utils.applications.app_spec.arc56.Arc56Contract) | ARC-0056 application specification. | + +## Module Contents + +### *class* algokit_utils.applications.app_spec.arc56.StructField + +Represents a field in a struct type. + +* **Variables:** + * **name** – Name of the struct field + * **type** – Type of the struct field, either a string or list of StructFields + +#### name *: str* + +#### type *: list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)] | str* + +#### *static* from_dict(data: dict[str, Any]) → [StructField](#algokit_utils.applications.app_spec.arc56.StructField) + +### *class* algokit_utils.applications.app_spec.arc56.CallEnum + +Bases: `str`, `enum.Enum` + +Enum representing different call types for application transactions. + +#### CLEAR_STATE *= 'ClearState'* + +#### CLOSE_OUT *= 'CloseOut'* + +#### DELETE_APPLICATION *= 'DeleteApplication'* + +#### NO_OP *= 'NoOp'* + +#### OPT_IN *= 'OptIn'* + +#### UPDATE_APPLICATION *= 'UpdateApplication'* + +### *class* algokit_utils.applications.app_spec.arc56.CreateEnum + +Bases: `str`, `enum.Enum` + +Enum representing different create types for application transactions. + +#### DELETE_APPLICATION *= 'DeleteApplication'* + +#### NO_OP *= 'NoOp'* + +#### OPT_IN *= 'OptIn'* + +### *class* algokit_utils.applications.app_spec.arc56.BareActions + +Represents bare call and create actions for an application. + +* **Variables:** + * **call** – List of allowed call actions + * **create** – List of allowed create actions + +#### call *: list[[CallEnum](#algokit_utils.applications.app_spec.arc56.CallEnum)]* + +#### create *: list[[CreateEnum](#algokit_utils.applications.app_spec.arc56.CreateEnum)]* + +#### *static* from_dict(data: dict[str, Any]) → [BareActions](#algokit_utils.applications.app_spec.arc56.BareActions) + +### *class* algokit_utils.applications.app_spec.arc56.ByteCode + +Represents the approval and clear program bytecode. + +* **Variables:** + * **approval** – Base64 encoded approval program bytecode + * **clear** – Base64 encoded clear program bytecode + +#### approval *: str* + +#### clear *: str* + +#### *static* from_dict(data: dict[str, Any]) → [ByteCode](#algokit_utils.applications.app_spec.arc56.ByteCode) + +### *class* algokit_utils.applications.app_spec.arc56.Compiler + +Bases: `str`, `enum.Enum` + +Enum representing different compiler types. + +#### ALGOD *= 'algod'* + +#### PUYA *= 'puya'* + +### *class* algokit_utils.applications.app_spec.arc56.CompilerVersion + +Represents compiler version information. + +* **Variables:** + * **commit_hash** – Git commit hash of the compiler + * **major** – Major version number + * **minor** – Minor version number + * **patch** – Patch version number + +#### commit_hash *: str | None* *= None* + +#### major *: int | None* *= None* + +#### minor *: int | None* *= None* + +#### patch *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [CompilerVersion](#algokit_utils.applications.app_spec.arc56.CompilerVersion) + +### *class* algokit_utils.applications.app_spec.arc56.CompilerInfo + +Information about the compiler used. + +* **Variables:** + * **compiler** – Type of compiler used + * **compiler_version** – Version information for the compiler + +#### compiler *: [Compiler](#algokit_utils.applications.app_spec.arc56.Compiler)* + +#### compiler_version *: [CompilerVersion](#algokit_utils.applications.app_spec.arc56.CompilerVersion)* + +#### *static* from_dict(data: dict[str, Any]) → [CompilerInfo](#algokit_utils.applications.app_spec.arc56.CompilerInfo) + +### *class* algokit_utils.applications.app_spec.arc56.Network + +Network-specific application information. + +* **Variables:** + **app_id** – Application ID on the network + +#### app_id *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Network](#algokit_utils.applications.app_spec.arc56.Network) + +### *class* algokit_utils.applications.app_spec.arc56.ScratchVariables + +Information about scratch space variables. + +* **Variables:** + * **slot** – Scratch slot number + * **type** – Type of the scratch variable + +#### slot *: int* + +#### type *: str* + +#### *static* from_dict(data: dict[str, Any]) → [ScratchVariables](#algokit_utils.applications.app_spec.arc56.ScratchVariables) + +### *class* algokit_utils.applications.app_spec.arc56.Source + +Source code for approval and clear programs. + +* **Variables:** + * **approval** – Base64 encoded approval program source + * **clear** – Base64 encoded clear program source + +#### approval *: str* + +#### clear *: str* + +#### *static* from_dict(data: dict[str, Any]) → [Source](#algokit_utils.applications.app_spec.arc56.Source) + +#### get_decoded_approval() → str + +Get decoded approval program source. + +* **Returns:** + Decoded approval program source code + +#### get_decoded_clear() → str + +Get decoded clear program source. + +* **Returns:** + Decoded clear program source code + +### *class* algokit_utils.applications.app_spec.arc56.Global + +Global state schema. + +* **Variables:** + * **bytes** – Number of byte slices in global state + * **ints** – Number of integers in global state + +#### bytes *: int* + +#### ints *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Global](#algokit_utils.applications.app_spec.arc56.Global) + +### *class* algokit_utils.applications.app_spec.arc56.Local + +Local state schema. + +* **Variables:** + * **bytes** – Number of byte slices in local state + * **ints** – Number of integers in local state + +#### bytes *: int* + +#### ints *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Local](#algokit_utils.applications.app_spec.arc56.Local) + +### *class* algokit_utils.applications.app_spec.arc56.Schema + +Application state schema. + +* **Variables:** + * **global_state** – Global state schema + * **local_state** – Local state schema + +#### global_state *: [Global](#algokit_utils.applications.app_spec.arc56.Global)* + +#### local_state *: [Local](#algokit_utils.applications.app_spec.arc56.Local)* + +#### *static* from_dict(data: dict[str, Any]) → [Schema](#algokit_utils.applications.app_spec.arc56.Schema) + +### *class* algokit_utils.applications.app_spec.arc56.TemplateVariables + +Template variable information. + +* **Variables:** + * **type** – Type of the template variable + * **value** – Optional value of the template variable + +#### type *: str* + +#### value *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [TemplateVariables](#algokit_utils.applications.app_spec.arc56.TemplateVariables) + +### *class* algokit_utils.applications.app_spec.arc56.EventArg + +Event argument information. + +* **Variables:** + * **type** – Type of the event argument + * **desc** – Optional description of the argument + * **name** – Optional name of the argument + * **struct** – Optional struct type name + +#### type *: str* + +#### desc *: str | None* *= None* + +#### name *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [EventArg](#algokit_utils.applications.app_spec.arc56.EventArg) + +### *class* algokit_utils.applications.app_spec.arc56.Event + +Event information. + +* **Variables:** + * **args** – List of event arguments + * **name** – Name of the event + * **desc** – Optional description of the event + +#### args *: list[[EventArg](#algokit_utils.applications.app_spec.arc56.EventArg)]* + +#### name *: str* + +#### desc *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Event](#algokit_utils.applications.app_spec.arc56.Event) + +### *class* algokit_utils.applications.app_spec.arc56.Actions + +Method actions information. + +* **Variables:** + * **call** – Optional list of allowed call actions + * **create** – Optional list of allowed create actions + +#### call *: list[[CallEnum](#algokit_utils.applications.app_spec.arc56.CallEnum)] | None* *= None* + +#### create *: list[[CreateEnum](#algokit_utils.applications.app_spec.arc56.CreateEnum)] | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Actions](#algokit_utils.applications.app_spec.arc56.Actions) + +### *class* algokit_utils.applications.app_spec.arc56.DefaultValue + +Default value information for method arguments. + +* **Variables:** + * **data** – Default value data + * **source** – Source of the default value + * **type** – Optional type of the default value + +#### data *: str* + +#### source *: Literal['box', 'global', 'local', 'literal', 'method']* + +#### type *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [DefaultValue](#algokit_utils.applications.app_spec.arc56.DefaultValue) + +### *class* algokit_utils.applications.app_spec.arc56.MethodArg + +Method argument information. + +* **Variables:** + * **type** – Type of the argument + * **default_value** – Optional default value + * **desc** – Optional description + * **name** – Optional name + * **struct** – Optional struct type name + +#### type *: str* + +#### default_value *: [DefaultValue](#algokit_utils.applications.app_spec.arc56.DefaultValue) | None* *= None* + +#### desc *: str | None* *= None* + +#### name *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [MethodArg](#algokit_utils.applications.app_spec.arc56.MethodArg) + +### *class* algokit_utils.applications.app_spec.arc56.Boxes + +Box storage requirements. + +* **Variables:** + * **key** – Box key + * **read_bytes** – Number of bytes to read + * **write_bytes** – Number of bytes to write + * **app** – Optional application ID + +#### key *: str* + +#### read_bytes *: int* + +#### write_bytes *: int* + +#### app *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Boxes](#algokit_utils.applications.app_spec.arc56.Boxes) + +### *class* algokit_utils.applications.app_spec.arc56.Recommendations + +Method execution recommendations. + +* **Variables:** + * **accounts** – Optional list of accounts + * **apps** – Optional list of applications + * **assets** – Optional list of assets + * **boxes** – Optional box storage requirements + * **inner_transaction_count** – Optional inner transaction count + +#### accounts *: list[str] | None* *= None* + +#### apps *: list[int] | None* *= None* + +#### assets *: list[int] | None* *= None* + +#### boxes *: [Boxes](#algokit_utils.applications.app_spec.arc56.Boxes) | None* *= None* + +#### inner_transaction_count *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Recommendations](#algokit_utils.applications.app_spec.arc56.Recommendations) + +### *class* algokit_utils.applications.app_spec.arc56.Returns + +Method return information. + +* **Variables:** + * **type** – Return type + * **desc** – Optional description + * **struct** – Optional struct type name + +#### type *: str* + +#### desc *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Returns](#algokit_utils.applications.app_spec.arc56.Returns) + +### *class* algokit_utils.applications.app_spec.arc56.Method + +Method information. + +* **Variables:** + * **actions** – Allowed actions + * **args** – Method arguments + * **name** – Method name + * **returns** – Return information + * **desc** – Optional description + * **events** – Optional list of events + * **readonly** – Optional readonly flag + * **recommendations** – Optional execution recommendations + +#### actions *: [Actions](#algokit_utils.applications.app_spec.arc56.Actions)* + +#### args *: list[[MethodArg](#algokit_utils.applications.app_spec.arc56.MethodArg)]* + +#### name *: str* + +#### returns *: [Returns](#algokit_utils.applications.app_spec.arc56.Returns)* + +#### desc *: str | None* *= None* + +#### events *: list[[Event](#algokit_utils.applications.app_spec.arc56.Event)] | None* *= None* + +#### readonly *: bool | None* *= None* + +#### recommendations *: [Recommendations](#algokit_utils.applications.app_spec.arc56.Recommendations) | None* *= None* + +#### to_abi_method() → algosdk.abi.Method + +Convert to ABI method. + +* **Raises:** + **ValueError** – If underlying ABI method is not initialized +* **Returns:** + ABI method + +#### *static* from_dict(data: dict[str, Any]) → [Method](#algokit_utils.applications.app_spec.arc56.Method) + +### *class* algokit_utils.applications.app_spec.arc56.PcOffsetMethod + +Bases: `str`, `enum.Enum` + +PC offset method types. + +#### CBLOCKS *= 'cblocks'* + +#### NONE *= 'none'* + +### *class* algokit_utils.applications.app_spec.arc56.SourceInfo + +Source code location information. + +* **Variables:** + * **pc** – List of program counter values + * **error_message** – Optional error message + * **source** – Optional source code + * **teal** – Optional TEAL version + +#### pc *: list[int]* + +#### error_message *: str | None* *= None* + +#### source *: str | None* *= None* + +#### teal *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [SourceInfo](#algokit_utils.applications.app_spec.arc56.SourceInfo) + +### *class* algokit_utils.applications.app_spec.arc56.StorageKey + +Storage key information. + +* **Variables:** + * **key** – Storage key + * **key_type** – Type of the key + * **value_type** – Type of the value + * **desc** – Optional description + +#### key *: str* + +#### key_type *: str* + +#### value_type *: str* + +#### desc *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey) + +### *class* algokit_utils.applications.app_spec.arc56.StorageMap + +Storage map information. + +* **Variables:** + * **key_type** – Type of map keys + * **value_type** – Type of map values + * **desc** – Optional description + * **prefix** – Optional key prefix + +#### key_type *: str* + +#### value_type *: str* + +#### desc *: str | None* *= None* + +#### prefix *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap) + +### *class* algokit_utils.applications.app_spec.arc56.Keys + +Storage keys for different storage types. + +* **Variables:** + * **box** – Box storage keys + * **global_state** – Global state storage keys + * **local_state** – Local state storage keys + +#### box *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### global_state *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### local_state *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### *static* from_dict(data: dict[str, Any]) → [Keys](#algokit_utils.applications.app_spec.arc56.Keys) + +### *class* algokit_utils.applications.app_spec.arc56.Maps + +Storage maps for different storage types. + +* **Variables:** + * **box** – Box storage maps + * **global_state** – Global state storage maps + * **local_state** – Local state storage maps + +#### box *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### global_state *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### local_state *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### *static* from_dict(data: dict[str, Any]) → [Maps](#algokit_utils.applications.app_spec.arc56.Maps) + +### *class* algokit_utils.applications.app_spec.arc56.State + +Application state information. + +* **Variables:** + * **keys** – Storage keys + * **maps** – Storage maps + * **schema** – State schema + +#### keys *: [Keys](#algokit_utils.applications.app_spec.arc56.Keys)* + +#### maps *: [Maps](#algokit_utils.applications.app_spec.arc56.Maps)* + +#### schema *: [Schema](#algokit_utils.applications.app_spec.arc56.Schema)* + +#### *static* from_dict(data: dict[str, Any]) → [State](#algokit_utils.applications.app_spec.arc56.State) + +### *class* algokit_utils.applications.app_spec.arc56.ProgramSourceInfo + +Program source information. + +* **Variables:** + * **pc_offset_method** – PC offset method + * **source_info** – List of source info entries + +#### pc_offset_method *: [PcOffsetMethod](#algokit_utils.applications.app_spec.arc56.PcOffsetMethod)* + +#### source_info *: list[[SourceInfo](#algokit_utils.applications.app_spec.arc56.SourceInfo)]* + +#### *static* from_dict(data: dict[str, Any]) → [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo) + +### *class* algokit_utils.applications.app_spec.arc56.SourceInfoModel + +Source information for approval and clear programs. + +* **Variables:** + * **approval** – Approval program source info + * **clear** – Clear program source info + +#### approval *: [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo)* + +#### clear *: [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo)* + +#### *static* from_dict(data: dict[str, Any]) → [SourceInfoModel](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) + +### *class* algokit_utils.applications.app_spec.arc56.Arc56Contract + +ARC-0056 application specification. + +See [https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) + +* **Variables:** + * **arcs** – List of supported ARC version numbers + * **bare_actions** – Bare call and create actions + * **methods** – List of contract methods + * **name** – Contract name + * **state** – Contract state information + * **structs** – Contract struct definitions + * **byte_code** – Optional bytecode for approval and clear programs + * **compiler_info** – Optional compiler information + * **desc** – Optional contract description + * **events** – Optional list of contract events + * **networks** – Optional network deployment information + * **scratch_variables** – Optional scratch variable information + * **source** – Optional source code + * **source_info** – Optional source code information + * **template_variables** – Optional template variable information + +#### arcs *: list[int]* + +#### bare_actions *: [BareActions](#algokit_utils.applications.app_spec.arc56.BareActions)* + +#### methods *: list[[Method](#algokit_utils.applications.app_spec.arc56.Method)]* + +#### name *: str* + +#### state *: [State](#algokit_utils.applications.app_spec.arc56.State)* + +#### structs *: dict[str, list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)]]* + +#### byte_code *: [ByteCode](#algokit_utils.applications.app_spec.arc56.ByteCode) | None* *= None* + +#### compiler_info *: [CompilerInfo](#algokit_utils.applications.app_spec.arc56.CompilerInfo) | None* *= None* + +#### desc *: str | None* *= None* + +#### events *: list[[Event](#algokit_utils.applications.app_spec.arc56.Event)] | None* *= None* + +#### networks *: dict[str, [Network](#algokit_utils.applications.app_spec.arc56.Network)] | None* *= None* + +#### scratch_variables *: dict[str, [ScratchVariables](#algokit_utils.applications.app_spec.arc56.ScratchVariables)] | None* *= None* + +#### source *: [Source](#algokit_utils.applications.app_spec.arc56.Source) | None* *= None* + +#### source_info *: [SourceInfoModel](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) | None* *= None* + +#### template_variables *: dict[str, [TemplateVariables](#algokit_utils.applications.app_spec.arc56.TemplateVariables)] | None* *= None* + +#### *static* from_dict(application_spec: dict) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +Create Arc56Contract from dictionary. + +* **Parameters:** + **application_spec** – Dictionary containing contract specification +* **Returns:** + Arc56Contract instance + +#### *static* from_json(application_spec: str) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +#### *static* from_arc32(arc32_application_spec: str | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract)) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +#### *static* get_abi_struct_from_abi_tuple(decoded_tuple: Any, struct_fields: list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)]]) → dict[str, Any] + +#### to_json(indent: int | None = None) → str + +#### dictify() → dict + +#### get_arc56_method(method_name_or_signature: str) → [Method](#algokit_utils.applications.app_spec.arc56.Method) diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md new file mode 100644 index 00000000..7a37b142 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md @@ -0,0 +1,6 @@ +# algokit_utils.applications.app_spec + +## Submodules + +* [algokit_utils.applications.app_spec.arc32](arc32/index.md) +* [algokit_utils.applications.app_spec.arc56](arc56/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/applications/enums/index.md b/docs/markdown/autoapi/algokit_utils/applications/enums/index.md new file mode 100644 index 00000000..ac63173b --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/enums/index.md @@ -0,0 +1,72 @@ +# algokit_utils.applications.enums + +## Classes + +| [`OnSchemaBreak`](#algokit_utils.applications.enums.OnSchemaBreak) | Action to take if an Application's schema has breaking changes | +|------------------------------------------------------------------------------|------------------------------------------------------------------| +| [`OnUpdate`](#algokit_utils.applications.enums.OnUpdate) | Action to take if an Application has been updated | +| [`OperationPerformed`](#algokit_utils.applications.enums.OperationPerformed) | Describes the actions taken during deployment | + +## Module Contents + +### *class* algokit_utils.applications.enums.OnSchemaBreak(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Action to take if an Application’s schema has breaking changes + +#### Fail *= 0* + +Fail the deployment + +#### ReplaceApp *= 2* + +Create a new Application and delete the old Application in a single transaction + +#### AppendApp *= 3* + +Create a new Application + +### *class* algokit_utils.applications.enums.OnUpdate(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Action to take if an Application has been updated + +#### Fail *= 0* + +Fail the deployment + +#### UpdateApp *= 1* + +Update the Application with the new approval and clear programs + +#### ReplaceApp *= 2* + +Create a new Application and delete the old Application in a single transaction + +#### AppendApp *= 3* + +Create a new application + +### *class* algokit_utils.applications.enums.OperationPerformed(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Describes the actions taken during deployment + +#### Nothing *= 0* + +An existing Application was found + +#### Create *= 1* + +No existing Application was found, created a new Application + +#### Update *= 2* + +An existing Application was found, but was out of date, updated to latest version + +#### Replace *= 3* + +An existing Application was found, but was out of date, created a new Application and deleted the original diff --git a/docs/markdown/autoapi/algokit_utils/applications/index.md b/docs/markdown/autoapi/algokit_utils/applications/index.md new file mode 100644 index 00000000..8f94c76d --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/index.md @@ -0,0 +1,11 @@ +# algokit_utils.applications + +## Submodules + +* [algokit_utils.applications.abi](abi/index.md) +* [algokit_utils.applications.app_client](app_client/index.md) +* [algokit_utils.applications.app_deployer](app_deployer/index.md) +* [algokit_utils.applications.app_factory](app_factory/index.md) +* [algokit_utils.applications.app_manager](app_manager/index.md) +* [algokit_utils.applications.app_spec](app_spec/index.md) +* [algokit_utils.applications.enums](enums/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md new file mode 100644 index 00000000..7b8afc34 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md @@ -0,0 +1,173 @@ +# algokit_utils.assets.asset_manager + +## Classes + +| [`AccountAssetInformation`](#algokit_utils.assets.asset_manager.AccountAssetInformation) | Information about an account's holding of a particular asset. | +|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [`AssetInformation`](#algokit_utils.assets.asset_manager.AssetInformation) | Information about an Algorand Standard Asset (ASA). | +| [`BulkAssetOptInOutResult`](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult) | Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. | +| [`AssetManager`](#algokit_utils.assets.asset_manager.AssetManager) | A manager for Algorand Standard Assets (ASAs). | + +## Module Contents + +### *class* algokit_utils.assets.asset_manager.AccountAssetInformation + +Information about an account’s holding of a particular asset. + +* **Variables:** + * **asset_id** – The ID of the asset + * **balance** – The amount of the asset held by the account + * **frozen** – Whether the asset is frozen for this account + * **round** – The round this information was retrieved at + +#### asset_id *: int* + +#### balance *: int* + +#### frozen *: bool* + +#### round *: int* + +### *class* algokit_utils.assets.asset_manager.AssetInformation + +Information about an Algorand Standard Asset (ASA). + +* **Variables:** + * **asset_id** – The ID of the asset + * **creator** – The address of the account that created the asset + * **total** – The total amount of the smallest divisible units that were created of the asset + * **decimals** – The amount of decimal places the asset was created with + * **default_frozen** – Whether the asset was frozen by default for all accounts, defaults to None + * **manager** – The address of the optional account that can manage the configuration of the asset and destroy it, + +defaults to None +:ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, +defaults to None +:ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, +defaults to None +:ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, +defaults to None +:ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None +:ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None +:ivar asset_name: The optional name of the asset, defaults to None +:ivar asset_name_b64: The optional name of the asset as bytes, defaults to None +:ivar url: Optional URL where more information about the asset can be retrieved, defaults to None +:ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None +:ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, +defaults to None + +#### asset_id *: int* + +#### creator *: str* + +#### total *: int* + +#### decimals *: int* + +#### default_frozen *: bool | None* *= None* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +#### unit_name *: str | None* *= None* + +#### unit_name_b64 *: bytes | None* *= None* + +#### asset_name *: str | None* *= None* + +#### asset_name_b64 *: bytes | None* *= None* + +#### url *: str | None* *= None* + +#### url_b64 *: bytes | None* *= None* + +#### metadata_hash *: bytes | None* *= None* + +### *class* algokit_utils.assets.asset_manager.BulkAssetOptInOutResult + +Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. + +* **Variables:** + * **asset_id** – The ID of the asset opted into / out of + * **transaction_id** – The transaction ID of the resulting opt in / out + +#### asset_id *: int* + +#### transaction_id *: str* + +### *class* algokit_utils.assets.asset_manager.AssetManager(algod_client: algosdk.v2client.algod.AlgodClient, new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)]) + +A manager for Algorand Standard Assets (ASAs). + +* **Parameters:** + * **algod_client** – An algod client + * **new_group** – A function that creates a new TransactionComposer transaction group + +#### get_by_id(asset_id: int) → [AssetInformation](#algokit_utils.assets.asset_manager.AssetInformation) + +Returns the current asset information for the asset with the given ID. + +* **Parameters:** + **asset_id** – The ID of the asset +* **Returns:** + The asset information + +#### get_account_information(sender: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) | algosdk.atomic_transaction_composer.TransactionSigner, asset_id: int) → [AccountAssetInformation](#algokit_utils.assets.asset_manager.AccountAssetInformation) + +Returns the given sender account’s asset holding for a given asset. + +* **Parameters:** + * **sender** – The address of the sender/account to look up + * **asset_id** – The ID of the asset to return a holding for +* **Returns:** + The account asset holding information + +#### bulk_opt_in(account: str, asset_ids: list[int], signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → list[[BulkAssetOptInOutResult](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult)] + +Opt an account in to a list of Algorand Standard Assets. + +* **Parameters:** + * **account** – The account to opt-in + * **asset_ids** – The list of asset IDs to opt-in to + * **signer** – The signer to use for the transaction, defaults to None + * **rekey_to** – The address to rekey the account to, defaults to None + * **note** – The note to include in the transaction, defaults to None + * **lease** – The lease to include in the transaction, defaults to None + * **static_fee** – The static fee to include in the transaction, defaults to None + * **extra_fee** – The extra fee to include in the transaction, defaults to None + * **max_fee** – The maximum fee to include in the transaction, defaults to None + * **validity_window** – The validity window to include in the transaction, defaults to None + * **first_valid_round** – The first valid round to include in the transaction, defaults to None + * **last_valid_round** – The last valid round to include in the transaction, defaults to None + * **send_params** – The send parameters to use for the transaction, defaults to None +* **Returns:** + An array of records matching asset ID to transaction ID of the opt in + +#### bulk_opt_out(\*, account: str, asset_ids: list[int], ensure_zero_balance: bool = True, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → list[[BulkAssetOptInOutResult](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult)] + +Opt an account out of a list of Algorand Standard Assets. + +* **Parameters:** + * **account** – The account to opt-out + * **asset_ids** – The list of asset IDs to opt-out of + * **ensure_zero_balance** – Whether to check if the account has a zero balance first, defaults to True + * **signer** – The signer to use for the transaction, defaults to None + * **rekey_to** – The address to rekey the account to, defaults to None + * **note** – The note to include in the transaction, defaults to None + * **lease** – The lease to include in the transaction, defaults to None + * **static_fee** – The static fee to include in the transaction, defaults to None + * **extra_fee** – The extra fee to include in the transaction, defaults to None + * **max_fee** – The maximum fee to include in the transaction, defaults to None + * **validity_window** – The validity window to include in the transaction, defaults to None + * **first_valid_round** – The first valid round to include in the transaction, defaults to None + * **last_valid_round** – The last valid round to include in the transaction, defaults to None + * **send_params** – The send parameters to use for the transaction, defaults to None +* **Raises:** + **ValueError** – If ensure_zero_balance is True and account has non-zero balance or is not opted in +* **Returns:** + An array of records matching asset ID to transaction ID of the opt out diff --git a/docs/markdown/autoapi/algokit_utils/assets/index.md b/docs/markdown/autoapi/algokit_utils/assets/index.md new file mode 100644 index 00000000..5091632c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/assets/index.md @@ -0,0 +1,5 @@ +# algokit_utils.assets + +## Submodules + +* [algokit_utils.assets.asset_manager](asset_manager/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md new file mode 100644 index 00000000..55dfc287 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md @@ -0,0 +1,367 @@ +# algokit_utils.clients.client_manager + +## Classes + +| [`AlgoSdkClients`](#algokit_utils.clients.client_manager.AlgoSdkClients) | Container for Algorand SDK client instances. | +|----------------------------------------------------------------------------|------------------------------------------------| +| [`NetworkDetail`](#algokit_utils.clients.client_manager.NetworkDetail) | Details about an Algorand network. | +| [`ClientManager`](#algokit_utils.clients.client_manager.ClientManager) | Manager for Algorand SDK clients. | + +## Module Contents + +### *class* algokit_utils.clients.client_manager.AlgoSdkClients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) + +Container for Algorand SDK client instances. + +Holds references to Algod, Indexer and KMD clients. + +* **Parameters:** + * **algod** – Algod client instance + * **indexer** – Optional Indexer client instance + * **kmd** – Optional KMD client instance + +#### algod + +#### indexer *= None* + +#### kmd *= None* + +### *class* algokit_utils.clients.client_manager.NetworkDetail + +Details about an Algorand network. + +Contains network type flags and genesis information. + +#### is_testnet *: bool* + +#### is_mainnet *: bool* + +#### is_localnet *: bool* + +#### genesis_id *: str* + +#### genesis_hash *: str* + +### *class* algokit_utils.clients.client_manager.ClientManager(clients_or_configs: [algokit_utils.models.network.AlgoClientConfigs](../../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) | [AlgoSdkClients](#algokit_utils.clients.client_manager.AlgoSdkClients), algorand_client: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) + +Manager for Algorand SDK clients. + +Provides access to Algod, Indexer and KMD clients and helper methods for working with them. + +* **Parameters:** + * **clients_or_configs** – Either client instances or client configurations + * **algorand_client** – AlgorandClient instance + +#### *property* algod *: algosdk.v2client.algod.AlgodClient* + +Returns an algosdk Algod API client. + +* **Returns:** + Algod client instance + +#### *property* indexer *: algosdk.v2client.indexer.IndexerClient* + +Returns an algosdk Indexer API client. + +* **Raises:** + **ValueError** – If no Indexer client is configured +* **Returns:** + Indexer client instance + +#### *property* indexer_if_present *: algosdk.v2client.indexer.IndexerClient | None* + +Returns the Indexer client if configured, otherwise None. + +* **Returns:** + Indexer client instance or None + +#### *property* kmd *: algosdk.kmd.KMDClient* + +Returns an algosdk KMD API client. + +* **Raises:** + **ValueError** – If no KMD client is configured +* **Returns:** + KMD client instance + +#### network() → [NetworkDetail](#algokit_utils.clients.client_manager.NetworkDetail) + +Get details about the connected Algorand network. + +* **Returns:** + Network details including type and genesis information + +#### is_localnet() → bool + +Check if connected to a local network. + +* **Returns:** + True if connected to a local network + +#### is_testnet() → bool + +Check if connected to TestNet. + +* **Returns:** + True if connected to TestNet + +#### is_mainnet() → bool + +Check if connected to MainNet. + +* **Returns:** + True if connected to MainNet + +#### get_testnet_dispenser(auth_token: str | None = None, request_timeout: int | None = None) → [algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient](../dispenser_api_client/index.md#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient) + +Get a TestNet dispenser API client. + +* **Parameters:** + * **auth_token** – Optional authentication token + * **request_timeout** – Optional request timeout in seconds +* **Returns:** + TestNet dispenser client instance + +#### get_app_factory(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, version: str | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [algokit_utils.applications.app_factory.AppFactory](../../applications/app_factory/index.md#algokit_utils.applications.app_factory.AppFactory) + +Get an application factory for deploying smart contracts. + +* **Parameters:** + * **app_spec** – Application specification + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **version** – Optional version string + * **compilation_params** – Optional compilation parameters +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application factory instance + +#### get_app_client_by_id(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client for an existing application by ID. + +* **Parameters:** + * **app_spec** – Application specification + * **app_id** – Application ID + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application client instance + +#### get_app_client_by_network(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client for an existing application by network. + +* **Parameters:** + * **app_spec** – Application specification + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application client instance + +#### get_app_client_by_creator_and_name(creator_address: str, app_name: str, app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client by creator address and name. + +* **Parameters:** + * **creator_address** – Creator address + * **app_name** – Application name + * **app_spec** – Application specification + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Returns:** + Application client instance + +#### *static* get_algod_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.algod.AlgodClient + +Get an Algod client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + Algod client instance + +#### *static* get_algod_client_from_environment() → algosdk.v2client.algod.AlgodClient + +Get an Algod client from environment variables. + +* **Returns:** + Algod client instance + +#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.kmd.KMDClient + +Get a KMD client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + KMD client instance + +#### *static* get_kmd_client_from_environment() → algosdk.kmd.KMDClient + +Get a KMD client from environment variables. + +* **Returns:** + KMD client instance + +#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.indexer.IndexerClient + +Get an Indexer client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + Indexer client instance + +#### *static* get_indexer_client_from_environment() → algosdk.v2client.indexer.IndexerClient + +Get an Indexer client from environment variables. + +* **Returns:** + Indexer client instance + +#### *static* genesis_id_is_localnet(genesis_id: str | None) → bool + +Check if a genesis ID indicates a local network. + +* **Parameters:** + **genesis_id** – Genesis ID to check +* **Returns:** + True if genesis ID indicates a local network + +#### get_typed_app_client_by_creator_and_name(typed_client: type[TypedAppClientT], \*, creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None) → TypedAppClientT + +Get a typed application client by creator address and name. + +* **Parameters:** + * **typed_client** – Typed client class + * **creator_address** – Creator address + * **app_name** – Application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application client instance + +#### get_typed_app_client_by_id(typed_client: type[TypedAppClientT], \*, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → TypedAppClientT + +Get a typed application client by ID. + +* **Parameters:** + * **typed_client** – Typed client class + * **app_id** – Application ID + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application client instance + +#### get_typed_app_client_by_network(typed_client: type[TypedAppClientT], \*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → TypedAppClientT + +Returns a new typed client, resolves the app ID for the current network. + +Uses pre-determined network-specific app IDs specified in the ARC-56 app spec. +If no IDs are in the app spec or the network isn’t recognised, an error is thrown. + +* **Parameters:** + * **typed_client** – The typed client class to instantiate + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + The typed client instance + +#### get_typed_app_factory(typed_factory: type[TypedFactoryT], \*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, version: str | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → TypedFactoryT + +Get a typed application factory. + +* **Parameters:** + * **typed_factory** – Typed factory class + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **version** – Optional version string + * **compilation_params** – Optional compilation parameters +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application factory instance + +#### *static* get_config_from_environment_or_localnet() → [algokit_utils.models.network.AlgoClientConfigs](../../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) + +Retrieve client configuration from environment variables or fallback to localnet defaults. + +If ALGOD_SERVER is set in environment variables, it will use environment configuration, +otherwise it will use default localnet configuration. + +* **Returns:** + Configuration for algod, indexer, and optionally kmd + +#### *static* get_default_localnet_config(config_or_port: Literal['algod', 'indexer', 'kmd'] | int) → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Get default configuration for local network services. + +* **Parameters:** + **config_or_port** – Service name or port number +* **Returns:** + Client configuration for local network + +#### *static* get_algod_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the algod configuration from environment variables. +Will raise an error if ALGOD_SERVER environment variable is not set + +* **Returns:** + Algod client configuration + +#### *static* get_indexer_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the indexer configuration from environment variables. +Will raise an error if INDEXER_SERVER environment variable is not set + +* **Returns:** + Indexer client configuration + +#### *static* get_kmd_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the kmd configuration from environment variables. + +* **Returns:** + KMD client configuration + +#### *static* get_algonode_config(network: Literal['testnet', 'mainnet'], config: Literal['algod', 'indexer']) → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Returns the Algorand configuration to point to the free tier of the AlgoNode service. + +* **Parameters:** + * **network** – Which network to connect to - TestNet or MainNet + * **config** – Which algod config to return - Algod or Indexer +* **Returns:** + Configuration for the specified network and service diff --git a/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md b/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md new file mode 100644 index 00000000..c1f83d93 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md @@ -0,0 +1,82 @@ +# algokit_utils.clients.dispenser_api_client + +## Attributes + +| [`DISPENSER_ASSETS`](#algokit_utils.clients.dispenser_api_client.DISPENSER_ASSETS) | | +|--------------------------------------------------------------------------------------------------------|----| +| [`DISPENSER_REQUEST_TIMEOUT`](#algokit_utils.clients.dispenser_api_client.DISPENSER_REQUEST_TIMEOUT) | | +| [`DISPENSER_ACCESS_TOKEN_KEY`](#algokit_utils.clients.dispenser_api_client.DISPENSER_ACCESS_TOKEN_KEY) | | + +## Classes + +| [`DispenserApiConfig`](#algokit_utils.clients.dispenser_api_client.DispenserApiConfig) | | +|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`DispenserAssetName`](#algokit_utils.clients.dispenser_api_client.DispenserAssetName) | Enum where members are also (and must be) ints | +| [`DispenserAsset`](#algokit_utils.clients.dispenser_api_client.DispenserAsset) | | +| [`DispenserFundResponse`](#algokit_utils.clients.dispenser_api_client.DispenserFundResponse) | | +| [`DispenserLimitResponse`](#algokit_utils.clients.dispenser_api_client.DispenserLimitResponse) | | +| [`TestNetDispenserApiClient`](#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient) | Client for interacting with the [AlgoKit TestNet Dispenser API]([https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md)). | + +## Module Contents + +### *class* algokit_utils.clients.dispenser_api_client.DispenserApiConfig + +#### BASE_URL *= 'https://api.dispenser.algorandfoundation.tools'* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserAssetName + +Bases: `enum.IntEnum` + +Enum where members are also (and must be) ints + +#### ALGO *= 0* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserAsset + +#### asset_id *: int* + +#### decimals *: int* + +#### description *: str* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserFundResponse + +#### tx_id *: str* + +#### amount *: int* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserLimitResponse + +#### amount *: int* + +### algokit_utils.clients.dispenser_api_client.DISPENSER_ASSETS + +### algokit_utils.clients.dispenser_api_client.DISPENSER_REQUEST_TIMEOUT *= 15* + +### algokit_utils.clients.dispenser_api_client.DISPENSER_ACCESS_TOKEN_KEY *= 'ALGOKIT_DISPENSER_ACCESS_TOKEN'* + +### *class* algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient(auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT) + +Client for interacting with the [AlgoKit TestNet Dispenser API]([https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md)). +To get started create a new access token via algokit dispenser login –ci +and pass it to the client constructor as auth_token. +Alternatively set the access token as environment variable ALGOKIT_DISPENSER_ACCESS_TOKEN, +and it will be auto loaded. If both are set, the constructor argument takes precedence. + +Default request timeout is 15 seconds. Modify by passing request_timeout to the constructor. + +#### auth_token *: str* + +#### request_timeout *= 15* + +#### fund(address: str, amount: int, asset_id: int) → [DispenserFundResponse](#algokit_utils.clients.dispenser_api_client.DispenserFundResponse) + +Fund an account with Algos from the dispenser API + +#### refund(refund_txn_id: str) → None + +Register a refund for a transaction with the dispenser API + +#### get_limit(address: str) → [DispenserLimitResponse](#algokit_utils.clients.dispenser_api_client.DispenserLimitResponse) + +Get current limit for an account with Algos from the dispenser API diff --git a/docs/markdown/autoapi/algokit_utils/clients/index.md b/docs/markdown/autoapi/algokit_utils/clients/index.md new file mode 100644 index 00000000..8ae2dbc7 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/index.md @@ -0,0 +1,6 @@ +# algokit_utils.clients + +## Submodules + +* [algokit_utils.clients.client_manager](client_manager/index.md) +* [algokit_utils.clients.dispenser_api_client](dispenser_api_client/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/config/index.md b/docs/markdown/autoapi/algokit_utils/config/index.md new file mode 100644 index 00000000..fa34f032 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/config/index.md @@ -0,0 +1,102 @@ +# algokit_utils.config + +## Attributes + +| [`ALGOKIT_PROJECT_ROOT`](#algokit_utils.config.ALGOKIT_PROJECT_ROOT) | | +|----------------------------------------------------------------------------|----| +| [`ALGOKIT_CONFIG_FILENAME`](#algokit_utils.config.ALGOKIT_CONFIG_FILENAME) | | +| [`config`](#algokit_utils.config.config) | | + +## Classes + +| [`AlgoKitLogger`](#algokit_utils.config.AlgoKitLogger) | | +|------------------------------------------------------------|----------------------------------------------------------------------------| +| [`UpdatableConfig`](#algokit_utils.config.UpdatableConfig) | Class to manage and update configuration settings for the AlgoKit project. | + +## Module Contents + +### algokit_utils.config.ALGOKIT_PROJECT_ROOT + +### algokit_utils.config.ALGOKIT_CONFIG_FILENAME *= '.algokit.toml'* + +### *class* algokit_utils.config.AlgoKitLogger + +#### error(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an error message, optionally suppressing output + +#### exception(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an exception message, optionally suppressing output + +#### warning(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a warning message, optionally suppressing output + +#### info(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an info message, optionally suppressing output + +#### debug(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a debug message, optionally suppressing output + +#### verbose(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a verbose message (maps to debug), optionally suppressing output + +### *class* algokit_utils.config.UpdatableConfig + +Class to manage and update configuration settings for the AlgoKit project. + +Attributes: +: debug (bool): Indicates whether debug mode is enabled. + project_root (Path | None): The path to the project root directory. + trace_all (bool): Indicates whether to trace all operations. + trace_buffer_size_mb (int): The size of the trace buffer in megabytes. + max_search_depth (int): The maximum depth to search for a specific file. + populate_app_call_resources (bool): Indicates whether to populate app call resources. + +#### *property* logger *: [AlgoKitLogger](#algokit_utils.config.AlgoKitLogger)* + +#### *property* debug *: bool* + +Returns the debug status. + +#### *property* project_root *: pathlib.Path | None* + +Returns the project root path. + +#### *property* trace_all *: bool* + +Indicates whether to store simulation traces for all operations. + +#### *property* trace_buffer_size_mb *: int | float* + +Returns the size of the trace buffer in megabytes. + +#### *property* populate_app_call_resource *: bool* + +#### with_debug(func: collections.abc.Callable[[], str | None]) → None + +Executes a function with debug mode temporarily enabled. + +#### configure(\*, debug: bool | None = None, project_root: pathlib.Path | None = None, trace_all: bool = False, trace_buffer_size_mb: float = 256, max_search_depth: int = 10, populate_app_call_resources: bool = False) → None + +Configures various settings for the application. +Please note, when project_root is not specified, by default config will attempt to find the algokit.toml by +scanning the parent directories according to the max_search_depth parameter. +Alternatively value can also be set via the ALGOKIT_PROJECT_ROOT environment variable. +If you are executing the config from an algokit compliant project, you can simply call +config.configure(debug=True). + +* **Parameters:** + * **debug** – Indicates whether debug mode is enabled. + * **project_root** – The path to the project root directory. Defaults to None. + * **trace_all** – Indicates whether to trace all operations. Defaults to False. Which implies that + only the operations that are failed will be traced by default. + * **trace_buffer_size_mb** – The size of the trace buffer in megabytes. Defaults to 256 + * **max_search_depth** – The maximum depth to search for a specific file. Defaults to 10 + * **populate_app_call_resources** – Indicates whether to populate app call resources. Defaults to False + +### algokit_utils.config.config diff --git a/docs/markdown/autoapi/algokit_utils/errors/index.md b/docs/markdown/autoapi/algokit_utils/errors/index.md new file mode 100644 index 00000000..47a58848 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/errors/index.md @@ -0,0 +1,5 @@ +# algokit_utils.errors + +## Submodules + +* [algokit_utils.errors.logic_error](logic_error/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md b/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md new file mode 100644 index 00000000..f5039daf --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md @@ -0,0 +1,76 @@ +# algokit_utils.errors.logic_error + +## Exceptions + +| [`LogicError`](#algokit_utils.errors.logic_error.LogicError) | Common base class for all non-exit exceptions. | +|----------------------------------------------------------------|--------------------------------------------------| + +## Classes + +| [`LogicErrorData`](#algokit_utils.errors.logic_error.LogicErrorData) | dict() -> new empty dictionary | +|------------------------------------------------------------------------|----------------------------------| + +## Functions + +| [`parse_logic_error`](#algokit_utils.errors.logic_error.parse_logic_error)(→ LogicErrorData | None) | | +|-------------------------------------------------------------------------------------------------------|----| + +## Module Contents + +### *class* algokit_utils.errors.logic_error.LogicErrorData + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### transaction_id *: str* + +#### message *: str* + +#### pc *: int* + +### algokit_utils.errors.logic_error.parse_logic_error(error_str: str) → [LogicErrorData](#algokit_utils.errors.logic_error.LogicErrorData) | None + +### *exception* algokit_utils.errors.logic_error.LogicError(\*, logic_error_str: str, program: str, source_map: AlgoSourceMap | None, transaction_id: str, message: str, pc: int, logic_error: Exception | None = None, traces: list[[algokit_utils.models.simulate.SimulationTrace](../../models/simulate/index.md#algokit_utils.models.simulate.SimulationTrace)] | None = None, get_line_for_pc: collections.abc.Callable[[int], int | None] | None = None) + +Bases: `Exception` + +Common base class for all non-exit exceptions. + +#### logic_error *= None* + +#### logic_error_str + +#### source_map + +#### lines + +#### transaction_id + +#### message + +#### pc + +#### traces *= None* + +#### line_no + +#### trace(lines: int = 5) → str diff --git a/docs/markdown/autoapi/algokit_utils/index.md b/docs/markdown/autoapi/algokit_utils/index.md new file mode 100644 index 00000000..1b2f3707 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/index.md @@ -0,0 +1,24 @@ +# algokit_utils + +AlgoKit Python Utilities - a set of utilities for building solutions on Algorand + +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + +> from algokit_utils.accounts import KmdAccountManager +> from algokit_utils.applications import AppClient +> from algokit_utils.applications.app_spec import Arc52Contract +> etc. + +## Submodules + +* [algokit_utils.accounts](accounts/index.md) +* [algokit_utils.algorand](algorand/index.md) +* [algokit_utils.applications](applications/index.md) +* [algokit_utils.assets](assets/index.md) +* [algokit_utils.clients](clients/index.md) +* [algokit_utils.config](config/index.md) +* [algokit_utils.errors](errors/index.md) +* [algokit_utils.models](models/index.md) +* [algokit_utils.protocols](protocols/index.md) +* [algokit_utils.transactions](transactions/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/models/account/index.md b/docs/markdown/autoapi/algokit_utils/models/account/index.md new file mode 100644 index 00000000..6f0eca22 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/account/index.md @@ -0,0 +1,120 @@ +# algokit_utils.models.account + +## Attributes + +| [`DISPENSER_ACCOUNT_NAME`](#algokit_utils.models.account.DISPENSER_ACCOUNT_NAME) | | +|------------------------------------------------------------------------------------|----| + +## Classes + +| [`TransactionSignerAccount`](#algokit_utils.models.account.TransactionSignerAccount) | A basic transaction signer account. | +|----------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| [`SigningAccount`](#algokit_utils.models.account.SigningAccount) | Holds the private key and address for an account. | +| [`MultisigMetadata`](#algokit_utils.models.account.MultisigMetadata) | Metadata for a multisig account. | +| [`MultiSigAccount`](#algokit_utils.models.account.MultiSigAccount) | Account wrapper that supports partial or full multisig signing. | + +## Module Contents + +### algokit_utils.models.account.DISPENSER_ACCOUNT_NAME *= 'DISPENSER'* + +### *class* algokit_utils.models.account.TransactionSignerAccount + +A basic transaction signer account. + +#### address *: str* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +### *class* algokit_utils.models.account.SigningAccount + +Holds the private key and address for an account. + +Provides access to the account’s private key, address, public key and transaction signer. + +#### private_key *: str* + +Base64 encoded private key + +#### address *: str* *= ''* + +Address for this account + +#### *property* public_key *: bytes* + +The public key for this account. + +* **Returns:** + The public key as bytes + +#### *property* signer *: algosdk.atomic_transaction_composer.AccountTransactionSigner* + +Get an AccountTransactionSigner for this account. + +* **Returns:** + A transaction signer for this account + +#### *static* new_account() → [SigningAccount](#algokit_utils.models.account.SigningAccount) + +Create a new random account. + +* **Returns:** + A new Account instance + +### *class* algokit_utils.models.account.MultisigMetadata + +Metadata for a multisig account. + +Contains the version, threshold and addresses for a multisig account. + +#### version *: int* + +#### threshold *: int* + +#### addresses *: list[str]* + +### *class* algokit_utils.models.account.MultiSigAccount(multisig_params: [MultisigMetadata](#algokit_utils.models.account.MultisigMetadata), signing_accounts: list[[SigningAccount](#algokit_utils.models.account.SigningAccount)]) + +Account wrapper that supports partial or full multisig signing. + +Provides functionality to manage and sign transactions for a multisig account. + +* **Parameters:** + * **multisig_params** – The parameters for the multisig account + * **signing_accounts** – The list of accounts that can sign + +#### *property* params *: [MultisigMetadata](#algokit_utils.models.account.MultisigMetadata)* + +Get the parameters for the multisig account. + +* **Returns:** + The multisig account parameters + +#### *property* signing_accounts *: list[[SigningAccount](#algokit_utils.models.account.SigningAccount)]* + +Get the list of accounts that are present to sign. + +* **Returns:** + The list of signing accounts + +#### *property* address *: str* + +Get the address of the multisig account. + +* **Returns:** + The multisig account address + +#### *property* signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +Get the transaction signer for this multisig account. + +* **Returns:** + The multisig transaction signer + +#### sign(transaction: algosdk.transaction.Transaction) → algosdk.transaction.MultisigTransaction + +Sign the given transaction with all present signers. + +* **Parameters:** + **transaction** – Either a transaction object or a raw, partially signed transaction +* **Returns:** + The transaction signed by the present signers diff --git a/docs/markdown/autoapi/algokit_utils/models/amount/index.md b/docs/markdown/autoapi/algokit_utils/models/amount/index.md new file mode 100644 index 00000000..89160023 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/amount/index.md @@ -0,0 +1,108 @@ +# algokit_utils.models.amount + +## Classes + +| [`AlgoAmount`](#algokit_utils.models.amount.AlgoAmount) | Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. | +|-----------------------------------------------------------|--------------------------------------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.models.amount.AlgoAmount(amount: dict[str, int | decimal.Decimal]) + +Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. + +* **Parameters:** + **amount** – A dictionary containing either algos, algo, microAlgos, or microAlgo as key + and their corresponding value as an integer or Decimal. +* **Raises:** + **ValueError** – If an invalid amount format is provided. +* **Example:** + +```pycon +>>> amount = AlgoAmount({"algos": 1}) +>>> amount = AlgoAmount({"microAlgos": 1_000_000}) +``` + +#### *property* micro_algos *: int* + +Return the amount as a number in µAlgo. + +* **Returns:** + The amount in µAlgo. + +#### *property* micro_algo *: int* + +Return the amount as a number in µAlgo. + +* **Returns:** + The amount in µAlgo. + +#### *property* algos *: decimal.Decimal* + +Return the amount as a number in Algo. + +* **Returns:** + The amount in Algo. + +#### *property* algo *: decimal.Decimal* + +Return the amount as a number in Algo. + +* **Returns:** + The amount in Algo. + +#### *static* from_algos(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of Algo. + +* **Parameters:** + **amount** – The amount in Algo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_algos(1) +``` + +#### *static* from_algo(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of Algo. + +* **Parameters:** + **amount** – The amount in Algo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_algo(1) +``` + +#### *static* from_micro_algos(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of µAlgo. + +* **Parameters:** + **amount** – The amount in µAlgo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_micro_algos(1_000_000) +``` + +#### *static* from_micro_algo(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of µAlgo. + +* **Parameters:** + **amount** – The amount in µAlgo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_micro_algo(1_000_000) +``` diff --git a/docs/markdown/autoapi/algokit_utils/models/application/index.md b/docs/markdown/autoapi/algokit_utils/models/application/index.md new file mode 100644 index 00000000..1211df33 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/application/index.md @@ -0,0 +1,72 @@ +# algokit_utils.models.application + +## Classes + +| [`AppState`](#algokit_utils.models.application.AppState) | | +|----------------------------------------------------------------------------------|----| +| [`AppInformation`](#algokit_utils.models.application.AppInformation) | | +| [`CompiledTeal`](#algokit_utils.models.application.CompiledTeal) | | +| [`AppCompilationResult`](#algokit_utils.models.application.AppCompilationResult) | | +| [`AppSourceMaps`](#algokit_utils.models.application.AppSourceMaps) | | + +## Module Contents + +### *class* algokit_utils.models.application.AppState + +#### key_raw *: bytes* + +#### key_base64 *: str* + +#### value_raw *: bytes | None* + +#### value_base64 *: str | None* + +#### value *: str | int* + +### *class* algokit_utils.models.application.AppInformation + +#### app_id *: int* + +#### app_address *: str* + +#### approval_program *: bytes* + +#### clear_state_program *: bytes* + +#### creator *: str* + +#### global_state *: dict[str, [AppState](#algokit_utils.models.application.AppState)]* + +#### local_ints *: int* + +#### local_byte_slices *: int* + +#### global_ints *: int* + +#### global_byte_slices *: int* + +#### extra_program_pages *: int | None* + +### *class* algokit_utils.models.application.CompiledTeal + +#### teal *: str* + +#### compiled *: str* + +#### compiled_hash *: str* + +#### compiled_base64_to_bytes *: bytes* + +#### source_map *: algosdk.source_map.SourceMap | None* + +### *class* algokit_utils.models.application.AppCompilationResult + +#### compiled_approval *: [CompiledTeal](#algokit_utils.models.application.CompiledTeal)* + +#### compiled_clear *: [CompiledTeal](#algokit_utils.models.application.CompiledTeal)* + +### *class* algokit_utils.models.application.AppSourceMaps + +#### approval_source_map *: algosdk.source_map.SourceMap | None* *= None* + +#### clear_source_map *: algosdk.source_map.SourceMap | None* *= None* diff --git a/docs/markdown/autoapi/algokit_utils/models/index.md b/docs/markdown/autoapi/algokit_utils/models/index.md new file mode 100644 index 00000000..e0f53185 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/index.md @@ -0,0 +1,11 @@ +# algokit_utils.models + +## Submodules + +* [algokit_utils.models.account](account/index.md) +* [algokit_utils.models.amount](amount/index.md) +* [algokit_utils.models.application](application/index.md) +* [algokit_utils.models.network](network/index.md) +* [algokit_utils.models.simulate](simulate/index.md) +* [algokit_utils.models.state](state/index.md) +* [algokit_utils.models.transaction](transaction/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/models/network/index.md b/docs/markdown/autoapi/algokit_utils/models/network/index.md new file mode 100644 index 00000000..4d94f90c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/network/index.md @@ -0,0 +1,32 @@ +# algokit_utils.models.network + +## Classes + +| [`AlgoClientNetworkConfig`](#algokit_utils.models.network.AlgoClientNetworkConfig) | Connection details for connecting to an {py:class}\`algosdk.v2client.algod.AlgodClient\` or | +|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [`AlgoClientConfigs`](#algokit_utils.models.network.AlgoClientConfigs) | | + +## Module Contents + +### *class* algokit_utils.models.network.AlgoClientNetworkConfig + +Connection details for connecting to an {py:class}\`algosdk.v2client.algod.AlgodClient\` or +{py:class}\`algosdk.v2client.indexer.IndexerClient\` + +#### server *: str* + +URL for the service e.g. http://localhost:4001 or https://testnet-api.algonode.cloud + +#### token *: str | None* *= None* + +API Token to authenticate with the service + +#### port *: str | int | None* *= None* + +### *class* algokit_utils.models.network.AlgoClientConfigs + +#### algod_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig)* + +#### indexer_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig) | None* + +#### kmd_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig) | None* diff --git a/docs/markdown/autoapi/algokit_utils/models/simulate/index.md b/docs/markdown/autoapi/algokit_utils/models/simulate/index.md new file mode 100644 index 00000000..7c1fec4c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/simulate/index.md @@ -0,0 +1,18 @@ +# algokit_utils.models.simulate + +## Classes + +| [`SimulationTrace`](#algokit_utils.models.simulate.SimulationTrace) | | +|-----------------------------------------------------------------------|----| + +## Module Contents + +### *class* algokit_utils.models.simulate.SimulationTrace + +#### app_budget_added *: int | None* + +#### app_budget_consumed *: int | None* + +#### failure_message *: str | None* + +#### exec_trace *: dict[str, object]* diff --git a/docs/markdown/autoapi/algokit_utils/models/state/index.md b/docs/markdown/autoapi/algokit_utils/models/state/index.md new file mode 100644 index 00000000..61e3c040 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/state/index.md @@ -0,0 +1,55 @@ +# algokit_utils.models.state + +## Attributes + +| [`TealTemplateParams`](#algokit_utils.models.state.TealTemplateParams) | | +|--------------------------------------------------------------------------|----| +| [`BoxIdentifier`](#algokit_utils.models.state.BoxIdentifier) | | + +## Classes + +| [`BoxName`](#algokit_utils.models.state.BoxName) | | +|------------------------------------------------------------|-----------------------------------------------------------------------| +| [`BoxValue`](#algokit_utils.models.state.BoxValue) | | +| [`DataTypeFlag`](#algokit_utils.models.state.DataTypeFlag) | Enum where members are also (and must be) ints | +| [`BoxReference`](#algokit_utils.models.state.BoxReference) | Represents a box reference with a foreign app index and the box name. | + +## Module Contents + +### *class* algokit_utils.models.state.BoxName + +#### name *: str* + +#### name_raw *: bytes* + +#### name_base64 *: str* + +### *class* algokit_utils.models.state.BoxValue + +#### name *: [BoxName](#algokit_utils.models.state.BoxName)* + +#### value *: bytes* + +### *class* algokit_utils.models.state.DataTypeFlag + +Bases: `enum.IntEnum` + +Enum where members are also (and must be) ints + +#### BYTES *= 1* + +#### UINT *= 2* + +### algokit_utils.models.state.TealTemplateParams *: TypeAlias* *= Mapping[str, str | int | bytes] | dict[str, str | int | bytes]* + +### algokit_utils.models.state.BoxIdentifier *: TypeAlias* *= str | bytes | AccountTransactionSigner* + +### *class* algokit_utils.models.state.BoxReference(app_id: int, name: bytes | str) + +Bases: `algosdk.box_reference.BoxReference` + +Represents a box reference with a foreign app index and the box name. + +Args: +: app_index (int): index of the application in the foreign app array + name (bytes): key for the box in bytes diff --git a/docs/markdown/autoapi/algokit_utils/models/transaction/index.md b/docs/markdown/autoapi/algokit_utils/models/transaction/index.md new file mode 100644 index 00000000..ad9cbd9d --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/transaction/index.md @@ -0,0 +1,89 @@ +# algokit_utils.models.transaction + +## Attributes + +| [`Arc2TransactionNote`](#algokit_utils.models.transaction.Arc2TransactionNote) | | +|----------------------------------------------------------------------------------|----| +| [`TransactionNoteData`](#algokit_utils.models.transaction.TransactionNoteData) | | +| [`TransactionNote`](#algokit_utils.models.transaction.TransactionNote) | | + +## Classes + +| [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) | Base ARC-0002 transaction note structure | +|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| [`StringFormatArc2Note`](#algokit_utils.models.transaction.StringFormatArc2Note) | ARC-0002 note for string-based formats (m/b/u) | +| [`JsonFormatArc2Note`](#algokit_utils.models.transaction.JsonFormatArc2Note) | ARC-0002 note for JSON format | +| [`TransactionWrapper`](#algokit_utils.models.transaction.TransactionWrapper) | Wrapper around algosdk.transaction.Transaction with optional property validators | +| [`SendParams`](#algokit_utils.models.transaction.SendParams) | Parameters for sending a transaction | + +## Module Contents + +### *class* algokit_utils.models.transaction.BaseArc2Note + +Bases: `TypedDict` + +Base ARC-0002 transaction note structure + +#### dapp_name *: str* + +### *class* algokit_utils.models.transaction.StringFormatArc2Note + +Bases: [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) + +ARC-0002 note for string-based formats (m/b/u) + +#### format *: Literal['m', 'b', 'u']* + +#### data *: str* + +### *class* algokit_utils.models.transaction.JsonFormatArc2Note + +Bases: [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) + +ARC-0002 note for JSON format + +#### format *: Literal['j']* + +#### data *: str | dict[str, Any] | list[Any] | int | None* + +### algokit_utils.models.transaction.Arc2TransactionNote + +### algokit_utils.models.transaction.TransactionNoteData + +### algokit_utils.models.transaction.TransactionNote + +### *class* algokit_utils.models.transaction.TransactionWrapper(transaction: algosdk.transaction.Transaction) + +Bases: `algosdk.transaction.Transaction` + +Wrapper around algosdk.transaction.Transaction with optional property validators + +#### *property* raw *: algosdk.transaction.Transaction* + +#### *property* payment *: algosdk.transaction.PaymentTxn* + +#### *property* keyreg *: algosdk.transaction.KeyregTxn* + +#### *property* asset_config *: algosdk.transaction.AssetConfigTxn* + +#### *property* asset_transfer *: algosdk.transaction.AssetTransferTxn* + +#### *property* asset_freeze *: algosdk.transaction.AssetFreezeTxn* + +#### *property* application_call *: algosdk.transaction.ApplicationCallTxn* + +#### *property* state_proof *: algosdk.transaction.StateProofTxn* + +### *class* algokit_utils.models.transaction.SendParams + +Bases: `TypedDict` + +Parameters for sending a transaction + +#### max_rounds_to_wait *: int | None* + +#### suppress_log *: bool | None* + +#### populate_app_call_resources *: bool | None* + +#### cover_app_call_inner_transaction_fees *: bool | None* diff --git a/docs/markdown/autoapi/algokit_utils/protocols/account/index.md b/docs/markdown/autoapi/algokit_utils/protocols/account/index.md new file mode 100644 index 00000000..190f37ab --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/account/index.md @@ -0,0 +1,23 @@ +# algokit_utils.protocols.account + +## Classes + +| [`TransactionSignerAccountProtocol`](#algokit_utils.protocols.account.TransactionSignerAccountProtocol) | An account that has a transaction signer. | +|-----------------------------------------------------------------------------------------------------------|---------------------------------------------| + +## Module Contents + +### *class* algokit_utils.protocols.account.TransactionSignerAccountProtocol + +Bases: `Protocol` + +An account that has a transaction signer. +Implemented by SigningAccount, LogicSigAccount, MultiSigAccount and TransactionSignerAccount abstractions. + +#### *property* address *: str* + +The address of the account. + +#### *property* signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +The transaction signer for the account. diff --git a/docs/markdown/autoapi/algokit_utils/protocols/index.md b/docs/markdown/autoapi/algokit_utils/protocols/index.md new file mode 100644 index 00000000..8796fff2 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/index.md @@ -0,0 +1,6 @@ +# algokit_utils.protocols + +## Submodules + +* [algokit_utils.protocols.account](account/index.md) +* [algokit_utils.protocols.typed_clients](typed_clients/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md b/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md new file mode 100644 index 00000000..751ea934 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md @@ -0,0 +1,97 @@ +# algokit_utils.protocols.typed_clients + +## Classes + +| [`TypedAppClientProtocol`](#algokit_utils.protocols.typed_clients.TypedAppClientProtocol) | Base class for protocol classes. | +|---------------------------------------------------------------------------------------------|------------------------------------| +| [`TypedAppFactoryProtocol`](#algokit_utils.protocols.typed_clients.TypedAppFactoryProtocol) | Base class for protocol classes. | + +## Module Contents + +### *class* algokit_utils.protocols.typed_clients.TypedAppClientProtocol(\*, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) + +Bases: `Protocol` + +Base class for protocol classes. + +Protocol classes are defined as: + +```default +class Proto(Protocol): + def meth(self) -> int: + ... +``` + +Such classes are primarily used with static type checkers that recognize +structural subtyping (static duck-typing). + +For example: + +```default +class C: + def meth(self) -> int: + return 0 + +def func(x: Proto) -> int: + return x.meth() + +func(C()) # Passes static type check +``` + +See PEP 544 for details. Protocol classes decorated with +@typing.runtime_checkable act as simple-minded runtime protocols that check +only the presence of given attributes, ignoring their type signatures. +Protocol classes can be generic, they are defined as: + +```default +class GenProto[T](Protocol): + def meth(self) -> T: + ... +``` + +#### *classmethod* from_creator_and_name(\*, creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) → typing_extensions.Self + +#### *classmethod* from_network(\*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) → typing_extensions.Self + +### *class* algokit_utils.protocols.typed_clients.TypedAppFactoryProtocol(algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), \*\*kwargs: Any) + +Bases: `Protocol`, `Generic`[`CreateParamsT`, `UpdateParamsT`, `DeleteParamsT`] + +Base class for protocol classes. + +Protocol classes are defined as: + +```default +class Proto(Protocol): + def meth(self) -> int: + ... +``` + +Such classes are primarily used with static type checkers that recognize +structural subtyping (static duck-typing). + +For example: + +```default +class C: + def meth(self) -> int: + return 0 + +def func(x: Proto) -> int: + return x.meth() + +func(C()) # Passes static type check +``` + +See PEP 544 for details. Protocol classes decorated with +@typing.runtime_checkable act as simple-minded runtime protocols that check +only the presence of given attributes, ignoring their type signatures. +Protocol classes can be generic, they are defined as: + +```default +class GenProto[T](Protocol): + def meth(self) -> T: + ... +``` + +#### deploy(\*, on_update: algokit_utils.applications.app_deployer.OnUpdate | None = None, on_schema_break: algokit_utils.applications.app_deployer.OnSchemaBreak | None = None, create_params: CreateParamsT | None = None, update_params: UpdateParamsT | None = None, delete_params: DeleteParamsT | None = None, existing_deployments: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, ignore_cache: bool = False, app_name: str | None = None, send_params: algokit_utils.models.SendParams | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → tuple[[TypedAppClientProtocol](#algokit_utils.protocols.typed_clients.TypedAppClientProtocol), [algokit_utils.applications.app_factory.AppFactoryDeployResult](../../applications/app_factory/index.md#algokit_utils.applications.app_factory.AppFactoryDeployResult)] diff --git a/docs/markdown/autoapi/algokit_utils/transactions/index.md b/docs/markdown/autoapi/algokit_utils/transactions/index.md new file mode 100644 index 00000000..7455d34c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/index.md @@ -0,0 +1,7 @@ +# algokit_utils.transactions + +## Submodules + +* [algokit_utils.transactions.transaction_composer](transaction_composer/index.md) +* [algokit_utils.transactions.transaction_creator](transaction_creator/index.md) +* [algokit_utils.transactions.transaction_sender](transaction_sender/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md new file mode 100644 index 00000000..6000abcd --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md @@ -0,0 +1,841 @@ +# algokit_utils.transactions.transaction_composer + +## Attributes + +| [`MethodCallParams`](#algokit_utils.transactions.transaction_composer.MethodCallParams) | | +|-------------------------------------------------------------------------------------------------------------------------|----| +| [`AppMethodCallTransactionArgument`](#algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument) | | +| [`TxnParams`](#algokit_utils.transactions.transaction_composer.TxnParams) | | + +## Classes + +| [`PaymentParams`](#algokit_utils.transactions.transaction_composer.PaymentParams) | Parameters for a payment transaction. | +|---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [`AssetCreateParams`](#algokit_utils.transactions.transaction_composer.AssetCreateParams) | Parameters for creating a new asset. | +| [`AssetConfigParams`](#algokit_utils.transactions.transaction_composer.AssetConfigParams) | Parameters for configuring an existing asset. | +| [`AssetFreezeParams`](#algokit_utils.transactions.transaction_composer.AssetFreezeParams) | Parameters for freezing an asset. | +| [`AssetDestroyParams`](#algokit_utils.transactions.transaction_composer.AssetDestroyParams) | Parameters for destroying an asset. | +| [`OnlineKeyRegistrationParams`](#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams) | Parameters for online key registration. | +| [`OfflineKeyRegistrationParams`](#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams) | Parameters for offline key registration. | +| [`AssetTransferParams`](#algokit_utils.transactions.transaction_composer.AssetTransferParams) | Parameters for transferring an asset. | +| [`AssetOptInParams`](#algokit_utils.transactions.transaction_composer.AssetOptInParams) | Parameters for opting into an asset. | +| [`AssetOptOutParams`](#algokit_utils.transactions.transaction_composer.AssetOptOutParams) | Parameters for opting out of an asset. | +| [`AppCallParams`](#algokit_utils.transactions.transaction_composer.AppCallParams) | Parameters for calling an application. | +| [`AppCreateSchema`](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | dict() -> new empty dictionary | +| [`AppCreateParams`](#algokit_utils.transactions.transaction_composer.AppCreateParams) | Parameters for creating an application. | +| [`AppUpdateParams`](#algokit_utils.transactions.transaction_composer.AppUpdateParams) | Parameters for updating an application. | +| [`AppDeleteParams`](#algokit_utils.transactions.transaction_composer.AppDeleteParams) | Parameters for deleting an application. | +| [`AppCallMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams) | Parameters for a regular ABI method call. | +| [`AppCreateMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams) | Parameters for an ABI method call that creates an application. | +| [`AppUpdateMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams) | Parameters for an ABI method call that updates an application. | +| [`AppDeleteMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams) | Parameters for an ABI method call that deletes an application. | +| [`BuiltTransactions`](#algokit_utils.transactions.transaction_composer.BuiltTransactions) | Set of transactions built by TransactionComposer. | +| [`TransactionComposerBuildResult`](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) | Result of building transactions with TransactionComposer. | +| [`SendAtomicTransactionComposerResults`](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) | Results from sending an AtomicTransactionComposer transaction group. | +| [`TransactionComposer`](#algokit_utils.transactions.transaction_composer.TransactionComposer) | A class for composing and managing Algorand transactions. | + +## Functions + +| [`send_atomic_transaction_composer`](#algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer)(...) | Send an AtomicTransactionComposer transaction group. | +|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.transactions.transaction_composer.PaymentParams + +Bases: `_CommonTxnParams` + +Parameters for a payment transaction. + +* **Variables:** + * **receiver** – The account that will receive the ALGO + * **amount** – Amount to send + * **close_remainder_to** – If given, close the sender account and send the remaining balance to this address, + +defaults to None + +#### receiver *: str* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### close_remainder_to *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetCreateParams + +Bases: `_CommonTxnParams` + +Parameters for creating a new asset. + +* **Variables:** + * **total** – The total amount of the smallest divisible unit to create + * **decimals** – The amount of decimal places the asset should have, defaults to None + * **default_frozen** – Whether the asset is frozen by default in the creator address, defaults to None + * **manager** – The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + * **reserve** – The address that holds the uncirculated supply, defaults to None + * **freeze** – The address that can freeze the asset in any account, defaults to None + * **clawback** – The address that can clawback the asset from any account, defaults to None + * **unit_name** – The short ticker name for the asset, defaults to None + * **asset_name** – The full name of the asset, defaults to None + * **url** – The metadata URL for the asset, defaults to None + * **metadata_hash** – Hash of the metadata contained in the metadata URL, defaults to None + +#### total *: int* + +#### asset_name *: str | None* *= None* + +#### unit_name *: str | None* *= None* + +#### url *: str | None* *= None* + +#### decimals *: int | None* *= None* + +#### default_frozen *: bool | None* *= None* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +#### metadata_hash *: bytes | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetConfigParams + +Bases: `_CommonTxnParams` + +Parameters for configuring an existing asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **manager** – The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + * **reserve** – The address that holds the uncirculated supply, defaults to None + * **freeze** – The address that can freeze the asset in any account, defaults to None + * **clawback** – The address that can clawback the asset from any account, defaults to None + +#### asset_id *: int* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetFreezeParams + +Bases: `_CommonTxnParams` + +Parameters for freezing an asset. + +* **Variables:** + * **asset_id** – The ID of the asset + * **account** – The account to freeze or unfreeze + * **frozen** – Whether the assets in the account should be frozen + +#### asset_id *: int* + +#### account *: str* + +#### frozen *: bool* + +### *class* algokit_utils.transactions.transaction_composer.AssetDestroyParams + +Bases: `_CommonTxnParams` + +Parameters for destroying an asset. + +* **Variables:** + **asset_id** – ID of the asset + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams + +Bases: `_CommonTxnParams` + +Parameters for online key registration. + +* **Variables:** + * **vote_key** – The root participation public key + * **selection_key** – The VRF public key + * **vote_first** – The first round that the participation key is valid + * **vote_last** – The last round that the participation key is valid + * **vote_key_dilution** – The dilution for the 2-level participation key + * **state_proof_key** – The 64 byte state proof public key commitment, defaults to None + +#### vote_key *: str* + +#### selection_key *: str* + +#### vote_first *: int* + +#### vote_last *: int* + +#### vote_key_dilution *: int* + +#### state_proof_key *: bytes | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams + +Bases: `_CommonTxnParams` + +Parameters for offline key registration. + +* **Variables:** + **prevent_account_from_ever_participating_again** – Whether to prevent the account from ever participating again + +#### prevent_account_from_ever_participating_again *: bool* + +### *class* algokit_utils.transactions.transaction_composer.AssetTransferParams + +Bases: `_CommonTxnParams` + +Parameters for transferring an asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **amount** – Amount of the asset to transfer (smallest divisible unit) + * **receiver** – The account to send the asset to + * **clawback_target** – The account to take the asset from, defaults to None + * **close_asset_to** – The account to close the asset to, defaults to None + +#### asset_id *: int* + +#### amount *: int* + +#### receiver *: str* + +#### clawback_target *: str | None* *= None* + +#### close_asset_to *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetOptInParams + +Bases: `_CommonTxnParams` + +Parameters for opting into an asset. + +* **Variables:** + **asset_id** – ID of the asset + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_composer.AssetOptOutParams + +Bases: `_CommonTxnParams` + +Parameters for opting out of an asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **creator** – The creator address of the asset + +#### asset_id *: int* + +#### creator *: str* + +### *class* algokit_utils.transactions.transaction_composer.AppCallParams + +Bases: `_CommonTxnParams` + +Parameters for calling an application. + +* **Variables:** + * **on_complete** – The OnComplete action + * **app_id** – ID of the application, defaults to None + * **approval_program** – The program to execute for all OnCompletes other than ClearState, defaults to None + * **clear_state_program** – The program to execute for ClearState OnComplete, defaults to None + * **schema** – The state schema for the app. This is immutable, defaults to None + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **extra_pages** – Number of extra pages required for the programs, defaults to None + * **box_references** – Box references, defaults to None + +#### on_complete *: algosdk.transaction.OnComplete* + +#### app_id *: int | None* *= None* + +#### approval_program *: str | bytes | None* *= None* + +#### clear_state_program *: str | bytes | None* *= None* + +#### schema *: dict[str, int] | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### extra_pages *: int | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateSchema + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### global_ints *: int* + +#### global_byte_slices *: int* + +#### local_ints *: int* + +#### local_byte_slices *: int* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateParams + +Bases: `_CommonTxnParams` + +Parameters for creating an application. + +* **Variables:** + **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) + +or compiled teal (bytes) +:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) +or compiled teal (bytes) +:ivar schema: The state schema for the app. This is immutable, defaults to None +:ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None +:ivar args: Application arguments, defaults to None +:ivar account_references: Account references, defaults to None +:ivar app_references: App references, defaults to None +:ivar asset_references: Asset references, defaults to None +:ivar box_references: Box references, defaults to None +:ivar extra_program_pages: Number of extra pages required for the programs, defaults to None + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### schema *: [AppCreateSchema](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### extra_program_pages *: int | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppUpdateParams + +Bases: `_CommonTxnParams` + +Parameters for updating an application. + +* **Variables:** + * **app_id** – ID of the application + * **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) + +or compiled teal (bytes) +:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) +or compiled teal (bytes) +:ivar args: Application arguments, defaults to None +:ivar account_references: Account references, defaults to None +:ivar app_references: App references, defaults to None +:ivar asset_references: Asset references, defaults to None +:ivar box_references: Box references, defaults to None +:ivar on_complete: The OnComplete action, defaults to None + +#### app_id *: int* + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppDeleteParams + +Bases: `_CommonTxnParams` + +Parameters for deleting an application. + +* **Variables:** + * **app_id** – ID of the application + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **box_references** – Box references, defaults to None + * **on_complete** – The OnComplete action, defaults to DeleteApplicationOC + +#### app_id *: int* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete* + +### *class* algokit_utils.transactions.transaction_composer.AppCallMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for a regular ABI method call. + +* **Variables:** + * **app_id** – ID of the application + * **method** – The ABI method to call + * **args** – Arguments to the ABI method, either an ABI value, transaction with explicit signer, + +transaction, another method call, or None +:ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + +#### app_id *: int* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that creates an application. + +* **Variables:** + * **approval_program** – The program to execute for all OnCompletes other than ClearState + * **clear_state_program** – The program to execute for ClearState OnComplete + * **schema** – The state schema for the app, defaults to None + * **on_complete** – The OnComplete action (cannot be ClearState), defaults to None + * **extra_program_pages** – Number of extra pages required for the programs, defaults to None + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### schema *: [AppCreateSchema](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +#### extra_program_pages *: int | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that updates an application. + +* **Variables:** + * **app_id** – ID of the application + * **approval_program** – The program to execute for all OnCompletes other than ClearState + * **clear_state_program** – The program to execute for ClearState OnComplete + * **on_complete** – The OnComplete action, defaults to UpdateApplicationOC + +#### app_id *: int* + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### on_complete *: algosdk.transaction.OnComplete* + +### *class* algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that deletes an application. + +* **Variables:** + * **app_id** – ID of the application + * **on_complete** – The OnComplete action, defaults to DeleteApplicationOC + +#### app_id *: int* + +#### on_complete *: algosdk.transaction.OnComplete* + +### algokit_utils.transactions.transaction_composer.MethodCallParams + +### algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument + +### algokit_utils.transactions.transaction_composer.TxnParams + +### *class* algokit_utils.transactions.transaction_composer.BuiltTransactions + +Set of transactions built by TransactionComposer. + +* **Variables:** + * **transactions** – The built transactions + * **method_calls** – Any ABIMethod objects associated with any of the transactions in a map keyed by txn id + * **signers** – Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id + +#### transactions *: list[algosdk.transaction.Transaction]* + +#### method_calls *: dict[int, algosdk.abi.Method]* + +#### signers *: dict[int, algosdk.atomic_transaction_composer.TransactionSigner]* + +### *class* algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult + +Result of building transactions with TransactionComposer. + +* **Variables:** + * **atc** – The AtomicTransactionComposer instance + * **transactions** – The list of transactions with signers + * **method_calls** – Map of transaction index to ABI method + +#### atc *: algosdk.atomic_transaction_composer.AtomicTransactionComposer* + +#### transactions *: list[algosdk.atomic_transaction_composer.TransactionWithSigner]* + +#### method_calls *: dict[int, algosdk.abi.Method]* + +### *class* algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults + +Results from sending an AtomicTransactionComposer transaction group. + +* **Variables:** + * **group_id** – The group ID if this was a transaction group + * **confirmations** – The confirmation info for each transaction + * **tx_ids** – The transaction IDs that were sent + * **transactions** – The transactions that were sent + * **returns** – The ABI return values from any ABI method calls + * **simulate_response** – The simulation response if simulation was performed, defaults to None + +#### group_id *: str* + +#### confirmations *: list[algosdk.v2client.algod.AlgodResponseType]* + +#### tx_ids *: list[str]* + +#### transactions *: list[[algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)]* + +#### returns *: list[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)]* + +#### simulate_response *: dict[str, Any] | None* *= None* + +### algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer, algod: algosdk.v2client.algod.AlgodClient, \*, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Send an AtomicTransactionComposer transaction group. + +Executes a group of transactions atomically using the AtomicTransactionComposer. + +* **Parameters:** + * **atc** – The AtomicTransactionComposer instance containing the transaction group to send + * **algod** – The Algod client to use for sending the transactions + * **max_rounds_to_wait** – Maximum number of rounds to wait for confirmation, defaults to 5 + * **skip_waiting** – If True, don’t wait for transaction confirmation, defaults to False + * **suppress_log** – If True, suppress logging, defaults to None + * **populate_app_call_resources** – If True, populate app call resources, defaults to None + * **cover_app_call_inner_transaction_fees** – If True, cover app call inner transaction fees, defaults to None + * **additional_atc_context** – Additional context for the AtomicTransactionComposer +* **Returns:** + Results from sending the transaction group +* **Raises:** + * **Exception** – If there is an error sending the transactions + * **error** – If there is an error from the Algorand node + +### *class* algokit_utils.transactions.transaction_composer.TransactionComposer(algod: algosdk.v2client.algod.AlgodClient, get_signer: collections.abc.Callable[[str], algosdk.atomic_transaction_composer.TransactionSigner], get_suggested_params: collections.abc.Callable[[], algosdk.transaction.SuggestedParams] | None = None, default_validity_window: int | None = None, app_manager: [algokit_utils.applications.app_manager.AppManager](../../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager) | None = None) + +A class for composing and managing Algorand transactions. + +Provides a high-level interface for building and executing transaction groups using the Algosdk library. +Supports various transaction types including payments, asset operations, application calls, and key registrations. + +* **Parameters:** + * **algod** – An instance of AlgodClient used to get suggested params and send transactions + * **get_signer** – A function that takes an address and returns a TransactionSigner for that address + * **get_suggested_params** – Optional function to get suggested transaction parameters, + +defaults to using algod.suggested_params() +:param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 +:param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None + +#### add_transaction(transaction: algosdk.transaction.Transaction, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add a raw transaction to the composer. + +* **Parameters:** + * **transaction** – The transaction to add + * **signer** – Optional transaction signer, defaults to getting signer from transaction sender +* **Returns:** + The transaction composer instance for chaining + +#### add_payment(params: [PaymentParams](#algokit_utils.transactions.transaction_composer.PaymentParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add a payment transaction. + +* **Parameters:** + **params** – The payment transaction parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_create(params: [AssetCreateParams](#algokit_utils.transactions.transaction_composer.AssetCreateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset creation transaction. + +* **Parameters:** + **params** – The asset creation parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_config(params: [AssetConfigParams](#algokit_utils.transactions.transaction_composer.AssetConfigParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset configuration transaction. + +* **Parameters:** + **params** – The asset configuration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_freeze(params: [AssetFreezeParams](#algokit_utils.transactions.transaction_composer.AssetFreezeParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset freeze transaction. + +* **Parameters:** + **params** – The asset freeze parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_destroy(params: [AssetDestroyParams](#algokit_utils.transactions.transaction_composer.AssetDestroyParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset destruction transaction. + +* **Parameters:** + **params** – The asset destruction parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_transfer(params: [AssetTransferParams](#algokit_utils.transactions.transaction_composer.AssetTransferParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset transfer transaction. + +* **Parameters:** + **params** – The asset transfer parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_opt_in(params: [AssetOptInParams](#algokit_utils.transactions.transaction_composer.AssetOptInParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset opt-in transaction. + +* **Parameters:** + **params** – The asset opt-in parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_opt_out(params: [AssetOptOutParams](#algokit_utils.transactions.transaction_composer.AssetOptOutParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset opt-out transaction. + +* **Parameters:** + **params** – The asset opt-out parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_create(params: [AppCreateParams](#algokit_utils.transactions.transaction_composer.AppCreateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application creation transaction. + +* **Parameters:** + **params** – The application creation parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_update(params: [AppUpdateParams](#algokit_utils.transactions.transaction_composer.AppUpdateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application update transaction. + +* **Parameters:** + **params** – The application update parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_delete(params: [AppDeleteParams](#algokit_utils.transactions.transaction_composer.AppDeleteParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application deletion transaction. + +* **Parameters:** + **params** – The application deletion parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_call(params: [AppCallParams](#algokit_utils.transactions.transaction_composer.AppCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application call transaction. + +* **Parameters:** + **params** – The application call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_create_method_call(params: [AppCreateMethodCallParams](#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application creation method call transaction. + +* **Parameters:** + **params** – The application creation method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_update_method_call(params: [AppUpdateMethodCallParams](#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application update method call transaction. + +* **Parameters:** + **params** – The application update method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_delete_method_call(params: [AppDeleteMethodCallParams](#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application deletion method call transaction. + +* **Parameters:** + **params** – The application deletion method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_call_method_call(params: [AppCallMethodCallParams](#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application call method call transaction. + +* **Parameters:** + **params** – The application call method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_online_key_registration(params: [OnlineKeyRegistrationParams](#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an online key registration transaction. + +* **Parameters:** + **params** – The online key registration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_offline_key_registration(params: [OfflineKeyRegistrationParams](#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an offline key registration transaction. + +* **Parameters:** + **params** – The offline key registration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_atc(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an existing AtomicTransactionComposer’s transactions. + +* **Parameters:** + **atc** – The AtomicTransactionComposer to add +* **Returns:** + The transaction composer instance for chaining + +#### count() → int + +Get the total number of transactions. + +* **Returns:** + The number of transactions + +#### build() → [TransactionComposerBuildResult](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) + +Build the transaction group. + +* **Returns:** + The built transaction group result + +#### rebuild() → [TransactionComposerBuildResult](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) + +Rebuild the transaction group from scratch. + +* **Returns:** + The rebuilt transaction group result + +#### build_transactions() → [BuiltTransactions](#algokit_utils.transactions.transaction_composer.BuiltTransactions) + +Build and return the transactions without executing them. + +* **Returns:** + The built transactions result + +#### execute(\*, max_rounds_to_wait: int | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +#### send(params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Send the transaction group to the network. + +* **Parameters:** + **params** – Parameters for the send operation +* **Returns:** + The transaction send results +* **Raises:** + **Exception** – If the transaction fails + +#### simulate(allow_more_logs: bool | None = None, allow_empty_signatures: bool | None = None, allow_unnamed_resources: bool | None = None, extra_opcode_budget: int | None = None, exec_trace_config: algosdk.v2client.models.SimulateTraceConfig | None = None, simulation_round: int | None = None, skip_signatures: bool | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Simulate transaction group execution with configurable validation rules. + +* **Parameters:** + * **allow_more_logs** – Whether to allow more logs than the standard limit + * **allow_empty_signatures** – Whether to allow transactions with empty signatures + * **allow_unnamed_resources** – Whether to allow unnamed resources + * **extra_opcode_budget** – Additional opcode budget to allocate + * **exec_trace_config** – Configuration for execution tracing + * **simulation_round** – Round number to simulate at + * **skip_signatures** – Whether to skip signature validation +* **Returns:** + The simulation results + +#### *static* arc2_note(note: algokit_utils.models.transaction.Arc2TransactionNote) → bytes + +Create an encoded transaction note that follows the ARC-2 spec. + +[https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) + +* **Parameters:** + **note** – The ARC-2 note to encode +* **Returns:** + The encoded note bytes +* **Raises:** + **ValueError** – If the dapp_name is invalid diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md new file mode 100644 index 00000000..34ebfb70 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md @@ -0,0 +1,90 @@ +# algokit_utils.transactions.transaction_creator + +## Classes + +| [`AlgorandClientTransactionCreator`](#algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator) | A creator for Algorand transactions. | +|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------| + +## Module Contents + +### *class* algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator(new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)]) + +A creator for Algorand transactions. + +Provides methods to create various types of Algorand transactions including payments, +asset operations, application calls and key registrations. + +* **Parameters:** + **new_group** – A lambda that starts a new TransactionComposer transaction group + +#### *property* payment *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.PaymentParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.PaymentParams)], algosdk.transaction.Transaction]* + +Create a payment transaction to transfer Algo between accounts. + +#### *property* asset_create *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetCreateParams)], algosdk.transaction.Transaction]* + +Create a create Algorand Standard Asset transaction. + +#### *property* asset_config *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetConfigParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetConfigParams)], algosdk.transaction.Transaction]* + +Create an asset config transaction to reconfigure an existing Algorand Standard Asset. + +#### *property* asset_freeze *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetFreezeParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetFreezeParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset freeze transaction. + +#### *property* asset_destroy *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetDestroyParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetDestroyParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset destroy transaction. + +#### *property* asset_transfer *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetTransferParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetTransferParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset transfer transaction. + +#### *property* asset_opt_in *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetOptInParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptInParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset opt-in transaction. + +#### *property* asset_opt_out *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetOptOutParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptOutParams)], algosdk.transaction.Transaction]* + +Create an asset opt-out transaction. + +#### *property* app_create *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams)], algosdk.transaction.Transaction]* + +Create an application create transaction. + +#### *property* app_update *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppUpdateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams)], algosdk.transaction.Transaction]* + +Create an application update transaction. + +#### *property* app_delete *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppDeleteParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams)], algosdk.transaction.Transaction]* + +Create an application delete transaction. + +#### *property* app_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallParams)], algosdk.transaction.Transaction]* + +Create an application call transaction. + +#### *property* app_create_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application create call with ABI method call transaction. + +#### *property* app_update_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application update call with ABI method call transaction. + +#### *property* app_delete_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application delete call with ABI method call transaction. + +#### *property* app_call_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCallMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application call with ABI method call transaction. + +#### *property* online_key_registration *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams)], algosdk.transaction.Transaction]* + +Create an online key registration transaction. + +#### *property* offline_key_registration *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams)], algosdk.transaction.Transaction]* + +Create an offline key registration transaction. diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md new file mode 100644 index 00000000..c8b886d6 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md @@ -0,0 +1,278 @@ +# algokit_utils.transactions.transaction_sender + +## Classes + +| [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) | Base class for transaction results. | +|-----------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------| +| [`SendSingleAssetCreateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult) | Result of creating a new ASA (Algorand Standard Asset). | +| [`SendAppTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult) | Result of an application transaction. | +| [`SendAppUpdateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult) | Result of updating an application. | +| [`SendAppCreateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult) | Result of creating a new application. | +| [`AlgorandClientTransactionSender`](#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender) | Orchestrates sending transactions for AlgorandClient. | + +## Module Contents + +### *class* algokit_utils.transactions.transaction_sender.SendSingleTransactionResult + +Base class for transaction results. + +Represents the result of sending a single transaction. + +#### transaction *: [algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)* + +#### confirmation *: algosdk.v2client.algod.AlgodResponseType* + +#### group_id *: str* + +#### tx_id *: str | None* *= None* + +#### tx_ids *: list[str]* + +#### transactions *: list[[algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)]* + +#### confirmations *: list[algosdk.v2client.algod.AlgodResponseType]* + +#### returns *: list[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### *classmethod* from_composer_result(result: [algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults), index: int = -1) → typing_extensions.Self + +### *class* algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult + +Bases: [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Result of creating a new ASA (Algorand Standard Asset). + +Contains the asset ID of the newly created asset. + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_sender.SendAppTransactionResult + +Bases: [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `Generic`[`ABIReturnT`] + +Result of an application transaction. + +Contains the ABI return value if applicable. + +#### abi_return *: ABIReturnT | None* *= None* + +### *class* algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult + +Bases: [`SendAppTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[`ABIReturnT`] + +Result of updating an application. + +Contains the compiled approval and clear programs. + +#### compiled_approval *: Any | None* *= None* + +#### compiled_clear *: Any | None* *= None* + +### *class* algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult + +Bases: [`SendAppUpdateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[`ABIReturnT`] + +Result of creating a new application. + +Contains the app ID and address of the newly created application. + +#### app_id *: int* + +#### app_address *: str* + +### *class* algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender(new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)], asset_manager: [algokit_utils.assets.asset_manager.AssetManager](../../assets/asset_manager/index.md#algokit_utils.assets.asset_manager.AssetManager), app_manager: [algokit_utils.applications.app_manager.AppManager](../../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager), algod_client: algosdk.v2client.algod.AlgodClient) + +Orchestrates sending transactions for AlgorandClient. + +Provides methods to send various types of transactions including payments, +asset operations, and application calls. + +#### new_group() → [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Create a new transaction group. + +* **Returns:** + A new TransactionComposer instance + +#### payment(params: [algokit_utils.transactions.transaction_composer.PaymentParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.PaymentParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Send a payment transaction to transfer Algo between accounts. + +* **Parameters:** + * **params** – Payment transaction parameters + * **send_params** – Send parameters +* **Returns:** + Result of the payment transaction + +#### asset_create(params: [algokit_utils.transactions.transaction_composer.AssetCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetCreateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleAssetCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult) + +Create a new Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset creation parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the new asset ID + +#### asset_config(params: [algokit_utils.transactions.transaction_composer.AssetConfigParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetConfigParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Configure an existing Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset configuration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the configuration transaction + +#### asset_freeze(params: [algokit_utils.transactions.transaction_composer.AssetFreezeParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetFreezeParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Freeze or unfreeze an Algorand Standard Asset for an account. + +* **Parameters:** + * **params** – Asset freeze parameters + * **send_params** – Send parameters +* **Returns:** + Result of the freeze transaction + +#### asset_destroy(params: [algokit_utils.transactions.transaction_composer.AssetDestroyParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetDestroyParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Destroys an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset destruction parameters + * **send_params** – Send parameters +* **Returns:** + Result of the destroy transaction + +#### asset_transfer(params: [algokit_utils.transactions.transaction_composer.AssetTransferParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetTransferParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Transfer an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset transfer parameters + * **send_params** – Send parameters +* **Returns:** + Result of the transfer transaction + +#### asset_opt_in(params: [algokit_utils.transactions.transaction_composer.AssetOptInParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptInParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Opt an account into an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset opt-in parameters + * **send_params** – Send parameters +* **Returns:** + Result of the opt-in transaction + +#### asset_opt_out(\*, params: [algokit_utils.transactions.transaction_composer.AssetOptOutParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptOutParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, ensure_zero_balance: bool = True) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Opt an account out of an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset opt-out parameters + * **send_params** – Send parameters + * **ensure_zero_balance** – Check if account has zero balance before opt-out, defaults to True +* **Raises:** + **ValueError** – If account has non-zero balance or is not opted in +* **Returns:** + Result of the opt-out transaction + +#### app_create(params: [algokit_utils.transactions.transaction_composer.AppCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Create a new application. + +* **Parameters:** + * **params** – Application creation parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the new application ID and address + +#### app_update(params: [algokit_utils.transactions.transaction_composer.AppUpdateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppUpdateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Update an application. + +* **Parameters:** + * **params** – Application update parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the compiled programs + +#### app_delete(params: [algokit_utils.transactions.transaction_composer.AppDeleteParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Delete an application. + +* **Parameters:** + * **params** – Application deletion parameters + * **send_params** – Send parameters +* **Returns:** + Result of the deletion transaction + +#### app_call(params: [algokit_utils.transactions.transaction_composer.AppCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application. + +* **Parameters:** + * **params** – Application call parameters + * **send_params** – Send parameters +* **Returns:** + Result containing any ABI return value + +#### app_create_method_call(params: [algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s create method. + +* **Parameters:** + * **params** – Method call parameters for application creation + * **send_params** – Send parameters +* **Returns:** + Result containing the new application ID and address + +#### app_update_method_call(params: [algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppUpdateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s update method. + +* **Parameters:** + * **params** – Method call parameters for application update + * **send_params** – Send parameters +* **Returns:** + Result containing the compiled programs + +#### app_delete_method_call(params: [algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s delete method. + +* **Parameters:** + * **params** – Method call parameters for application deletion + * **send_params** – Send parameters +* **Returns:** + Result of the deletion transaction + +#### app_call_method_call(params: [algokit_utils.transactions.transaction_composer.AppCallMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s call method. + +* **Parameters:** + * **params** – Method call parameters + * **send_params** – Send parameters +* **Returns:** + Result containing any ABI return value + +#### online_key_registration(params: [algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Register an online key. + +* **Parameters:** + * **params** – Key registration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the registration transaction + +#### offline_key_registration(params: [algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Register an offline key. + +* **Parameters:** + * **params** – Key registration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the registration transaction diff --git a/docs/markdown/autoapi/algorand_client/index.md b/docs/markdown/autoapi/algorand_client/index.md new file mode 100644 index 00000000..e2034ce0 --- /dev/null +++ b/docs/markdown/autoapi/algorand_client/index.md @@ -0,0 +1 @@ +# algorand_client diff --git a/docs/markdown/autoapi/client_manager/index.md b/docs/markdown/autoapi/client_manager/index.md new file mode 100644 index 00000000..e73efa8f --- /dev/null +++ b/docs/markdown/autoapi/client_manager/index.md @@ -0,0 +1 @@ +# client_manager diff --git a/docs/markdown/autoapi/composer/index.md b/docs/markdown/autoapi/composer/index.md new file mode 100644 index 00000000..4cb259e0 --- /dev/null +++ b/docs/markdown/autoapi/composer/index.md @@ -0,0 +1 @@ +# composer diff --git a/docs/markdown/autoapi/index.md b/docs/markdown/autoapi/index.md new file mode 100644 index 00000000..32153723 --- /dev/null +++ b/docs/markdown/autoapi/index.md @@ -0,0 +1,48 @@ +# API Reference + +This page contains auto-generated API reference documentation [1](#f1). + +* [composer](composer/index.md) +* [algokit_utils](algokit_utils/index.md) + * [algokit_utils.accounts](algokit_utils/accounts/index.md) + * [algokit_utils.accounts.account_manager](algokit_utils/accounts/account_manager/index.md) + * [algokit_utils.accounts.kmd_account_manager](algokit_utils/accounts/kmd_account_manager/index.md) + * [algokit_utils.algorand](algokit_utils/algorand/index.md) + * [algokit_utils.applications](algokit_utils/applications/index.md) + * [algokit_utils.applications.abi](algokit_utils/applications/abi/index.md) + * [algokit_utils.applications.app_client](algokit_utils/applications/app_client/index.md) + * [algokit_utils.applications.app_deployer](algokit_utils/applications/app_deployer/index.md) + * [algokit_utils.applications.app_factory](algokit_utils/applications/app_factory/index.md) + * [algokit_utils.applications.app_manager](algokit_utils/applications/app_manager/index.md) + * [algokit_utils.applications.app_spec](algokit_utils/applications/app_spec/index.md) + * [algokit_utils.applications.app_spec.arc32](algokit_utils/applications/app_spec/arc32/index.md) + * [algokit_utils.applications.app_spec.arc56](algokit_utils/applications/app_spec/arc56/index.md) + * [algokit_utils.applications.enums](algokit_utils/applications/enums/index.md) + * [algokit_utils.assets](algokit_utils/assets/index.md) + * [algokit_utils.assets.asset_manager](algokit_utils/assets/asset_manager/index.md) + * [algokit_utils.clients](algokit_utils/clients/index.md) + * [algokit_utils.clients.client_manager](algokit_utils/clients/client_manager/index.md) + * [algokit_utils.clients.dispenser_api_client](algokit_utils/clients/dispenser_api_client/index.md) + * [algokit_utils.config](algokit_utils/config/index.md) + * [algokit_utils.errors](algokit_utils/errors/index.md) + * [algokit_utils.errors.logic_error](algokit_utils/errors/logic_error/index.md) + * [algokit_utils.models](algokit_utils/models/index.md) + * [algokit_utils.models.account](algokit_utils/models/account/index.md) + * [algokit_utils.models.amount](algokit_utils/models/amount/index.md) + * [algokit_utils.models.application](algokit_utils/models/application/index.md) + * [algokit_utils.models.network](algokit_utils/models/network/index.md) + * [algokit_utils.models.simulate](algokit_utils/models/simulate/index.md) + * [algokit_utils.models.state](algokit_utils/models/state/index.md) + * [algokit_utils.models.transaction](algokit_utils/models/transaction/index.md) + * [algokit_utils.protocols](algokit_utils/protocols/index.md) + * [algokit_utils.protocols.account](algokit_utils/protocols/account/index.md) + * [algokit_utils.protocols.typed_clients](algokit_utils/protocols/typed_clients/index.md) + * [algokit_utils.transactions](algokit_utils/transactions/index.md) + * [algokit_utils.transactions.transaction_composer](algokit_utils/transactions/transaction_composer/index.md) + * [algokit_utils.transactions.transaction_creator](algokit_utils/transactions/transaction_creator/index.md) + * [algokit_utils.transactions.transaction_sender](algokit_utils/transactions/transaction_sender/index.md) +* [client_manager](client_manager/index.md) +* [algorand_client](algorand_client/index.md) +* [account_manager](account_manager/index.md) + +* **[1]** Created with [sphinx-autoapi](https://github.com/readthedocs/sphinx-autoapi) diff --git a/docs/markdown/capabilities/account.md b/docs/markdown/capabilities/account.md index 54bd929e..a08b215b 100644 --- a/docs/markdown/capabilities/account.md +++ b/docs/markdown/capabilities/account.md @@ -1,32 +1,213 @@ # Account management -Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, idempotent KMD and environment variable injected accounts -that can be used to sign transactions as well as representing a sender address at the same time. +Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, rekeyed, multisig, transaction signer, idempotent KMD and environment variable injected accounts that can be used to sign transactions as well as representing a sender address at the same time. This significantly simplifies management of transaction signing. - +## `AccountManager` -## `Account` +The [`AccountManager`]() is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! -Encapsulates a private key with convenience properties for `address`, `signer` and `public_key`. +To get an instance of `AccountManager`, you can use either [`AlgorandClient`](algorand-client.md) via `algorand.account` or instantiate it directly: -There are various methods of obtaining an `Account` instance +```python +from algokit_utils import AccountManager -* `get_account`: Returns an `Account` instance with the private key loaded by convention based on the given name identifier: - * from an environment variable containing a mnemonic `{NAME}_MNEMONIC` OR - * loading the account from KMD ny name if it exists (LocalNet only) OR - * creating the account in KMD with associated name (LocalNet only) +account_manager = AccountManager(client_manager) +``` - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against - TestNet/MainNet will automatically resolve from environment variables -* `Account.new_account`: Returns a new `Account` using `algosdk.account.generate_account()` -* `Account(private_key)`: Load an existing account from a private key -* `Account(private_key, address)`: Load an existing account from a private key and address, useful for re-keyed accounts -* `get_account_from_mnemonic`: Load an existing account from a mnemonic -* `get_dispenser_account`: Gets a dispenser account that is funded by either: - * Using the LocalNet default account (LocalNet only) OR - * Loading an account from `DISPENSER_MNEMONIC` +## `TransactionSignerAccountProtocol` -If working with a LocalNet instance, there are some additional functions that rely on a KMD service being exposed: +The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. -* `create_kmd_wallet_account`, `get_kmd_wallet_account` or `get_or_create_kmd_wallet_account`: These functions allow retrieving a KMD wallet account by name, -* `get_localnet_default_account`: Gets default localnet account that is funded with algos +The following conform to `TransactionSignerAccountProtocol`: + +- [`TransactionSignerAccount`]() - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- [`SigningAccount`]() - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- [`LogicSigAccount`]() - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- [`MultisigAccount`]() - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` + +## Registering a signer + +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by [`AlgorandClient`](algorand-client.md) to automatically sign transactions by that sender. Any of the [methods]() within `AccountManager` that return an account will automatically register the signer with the sender. + +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of [account based objects]() that combine signer and sender (`TransactionSignerAccount` | `SigningAccount` | `LogicSigAccount` | `MultisigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: + +```python +algorand.account + .set_signer_from_account(TransactionSignerAccount(your_address, your_signer)) + .set_signer_from_account(SigningAccount.new_account()) + .set_signer_from_account( + LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)) + ) + .set_signer_from_account( + MultisigAccount( + MultisigMetadata( + version = 1, + threshold = 1, + addresses = ["ADDRESS1...", "ADDRESS2..."] + ), + [account1, account2] + ) + ) + .set_signer("SENDERADDRESS", transaction_signer) +``` + +## Default signer + +If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can [register a default signer](): + +```python +algorand.account.set_default_signer(my_default_signer) +``` + +## Get a signer + +[`AlgorandClient`](algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer]() for a given sender address: + +```python +signer = algorand.account.get_signer("SENDER_ADDRESS") +``` + +If there is no signer registered for that sender address it will either return the default signer ([if registered]()) or throw an exception. + +## Accounts + +In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`]() (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](algorand-client.md)): + +- [`algorand.account.from_environment(name, fund_with)`]() - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) + - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code + - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD +- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`]() - Registers and returns an account with secret key loaded by taking the mnemonic secret +- [`algorand.account.multisig(multisig_params, signing_accounts)`]() - Registers and returns a multisig account with one or more signing keys loaded +- [`algorand.account.rekeyed(sender, signer)`]() - Registers and returns an account representing the given rekeyed sender/signer combination +- [`algorand.account.random()`]() - Returns a new, cryptographically randomly generated account with private key loaded +- [`algorand.account.from_kmd()`]() - Returns an account with private key loaded from the given KMD wallet (identified by name) +- [`algorand.account.logicsig(program, args?)`]() - Returns an account that represents a logic signature + +### Underlying account classes + +While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. + +- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created +- [`SigningAccount`]() - An abstraction around `algosdk.Account` that supports rekeyed accounts +- `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object +- [`MultisigAccount`]() - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present + +### Dispenser + +- [`algorand.account.dispenserFromEnvironment()`]() - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- [`algorand.account.localNetDispenser()`]() - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account + +## Rekey account + +One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +> [!WARNING] +> Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. + +You can issue a transaction to rekey an account by using the [`algorand.account.rekeyAccount(account, rekeyTo, options)`]() function: + +- `account: string | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekeyTo: string | TransactionSignerAccount` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- An `options` object, which has: + - [Common transaction parameters](algorand-client.md#transaction-parameters) + - [Execution parameters](algorand-client.md#sending-a-single-transaction) + +You can also pass in `rekeyTo` as a [common transaction parameter](algorand-client.md#transaction-parameters) to any transaction. + +### Examples + +```python +# Basic example (with string addresses) + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", +}) + +# Basic example (with signer accounts) + +algorand.account.rekey_account({ + account: account1, + rekey_to: new_signer_account, +}) + +# Advanced example + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", + lease: "lease", + note: "note", + first_valid_round: 1000, + validity_window: 10, + extra_fee: AlgoAmount.from_micro_algos(1000), + static_fee: AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee: AlgoAmount.from_micro_algos(3000), + max_rounds_to_wait_for_confirmation: 5, + suppress_log: True, +}) + + +# Using a rekeyed account + +Note: if a signing account is passed into `algorand.account.rekey_account` then you don't need to call `rekeyed_account` to register the new signer + +rekeyed_account = algorand.account.rekey_account(account, new_account) +# rekeyed_account can be used to sign transactions on behalf of account... +``` + +## KMD account management + +When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for: + +- Accessing the private key of the default accounts that are pre-seeded with Algo so that other accounts can be funded and it’s possible to use LocalNet +- Idempotently creating new accounts against a name that will stay intact while the LocalNet instance is running without you needing to store private keys anywhere (i.e. completely automated) + +The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that’s needed. This code has been abstracted away into the `KmdAccountManager` class. + +To get an instance of the `KmdAccountManager` class you can access it from [`AlgorandClient`](algorand-client.md) via `algorand.account.kmd` or instantiate it directly (passing in a [`ClientManager`](client.md)): + +```python +from algokit_utils import KmdAccountManager + +kmd_account_manager = KmdAccountManager(client_manager) +``` + +The methods that are available are: + +- [`get_wallet_account(wallet_name, predicate?, sender?)`]()\` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- [`get_or_create_wallet_account(name, fund_with?)`]()\` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- [`get_localnet_dispenser_account()`]()\` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) + +```python +# Get a wallet account that seeded the LocalNet network +default_dispenser_account = kmd_account_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 +) +# Same as above, but dedicated method call for convenience +local_net_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +# Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD +# if creating it then fund it with 2 ALGO from the default dispenser account +new_account = kmd_account_manager.get_or_create_wallet_account( + "account1", + AlgoAmount.from_algos(2) +) +# This will return the same account as above since the name matches +existing_account = kmd_account_manager.get_or_create_wallet_account( + "account1" +) +``` + +Some of this functionality is directly exposed from [`AccountManager`](), which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via [`AlgorandClient`](algorand-client.md): + +```python +# Get and register LocalNet dispenser +local_net_dispenser = algorand.account.localnet_dispenser() +# Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD +dispenser = algorand.account.dispenser_from_environment() +# Get / create and register account from KMD idempotently by name +account1 = algorand.account.from_kmd("account1", AlgoAmount.from_algos(2)) +``` diff --git a/docs/markdown/capabilities/algorand-client.md b/docs/markdown/capabilities/algorand-client.md new file mode 100644 index 00000000..a94de631 --- /dev/null +++ b/docs/markdown/capabilities/algorand-client.md @@ -0,0 +1,191 @@ +# Algorand client + +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It’s the [default entrypoint](../index.md#id3) into AlgoKit Utils functionality. + +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](), e.g.: + +```python +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet +# configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=algod) +# Point to pre-created algod, indexer and kmd clients +algorand = AlgorandClient.from_clients(algod=algod, indexer=indexer, kmd=kmd) +# Point to custom configuration for algod +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod, indexer and kmd +algorand = AlgorandClient.from_config( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config +) +``` + +## Accessing SDK clients + +Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. + +```py +algorand = AlgorandClient.default_localnet() + +algod_client = algorand.client.algod +indexer_client = algorand.client.indexer +kmd_client = algorand.client.kmd +``` + +## Accessing manager class instances + +The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. + +- [`AccountManager`](account.md) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.setDefaultSigner(signer)` - + - `algorand.setSignerFromAccount(account)` - + - `algorand.setSigner(sender, signer)` +- [`AssetManager`](asset.md) via `algorand.asset` +- [`ClientManager`](client.md) via `algorand.client` + +## Creating and issuing transactions + +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](transaction-composer.md)). + +### Creating transactions + +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`]() class. Intellisense will guide you on the different options. + +The signature for the calls to send a single transaction usually look like: + +```python +algorand.create_transaction.{method}(params=TxnParams(...), send_params=SendParams(...)) -> Transaction: +``` + +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils` and consist of: + - `AppCallParams`, + - `AppCreateParams`, + - `AppDeleteParams`, + - `AppUpdateParams`, + - `AssetConfigParams`, + - `AssetCreateParams`, + - `AssetDestroyParams`, + - `AssetFreezeParams`, + - `AssetOptInParams`, + - `AssetOptOutParams`, + - `AssetTransferParams`, + - `OfflineKeyRegistrationParams`, + - `OnlineKeyRegistrationParams`, + - `PaymentParams`, +- `SendParams` is a typed dictionary exposing setting to apply during send operation: + - `max_rounds_to_wait_for_confirmation: int | None` - The number of rounds to wait for confirmation. By default until the latest lastValid has past. + - `suppress_log: bool | None` - Whether to suppress log messages from transaction send, default: do not suppress. + - `populate_app_call_resources: bool | None` - Whether to use simulate to automatically populate app call resources in the txn objects. Defaults to `Config.populateAppCallResources`. + - `cover_app_call_inner_transaction_fees: bool | None` - Whether to use simulate to automatically calculate required app call inner transaction fees and cover them in the parent app call transaction fee + +The return type for the ABI method call methods are slightly different: + +```python +algorand.createTransaction.app{call_type}_method_call(params=MethodCallParams(...), send_params=SendParams(...)) -> BuiltTransactions +``` + +MethodCallParams is a union type that can be any of the Algorand method call types, exact dataclasses can be imported from `algokit_utils` and consist of: + +- `AppCreateMethodCallParams`, +- `AppCallMethodCallParams`, +- `AppDeleteMethodCallParams`, +- `AppUpdateMethodCallParams`, + +Where `BuiltTransactions` looks like this: + +```python +@dataclass(frozen=True) +class BuiltTransactions: + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] + signers: dict[int, TransactionSigner] +``` + +This signifies the fact that an ABI method call can actually result in multiple transactions (which in turn may have different signers), that you need ABI metadata to be able to extract the return value from the transaction result. + +### Sending a single transaction + +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`]() class. Intellisense will guide you on the different options. + +Further documentation is present in the related capabilities: + +- [App management](app.md) +- [Asset management](asset.md) +- [Algo transfers](transfer.md) + +The signature for the calls to send a single transaction usually look like: + +`algorand.send.{method}(params=TxnParams, send_params=SendParams) -> SingleSendTransactionResult` + +- To get intellisense on the params, use your IDE’s intellisense keyboard shortcut (e.g. ctrl+space). +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils`. +- [`SendParams`]() a typed dictionary exposing setting to apply during send operation. +- [`SendSingleTransactionResult`]() is all of the information that is relevant when [sending a single transaction to the network](transaction.md#sending-a-transaction) + +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppressLog: true`. + +### Composing a group of transactions + +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{Type}()` methods on [`TransactionComposer`](transaction-composer.md) to add a series of transactions. + +```typescript +result = (algorand + .new_group() + .add_payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=1_000_000 # 1 Algo in microAlgos + ) + ) + .add_asset_opt_in( + AssetOptInParams( + sender="SENDERADDRESS", + asset_id=12345 + ) + ) + .send()) +``` + +`new_group()` returns a new [`TransactionComposer`](transaction-composer.md) instance, which can also return the group of transactions, simulate them and other things. + +### Transaction parameters + +To create a transaction you instantiate a relevant Transaction parameters dataclass from `algokit_utils.transactions import *` or `from algokit_utils import PaymentParams, AssetOptInParams, etc`. + +All transaction parameters share the following common base parameters: + +- [`CommonTransactionParams`]() + - `sender: str` - The address of the account sending the transaction. + - `signer: algosdk.TransactionSigner | TransactionSignerAccount | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured). + - `rekey_to: string | None` - Change the signing key of the sender to the given address. **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + - `note: bytes | str | None` - Note to attach to the transaction. Max of 1000 bytes. + - `lease: bytes | str | None` - Prevent multiple transactions with the same lease being included within the validity window. A [lease](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). + - Fee management + - `static_fee: AlgoAmount | None` - The static transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. + - `extra_fee: AlgoAmount | None` - The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. + - `max_fee: AlgoAmount | None` - Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. + - Round validity management + - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. + - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. + - `last_valid_round: bigint | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. + +Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. + +### Transaction configuration + +AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to 10, except in [automated testing](testing.md) where it’s set to 1000 when targeting LocalNet. +- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) +- `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/markdown/capabilities/amount.md b/docs/markdown/capabilities/amount.md new file mode 100644 index 00000000..5770ef43 --- /dev/null +++ b/docs/markdown/capabilities/amount.md @@ -0,0 +1,55 @@ +# Algo amount handling + +Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. + +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the modularity principle) you can safely and explicitly convert to microAlgo or Algo. + +To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. + +## `AlgoAmount` + +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or existing the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it’s easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn’t be!). + +To import the AlgoAmount class you can access it via: + +```python +from algokit_utils import AlgoAmount +``` + +### Creating an `AlgoAmount` + +There are a few ways to create an `AlgoAmount`: + +- Algo + - Constructor: `AlgoAmount({"algo": 10})` or `AlgoAmount({"algos": 10})` + - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` +- microAlgo + - Constructor: `AlgoAmount({"microAlgo": 10_000})` or `AlgoAmount({"microAlgos": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` + +### Extracting a value from `AlgoAmount` + +The `AlgoAmount` class has properties to return Algo and microAlgo: + +- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer + +`AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. + +You can also call `str(amount)` or use an `AlgoAmount` directly in string interpolation to convert it to a nice user-facing formatted amount expressed in microAlgo. + +### Additional Features + +The `AlgoAmount` class supports arithmetic operations: + +- Addition: `amount1 + amount2` +- Subtraction: `amount1 - amount2` +- Comparison operations: `<`, `<=`, `>`, `>=`, `==`, `!=` + +Example: + +```python +amount1 = AlgoAmount({"algo": 1}) +amount2 = AlgoAmount({"microAlgo": 500_000}) +total = amount1 + amount2 # Results in 1.5 Algo +``` diff --git a/docs/markdown/capabilities/app-client.md b/docs/markdown/capabilities/app-client.md index abf57d3b..c69ce15b 100644 --- a/docs/markdown/capabilities/app-client.md +++ b/docs/markdown/capabilities/app-client.md @@ -1,154 +1,347 @@ -# App client +# App client and App factory -Application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker). +> [!NOTE] +> This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client is a high productivity application client that works with ARC-0032 application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](app-deploy.md) and [App management](app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_app_client_call.py). +> [!NOTE] +> If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don’t know the app ID (deferred knowledge or the instance doesn’t exist yet on the blockchain) or you have multiple app IDs -## Design +## `AppFactory` -The design for the app client is based on a wrapper for parsing an [ARC-0032](https://github.com/algorandfoundation/ARCs/pull/150) application spec and wrapping the [App deployment](app-deploy.md) functionality and corresponding [design](app-deploy.md#id1). +The `AppFactory` is a class that, for a given app spec, allows you to create and deploy one or more app instances and to create one or more app clients to interact with those (or other) app instances. -## Creating an application client +To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: -There are two key ways of instantiating an ApplicationClient: +```python +# Minimal example +factory = algorand.get_app_factory( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", +) + +# Advanced example +factory = algorand.get_app_factory( + app_spec=parsed_arc32_or_arc56_app_spec, + default_sender="SENDERADDRESS", + app_name="OverriddenAppName", + version="2.0.0", + compilation_params={ + "updatable": True, + "deletable": False, + "deploy_time_params": { "ONE": 1, "TWO": "value" }, + } +) +``` + +## `AppClient` -1. By app ID - When needing to call an existing app by app ID or unconditionally create a new app. - The signature `ApplicationClient(algod_client, app_spec, app_id=..., ...)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `app_id`: The app_id of an existing application, or 0 if creating a new app -2. By creator and app name - When needing to deploy or find an app associated with a specific creator account and app name. - The signature `ApplicationClient(algod_client, app_spec, creator=..., indexer=..., app_lookup)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `creator`: The address or `Account` of the creator of the app for which to search for the deployed app under - * `indexer`: - * `app_lookup`: Optional if an indexer is provided, - * `app_name`: An overridden name to identify the contract with, otherwise `contract.name` is used from the app spec +The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). -Both approaches also allow specifying the following parameters that will be used as defaults for all application calls: +To get an instance of `AppClient` you can use either `AlgorandClient` or instantiate it directly: -* `signer`: `TransactionSigner` to sign transactions with. -* `sender`: Address to use for transaction signing, will be derived from the signer if not provided. -* `suggested_params`: Default `SuggestedParams` to use, will use current network suggested params by default +```python +# Minimal examples +app_client = AppClient.from_creator_and_name( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + creator_address="CREATORADDRESS", + algorand=algorand, +) + +app_client = AppClient( + AppClientParams( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + app_id=12345, + algorand=algorand, + ) +) + +app_client = AppClient.from_network( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + algorand=algorand, +) + +# Advanced example +app_client = AppClient( + AppClientParams( + app_spec=parsed_app_spec, + app_id=12345, + algorand=algorand, + app_name="OverriddenAppName", + default_sender="SENDERADDRESS", + approval_source_map=approval_teal_source_map, + clear_source_map=clear_teal_source_map, + ) +) +``` -Both approaches also allow specifying a mapping of template values via the `template_values` parameter, this will be used before compiling the application to replace any -`TMPL_` variables that may be in the TEAL. The `TMPL_UPDATABLE` and `TMPL_DELETABLE` variables used in some AlgoKit templates are handled by the `deploy` method, but should be included if -using `create` or `update` directly. +You can access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. -## Calling methods on the app +## Dynamically creating clients for a given app spec -There are various methods available on `ApplicationClient` that can be used to call an app: +The `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. -* `call`: Used to call methods with an on complete action of `no_op` -* `create`: Used to create an instance of the app, by using an `app_id` of 0, includes the approval and clear programs in the call -* `update`: Used to update an existing app, includes the approval and clear programs in the call, and is called with an on complete action of `update_application` -* `delete`: Used to remove an existing app, is called with an on complete action of `delete_application` -* `opt_in`: Used to opt in to an existing app, is called with an on complete action of `opt_in` -* `close_out`: Used to close out of an existing app, is called with an on complete action of `opt_in` -* `clear_state`: Used to unconditionally close out from an app, calls the clear program of an app +This is possible via two methods on the app factory: -### Specifying which method +- `factory.get_app_client_by_id(app_id, ...)` - Returns a new `AppClient` for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified. +- `factory.get_app_client_by_creator_and_name(creator_address, app_name, ...)` - Returns a new `AppClient`, resolving the app by creator address and name using AlgoKit app deployment semantics. Automatically populates app_name, default_sender and source maps from the factory if not specified. -All methods for calling an app that support ABI methods (everything except `clear_state`) take a parameter `call_abi_method` which can be used to specify which method to call. -The method selected can be specified explicitly, or allow the client to infer the method where possible, supported values are: +```python +app_client1 = factory.get_app_client_by_id(app_id=12345) +app_client2 = factory.get_app_client_by_id(app_id=12346) +app_client3 = factory.get_app_client_by_id( + app_id=12345, + default_sender="SENDER2ADDRESS" +) + +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" +) +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" +) +app_client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName", + ignore_cache=True, # Perform fresh indexer lookups + default_sender="SENDER2ADDRESS" +) +``` -* `None`: The default value, when `None` is passed the client will attempt to find any ABI method or bare method that is compatible with the provided arguments -* `False`: Indicates that an ABI method should not be used, and instead a bare method call is made -* `True`: Indicates that an ABI method should be used, and the client will attempt to find an ABI method that is compatible with the provided arguments -* `str`: If a string is provided, it will be interpreted as either an ABI signature specifying a method, or as an ABI method name -* `algosdk.abi.Method`: The specified ABI method will be called -* `ABIReturnSubroutine`: Any type that has a `method_spec` function that returns an `algosd.abi.Method` +## Creating and deploying an app -### ABI arguments +Once you have an app factory you can perform the following actions: -ABI arguments are passed as python keyword arguments e.g. to pass the ABI parameter `name` for the ABI method `hello` the following syntax is used `client.call("hello", name="world")` +- `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it’s an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. -### Transaction Parameters +> See [API docs]() for details on parameter signatures. -All methods for calling an app take an optional `transaction_parameters` argument, with the following supported parameters: +### Create -* `signer`: The `TransactionSigner` to use on the call. This overrides any signer specified on the client -* `sender`: The address of the sender to use on the call, must be able to be signed for by the `signer`. This overrides any sender specified on the client -* `suggested_params`: `SuggestedParams` to use on the call. This overrides any suggested_params specified on the client -* `note`: Note to include in the transaction -* `lease`: Lease parameter for the transaction -* `boxes`: A sequence of boxes to use in the transaction, this is a list of (app_index, box_name) tuples `[(0, "box_name"), (0, ...)]` -* `accounts`: Account references to include in the transaction -* `foreign_apps`: Foreign apps to include in the transaction -* `foreign_assets`: Foreign assets to include in the transaction -* `on_complete`: The on complete action to use for the transaction, only available when using `call` or `create` -* `extra_pages`: Additional pages to allocate when calling `create`, by default a sufficient amount will be calculated based on the current approval and clear. This can be overridden, if more is required - for a future update +The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: -Parameters can be passed as one of the dataclasses `CommonCallParameters`, `OnCompleteCallParameters`, `CreateCallParameters` (exact type depends on method used) +- You don’t need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs]() for details. ```python -client.call("hello", transaction_parameters=algokit_utils.OnCompleteCallParameters(signer=...)) +# Use no-argument bare-call +result, app_client = factory.send.bare.create() + +# Specify parameters for bare-call and override other parameters +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( + args=[bytes([1, 2, 3, 4])], + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { + "ONE": 1, + "TWO": "two", + }, + "updatable": True, + "deletable": False, + } +) + +# Specify parameters for ABI method call +result, app_client = factory.send.create( + AppClientMethodCallParams( + method="create_application", + args=[1, "something"] + ) +) ``` -Alternatively, parameters can be passed as a dictionary e.g. +## Updating and deleting an app + +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. + +## Calling the app + +You can construct a params object, transaction(s) and sign and send a transaction to call the app that a given `AppClient` instance is pointing to. + +This is done via the following properties: + +- `app_client.params.{method}(params)` - Params for an ABI method call +- `app_client.params.bare.{method}(params)` - Params for a bare call +- `app_client.create_transaction.{method}(params)` - Transaction(s) for an ABI method call +- `app_client.create_transaction.bare.{method}(params)` - Transaction for a bare call +- `app_client.send.{method}(params)` - Sign and send an ABI method call +- `app_client.send.bare.{method}(params)` - Sign and send a bare call + +Where `{method}` is one of: + +- `update` - An update call +- `opt_in` - An opt-in call +- `delete` - A delete application call +- `clear_state` - A clear state call (note: calls the clear program and only applies to bare calls) +- `close_out` - A close-out call +- `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) ```python -client.call("hello", transaction_parameters={"signer":...}) +call1 = app_client.send.update( + AppClientMethodCallParams( + method="update_abi", + args=["string_io"], + ), + compilation_params={"deploy_time_params": deploy_time_params} +) + +call2 = app_client.send.delete( + AppClientMethodCallParams( + method="delete_abi", + args=["string_io"] + ) +) + +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") +) + +call4 = app_client.send.bare.clear_state() + +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) +) + +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") +) ``` -## Composing calls +## Funding the app account -If multiple calls need to be made in a single transaction, the `compose_` method variants can be used. All these methods take an `AtomicTransactionComposer` as their first argument. -Once all the calls have been added to the ATC, it can then be executed. For example: +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you via `fund_app_account(params)`. -```python -from algokit_utils import ApplicationClient -from algosdk.atomic_transaction_composer import AtomicTransactionComposer +The input parameters are: -client = ApplicationClient(...) -atc = AtomicTransactionComposer() -client.compose_call(atc, "hello", name="world") -... # additional compose calls +- A `FundAppAccountParams` object, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client’s default sender if configured). -response = client.execute_atc(atc) +Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you’ll want to get the funding call as a transaction, e.g.: + +```python +result = app_client.send.call( + AppClientMethodCallParams( + method="bootstrap", + args=[ + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) + ) + ) + ], + box_references=["Box1"] + ) +) ``` +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. + ## Reading state +`AppClient` has a number of mechanisms to read state (global, local and box storage) from the app instance. + +### App spec methods + +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the generic methods give you. + +You can access this functionality via: + +- `app_client.state.global_state.{method}()` - Global state +- `app_client.state.local_state(address).{method}()` - Local state +- `app_client.state.box.{method}()` - Box storage + +Where `{method}` is one of: + +- `get_all()` - Returns all single-key state values in a dict keyed by the key name and the value a decoded ABI value. +- `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be bytes with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value dict. It’s recommended that this is only done when you have a unique `prefix` for the map otherwise there’s a high risk that incorrect values will be included in the map. + +```python +values = app_client.state.global_state.get_all() +value = app_client.state.local_state("ADDRESS").get_value("value1") +map_value = app_client.state.box.get_map_value("map1", "mapKey") +map_dict = app_client.state.global_state.get_map("myMap") +``` + +### Generic methods + There are various methods defined that let you read state from the smart contract app: -* `get_global_state` - Gets the current global state of the app -* `get_local_state` - Gets the current local state for the given account address +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`]() +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](). + +```python +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box") +box_name2: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box2") + +box_names = app_client.get_box_names() +box_value = app_client.get_box_value(box_name) +box_values = app_client.get_box_values([box_name, box_name2]) +box_abi_value = app_client.get_box_value_from_abi_type( + box_name, + algosdk.ABIStringType +) +box_abi_values = app_client.get_box_values_from_abi_type( + [box_name, box_name2], + algosdk.ABIStringType +) +``` ## Handling logic errors and diagnosing errors -Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, -exhaustion of opcode budget, or any number of other reasons. +Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons. When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](app-deploy.md#id2) you can expose debugging -information that makes it much easier to understand what’s happening. +The information in that error message can be parsed and when combined with the [source map from compilation]() you can expose debugging information that makes it much easier to understand what’s happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. + +The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -When an error is thrown then the resulting error that is re-thrown will be a `LogicError`, which has the following fields: +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](), which has the following fields: -* `logic_error`: Original exception -* `program`: Program source (if available) -* `source_map`: Source map used (if available) -* `transaction_id`: Transaction ID of failing transaction -* `message`: The error message -* `line_no`: The line number in the TEAL program that -* `traces`: A list of Trace objects providing additional insights on simulation when debug mode is active. +- `logic_error: Exception` - The original logic error exception +- `logic_error_str: str` - The string representation of the logic error +- `program: str` - The TEAL program source code +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - Combined error message with debugging information +- `pc: int` - The program counter value where error occurred +- `traces: list[SimulationTrace] | None` - Simulation traces if debug enabled +- `line_no: int | None` - The line number in the TEAL source code +- `lines: list[str]` - The TEAL program split into individual lines -The function `trace()` will provide a formatted output of the surrounding TEAL where the error occurred. +Note: This information will only show if the app client / app factory has a source map. This will occur if: -#### NOTE -The extended information will only show if the Application Client has a source map. This will occur if: +- You have called `create`, `update` or `deploy` +- You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) +- You had source maps present in an app factory and then used it to [create an app client]() (they are automatically passed through) + +If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: + +```python +config.configure(debug=True) +``` -1.) The ApplicationClient instance has already called, `create, `update`or`deploy`OR 2.)`template_values`are provided when creating the ApplicationClient, so a SourceMap can be obtained automatically OR 3.)`approval_source_map`on`ApplicationClient`has been set from a previously compiled approval program OR 4.) A source map has been exported/imported using`export_source_map`/`import_source_map\`””” +If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. -### Debug Mode and traces Field +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](debugging.md). -When debug mode is active, the LogicError will contain a field named traces. This field will include raw simulate execution traces, providing a detailed account of the transaction simulation. These traces are crucial for diagnosing complex issues and are automatically included in all application client calls when debug mode is active. +## Default arguments -#### NOTE -Remember to enable debug mode (`config.debug = True`) to include raw simulate execution traces in the `LogicError`. +If an ABI method call specifies default argument values for any of its arguments you can pass in `None` for the value of that argument for the default value to be automatically populated. diff --git a/docs/markdown/capabilities/app-deploy.md b/docs/markdown/capabilities/app-deploy.md index 2fb69175..1db5a88c 100644 --- a/docs/markdown/capabilities/app-deploy.md +++ b/docs/markdown/capabilities/app-deploy.md @@ -1,19 +1,16 @@ # App deployment -Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution +AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet). -App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, -particularly [App management](app-client.md). It allows you to idempotently (with safe retryability) deploy an app, including deploy-time immutability and permanence control and -TEAL template substitution. +It’s optional to use this functionality, since you can construct your own deployment logic using create / update / delete calls and your own mechanism to maintaining app metadata (like app IDs etc.), but this capability is an opinionated out-of-the-box solution that takes care of the heavy lifting for you. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). +App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App management](app.md). - +To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -## Design +## Smart contract development lifecycle -The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). -While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. +The design behind the deployment capability is unique. The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. Namely, it described the concept of a smart contract development lifecycle: @@ -36,86 +33,182 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) and - [ARC-0004](https://arc.algorand.foundation/ARCs/arc-0004) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be - different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance -- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier - development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) -- Contracts are resolvable by a string “name” for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID - instead +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md), [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150) and [ARC-4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) +- Contracts are resolvable by a string “name” for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead + +This design allows you to have the same deployment code across environments without having to specify an ID for each environment. This makes it really easy to apply [continuous delivery](https://continuousdelivery.com/) practices to your smart contract deployment and make the deployment process completely automated. + +## `AppDeployer` -## Finding apps by creator +The [`AppDeployer`]() is a class that is used to manage app deployments and deployment metadata. + +To get an instance of `AppDeployer` you can use either [`AlgorandClient`](algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](app.md#appmanager), [`AlgorandClientTransactionSender`](algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): + +```python +from algokit_utils.app_deployer import AppDeployer + +app_deployer = AppDeployer(app_manager, transaction_sender, indexer) +``` -There is a method `algokit.get_creator_apps(creatorAccount, indexer)`, which performs a series of indexer lookups that return all apps created by the given creator. These are indexed by the name it -was deployed under if the creation transaction contained the following payload in the transaction note field: +## Deployment metadata + +When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. + +The deployment metadata is defined in [`AppDeployMetadata`](), which is an object with: + +- `name: str` - The unique name identifier of the app within the creator account +- `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) +- `deletable: bool | None` - Whether or not the app is deletable (`true`) / permanent (`false`) / unspecified (`None`) +- `updatable: bool | None` - Whether or not the app is updatable (`true`) / immutable (`false`) / unspecified (`None`) + +An example of the ARC-2 transaction note that is attached as an app creation / update transaction note to specify this metadata is: ```default -ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} +ALGOKIT_DEPLOYER:j{name:"MyApp",version:"1.0",updatable:true,deletable:false} ``` -Any creation transactions or update transactions are then retrieved and processed in chronological order to result in an `AppLookup` object +## Lookup deployed apps by name -Given there are a number of indexer calls to retrieve this data it’s a non-trivial object to create, and it’s recommended that for the duration you are performing a single deployment -you hold a value of it rather than recalculating it. Most AlgoKit Utils functions that need it will also take an optional value of it that will be used in preference to retrieving a -fresh version. +In order to resolve what apps have been previously deployed and their metadata, AlgoKit provides a method that does a series of indexer lookups and returns a map of name to app metadata via `get_creator_apps_by_name(creator_address)`. -## Deploying an application +```python +app_lookup = algorand.app_deployer.get_creator_apps_by_name("CREATORADDRESS") +app1_metadata = app_lookup.apps["app1"] +``` -The method that performs the deployment logic is the instance method `ApplicationClient.deploy`. It performs an idempotent (safely retryable) deployment. It will detect if the app already -exists and if it doesn’t it will create it. If the app does already exist then it will: +This method caches the result of the lookup, since it’s a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -- Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the - deployment configuration. +The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](): -It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. -This metadata works in concert with `get_creator_apps` to allow the app to be reliably retrieved against that creator in it’s currently deployed state. +```python +@dataclasses.dataclass +class ApplicationLookup: + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) +``` -`deploy` automatically executes [template substitution]() including deploy-time control of permanence and immutability. +The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](). + +> Refer to the [API docs]() for latest information on exact types. + +## Performing a deployment + +In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. + +For example: + +```python +deployment_result = algorand.app_deployer.deploy( + AppDeployParams( + metadata=AppDeploymentMetaData( + name="MyApp", + version="1.0.0", + deletable=False, + updatable=False, + ), + create_params=AppCreateParams( + sender="CREATORADDRESS", + approval_program=approval_teal_template_or_byte_code, + clear_state_program=clear_state_teal_template_or_byte_code, + schema=StateSchema( + global_ints=1, + global_byte_slices=2, + local_ints=3, + local_byte_slices=4, + ), + # Other parameters if a create call is made... + ), + update_params=AppUpdateParams( + sender="SENDERADDRESS", + # Other parameters if an update call is made... + ), + delete_params=AppDeleteParams( + sender="SENDERADDRESS", + # Other parameters if a delete call is made... + ), + deploy_time_params={ + "VALUE": 1, # TEAL template variables to replace + }, + on_schema_break=OnSchemaBreak.Append, + on_update=OnUpdate.Update, + send_params=SendParams( + populate_app_call_resources=True, + # Other execution control parameters + ), + ) +) +``` + +This method performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn’t it will create it. If the app does already exist then it will: + +- Detect if the app has been updated (i.e. the program logic has changed) and either fail, perform an update, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than were originally requested) and either fail, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. + +It will automatically [add metadata to the transaction note of the create or update transactions]() that indicates the name, version, updatability and deletability of the contract. This metadata works in concert with [`appDeployer.get_creator_apps_by_name`]() to allow the app to be reliably retrieved against that creator in it’s currently deployed state. It will automatically update it’s lookup cache so subsequent calls to `get_creator_apps_by_name` or `deploy` will use the latest metadata without needing to call indexer again. + +`deploy` also automatically executes [template substitution]() including deploy-time control of permanence and immutability if the requisite template parameters are specified in the provided TEAL template. ### Input parameters -The following inputs are used when deploying an App +The first parameter `deployment` is an [`AppDeployParams`](), which is an object with: -- `version`: The version string for the app defined in app_spec, if not specified the version will automatically increment for existing apps that are updated, and set to 1.0 for new apps -- `signer`, `sender`: Optional signer and sender for deployment operations, sender must be the same as the creator specified -- `allow_update`, `allow_delete`: Control the updatability and deletability of the app, used to populate `TMPL_UPDATABLE` and `TMPL_DELETABLE` template values -- `on_update`: Determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment) -- `on_schema_break`: Determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created) -- `create_args`: Args to use if a create operation is performed -- `update_args`: Args to use if an update operation is performed -- `delete_args`: Args to use if a delete operation is performed -- `template_values`: Values to use for automatic substitution of [deploy-time parameter values]() is mapping of `key: value` that will result in `TMPL_{key}` being replaced with `value` +- `metadata: AppDeployMetadata` - determines the [deployment metadata]() of the deployment +- `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](app.md#creation) (raw parameters or ABI method call) +- `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic +- `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter +- `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution]() + - [`TealTemplateParams`]() is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens]() if schema requirements increase (values: ‘replace’, ‘fail’, ‘append’) +- `on_update: OnUpdate | str | None` - determines [what happens]() if contract logic changes (values: ‘update’, ‘replace’, ‘fail’, ‘append’) +- `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries +- `ignore_cache: bool | None` - if True, bypasses cached deployment metadata +- Additional fields from [`SendParams`]() - transaction execution parameters ### Idempotency -`deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will -do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. - - +`deploy` is idempotent which means you can safely call it again multiple times and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. ### Compilation and template substitution -When compiling TEAL template code, the capabilities described in the [design above]() are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. +When compiling TEAL template code, the capabilities described in the [above design]() are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. -In order for a smart contract to be able to use this functionality, it must have a TEAL Template that contains the following: +In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn’t (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn’t (permanent) -If you are building a smart contract using the [beaker_production AlgoKit template](https://github.com/algorandfoundation/algokit-beaker-default-template) if provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. + +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result]() of substituting then compiling the TEAL template(s) in the following properties of the return value: + +- `compiled_approval: CompiledTeal | None` +- `compiled_clear: CompiledTeal | None` + +Template substitution is done by executing `algorand.app.compile_teal_template(teal_template_code, template_params, deployment_metadata)`, which in turn calls the following in order and returns the compilation result per above (all of which can also be invoked directly): + +- `AppManager.strip_teal_comments(teal_code)` - Strips out any TEAL comments to reduce the payload that is sent to algod and reduce the likelihood of hitting the max payload limit +- `AppManager.replace_template_variables(teal_template_code, template_values)` - Replaces the template variables by looking for `TMPL_{key}` +- `AppManager.replace_teal_template_deploy_time_control_params(teal_template_code, params)` - If `params` is provided, it allows for deploy-time immutability and permanence control by replacing `TMPL_UPDATABLE` with `params.get("updatable")` if not `None` and replacing `TMPL_DELETABLE` with `params.get("deletable")` if not `None` +- `algorand.app.compile_teal(teal_code)` - Sends the final TEAL to algod for compilation and returns the result including the source map and caches the compilation result within the `AppManager` instance ### Return value -`deploy` returns a `DeployResponse` object, that describes the action taken. - -- `action_taken`: Describes what happened during deployment - - `Create` - The smart contract app is created. - - `Update` - The smart contract app is updated - - `Replace` - The smart contract app was deleted and created again (in an atomic transaction) - - `Nothing` - Nothing was done since an existing up-to-date app was found -- `create_response`: If action taken was `Create` or `Replace`, the result of the create transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `update_response`: If action taken was `Update`, the result of the update transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `delete_response`: If action taken was `Replace`, the result of the delete transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `app`: An `AppMetaData` object, describing the final app state +When `deploy` executes it will return a [comprehensive result]() object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. + +The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): + +- `OperationPerformed.CREATE` - The smart contract app was created +- `OperationPerformed.UPDATE` - The smart contract app was updated +- `OperationPerformed.REPLACE` - The smart contract app was deleted and created again (in an atomic transaction) +- `OperationPerformed.NOTHING` - Nothing was done since it was detected the existing smart contract app deployment was up to date + +As well as the `operation_performed` parameter and the [optional compilation result](), the return value will have the [`ApplicationMetaData`]() [fields]() present. + +Based on the value of `operation_performed`, there will be other data available in the return value: + +- If `CREATE`, `UPDATE` or `REPLACE` then it will have the relevant [`SendAppTransactionResult`](app.md#calling-an-app) values: + - `create_result` for create operations + - `update_result` for update operations +- If `REPLACE` then it will also have `delete_result` to capture the result of deleting the existing app diff --git a/docs/markdown/capabilities/app.md b/docs/markdown/capabilities/app.md new file mode 100644 index 00000000..1cb907c3 --- /dev/null +++ b/docs/markdown/capabilities/app.md @@ -0,0 +1,163 @@ +# App management + +App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes). + +## `AppManager` + +The `AppManager` is a class that is used to manage app information. To get an instance of `AppManager` you can use either [`AlgorandClient`](algorand-client.md) via `algorand.app` or instantiate it directly (passing in an algod client instance): + +```python +from algokit_utils import AppManager + +app_manager = AppManager(algod_client) +``` + +## Calling apps + +### App Clients + +The recommended way of interacting with apps is via [App clients](app-client.md) and [App factory](app-client.md#appfactory). The methods shown on this page are the underlying mechanisms that app clients use and are for advanced use cases when you want more control. + +### Compilation + +The `AppManager` class allows you to compile TEAL code with caching semantics that allows you to avoid duplicate compilation and keep track of source maps from compiled code. + +```python +# Basic compilation +teal_code = "return 1" +compilation_result = app_manager.compile_teal(teal_code) + +# Get cached compilation result +cached_result = app_manager.get_compilation_result(teal_code) + +# Compile with template substitution +template_code = "int TMPL_VALUE" +template_params = {"VALUE": 1} +compilation_result = app_manager.compile_teal_template( + template_code, + template_params=template_params +) + +# Compile with deployment control (updatable/deletable) +control_template = f"""#pragma version 8 +int {UPDATABLE_TEMPLATE_NAME} +int {DELETABLE_TEMPLATE_NAME}""" +deployment_metadata = {"updatable": True, "deletable": True} +compilation_result = app_manager.compile_teal_template( + control_template, + deployment_metadata=deployment_metadata +) +``` + +The compilation result contains: + +- `teal` - Original TEAL code +- `compiled` - Base64 encoded compiled bytecode +- `compiled_hash` - Hash of compiled bytecode +- `compiled_base64_to_bytes` - Raw bytes of compiled bytecode +- `source_map` - Source map for debugging + +## Accessing state + +### Global state + +To access global state you can use: + +```python +# Get global state for app +global_state = app_manager.get_global_state(app_id) + +# Parse raw state from algod +decoded_state = AppManager.decode_app_state(raw_state) + +# Access state values +key_raw = decoded_state["value1"].key_raw # Raw bytes +key_base64 = decoded_state["value1"].key_base64 # Base64 encoded +value = decoded_state["value1"].value # Parsed value (str or int) +value_raw = decoded_state["value1"].value_raw # Raw bytes if bytes value +value_base64 = decoded_state["value1"].value_base64 # Base64 if bytes value +``` + +### Local state + +To access local state you can use: + +```python +local_state = app_manager.get_local_state(app_id, "ACCOUNT_ADDRESS") +``` + +### Boxes + +To access box storage: + +```python +# Get box names +box_names = app_manager.get_box_names(app_id) + +# Get box values +box_value = app_manager.get_box_value(app_id, box_name) +box_values = app_manager.get_box_values(app_id, [box_name1, box_name2]) + +# Get decoded ABI values +abi_value = app_manager.get_box_value_from_abi_type( + app_id, box_name, algosdk.abi.StringType() +) +abi_values = app_manager.get_box_values_from_abi_type( + app_id, [box_name1, box_name2], algosdk.abi.StringType() +) + +# Get box reference for transaction +box_ref = AppManager.get_box_reference(box_id) +``` + +## Getting app information + +To get app information: + +```python +# Get app info by ID +app_info = app_manager.get_by_id(app_id) + +# Get ABI return value from transaction +abi_return = AppManager.get_abi_return(confirmation, abi_method) +``` + +## Box references + +Box references can be specified in several ways: + +```python +# String name (encoded to bytes) +box_ref = "my_box" + +# Raw bytes +box_ref = b"my_box" + +# Account signer (uses address as name) +box_ref = account_signer + +# Box reference with app ID +box_ref = BoxReference(app_id=123, name=b"my_box") +``` + +## Common app parameters + +When interacting with apps (creating, updating, deleting, calling), there are common parameters that can be passed: + +- `app_id` - ID of the application +- `sender` - Address of transaction sender +- `signer` - Transaction signer (optional) +- `args` - Arguments to pass to the smart contract +- `account_references` - Account addresses to reference +- `app_references` - App IDs to reference +- `asset_references` - Asset IDs to reference +- `box_references` - Box references to load +- `on_complete` - On complete action +- Other common transaction parameters like `note`, `lease`, etc. + +For ABI method calls, additional parameters: + +- `method` - The ABI method to call +- `args` - ABI typed arguments to pass + +See [App client](app-client.md) for more details on constructing app calls. diff --git a/docs/markdown/capabilities/asset.md b/docs/markdown/capabilities/asset.md new file mode 100644 index 00000000..63a574b5 --- /dev/null +++ b/docs/markdown/capabilities/asset.md @@ -0,0 +1,134 @@ +# Assets + +The Algorand Standard Asset (ASA) management functions include creating, opting in and transferring assets, which are fundamental to asset interaction in a blockchain environment. + +## `AssetManager` + +The `AssetManager` class provides functionality for managing Algorand Standard Assets (ASAs). It can be accessed through the `AlgorandClient` via `algorand.asset` or instantiated directly: + +```python +from algokit_utils import AssetManager, TransactionComposer +from algosdk.v2client import algod + +asset_manager = AssetManager( + algod_client=algod_client, + new_group=lambda: TransactionComposer() +) +``` + +## Asset Information + +The `AssetManager` provides two key data classes for asset information: + +### `AssetInformation` + +Contains details about an Algorand Standard Asset (ASA): + +```python +@dataclass +class AssetInformation: + asset_id: int # The ID of the asset + creator: str # Address of the creator account + total: int # Total units created + decimals: int # Number of decimal places + default_frozen: bool | None = None # Whether asset is frozen by default + manager: str | None = None # Optional manager address + reserve: str | None = None # Optional reserve address + freeze: str | None = None # Optional freeze address + clawback: str | None = None # Optional clawback address + unit_name: str | None = None # Optional unit name (e.g. ticker) + asset_name: str | None = None # Optional asset name + url: str | None = None # Optional URL for more info + metadata_hash: bytes | None = None # Optional 32-byte metadata hash +``` + +### `AccountAssetInformation` + +Contains information about an account’s holding of a particular asset: + +```python +@dataclass +class AccountAssetInformation: + asset_id: int # The ID of the asset + balance: int # Amount held by the account + frozen: bool # Whether frozen for this account + round: int # Round this info was retrieved at +``` + +## Bulk Operations + +The `AssetManager` provides methods for bulk opt-in/opt-out operations: + +### Bulk Opt-In + +```python +# Basic example +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + signer=transaction_signer, + note=b"opt-in note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +### Bulk Opt-Out + +```python +# Basic example +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + ensure_zero_balance=True, + signer=transaction_signer, + note=b"opt-out note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +The bulk operations return a list of `BulkAssetOptInOutResult` objects containing: + +- `asset_id`: The ID of the asset opted into/out of +- `transaction_id`: The transaction ID of the opt-in/out + +## Get Asset Information + +### Getting Asset Parameters + +You can get the current parameters of an asset from algod using `get_by_id()`: + +```python +asset_info = asset_manager.get_by_id(12345) +``` + +### Getting Account Holdings + +You can get an account’s current holdings of an asset using `get_account_information()`: + +```python +address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +asset_id = 12345 +account_info = asset_manager.get_account_information(address, asset_id) +``` diff --git a/docs/markdown/capabilities/client.md b/docs/markdown/capabilities/client.md index 09973c11..1bc30811 100644 --- a/docs/markdown/capabilities/client.md +++ b/docs/markdown/capabilities/client.md @@ -1,29 +1,109 @@ # Client management -Client management is one of the core capabilities provided by AlgoKit Utils. -It allows you to create [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) -and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. +Client management is one of the core capabilities provided by AlgoKit Utils. It allows you to create (auto-retry) [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. -Any AlgoKit Utils function that needs one of these clients will take the underlying `algosdk` classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, -`algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#id1) principle you can use existing logic to get instances of these clients without needing to use the -Client management capability if you prefer. +Any AlgoKit Utils function that needs one of these clients will take the underlying algosdk classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, `algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#id1) principle you can use existing logic to get instances of these clients without needing to use the Client management capability if you prefer. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_network_clients.py). +## `ClientManager` + +The `ClientManager` is a class that is used to manage client instances. + +To get an instance of `ClientManager` you can instantiate it directly: + +```python +from algokit_utils import ClientManager, AlgoSdkClients, AlgoClientConfigs +from algosdk.v2client.algod import AlgodClient + +# Using AlgoSdkClients +algod_client = AlgodClient(...) +algorand_client = ... # Get AlgorandClient instance from somewhere +clients = AlgoSdkClients(algod=algod_client, indexer=indexer_client, kmd=kmd_client) +client_manager = ClientManager(clients, algorand_client) + +# Using AlgoClientConfigs +algod_config = AlgoClientNetworkConfig(server="https://...", token="") +configs = AlgoClientConfigs(algod_config=algod_config) +client_manager = ClientManager(configs, algorand_client) +``` + ## Network configuration -The network configuration is specified using the `AlgoClientConfig` class. This same interface is used to specify the config for algod, indexer and kmd clients. +The network configuration is specified using the `AlgoClientConfig` type. This same type is used to specify the config for [algod](https://developer.algorand.org/docs/sdks/python/), [indexer](https://developer.algorand.org/docs/sdks/python/) and [kmd](https://developer.algorand.org/docs/sdks/python/) SDK clients. There are a number of ways to produce one of these configuration objects: -- Manually creating the object, e.g. `AlgoClientConfig(server="https://myalgodnode.com", token="SECRET_TOKEN")` -- `algokit_utils.get_algonode_config(network, config, token)`: Loads an Algod or indexer config against [Nodely](https://nodely.io/docs/free/start) to either MainNet or TestNet -- `algokit_utils.get_default_localnet_config(configOrPort)`: Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration +- Manually specifying a dataclass, e.g. + ```python + from algokit_utils import AlgoClientNetworkConfig + + config = AlgoClientNetworkConfig( + server="https://myalgodnode.com", + token="SECRET_TOKEN" # optional + ) + ``` +- `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables +- `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algonode_config(network)` - Loads an Algod or indexer config against [AlgoNode free tier](https://nodely.io/docs/free/start) to either MainNet or TestNet +- `ClientManager.get_default_localnet_config()` - Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration ## Clients -Once you have the configuration for a client, to get the client you can use the following functions: +### Creating an SDK client instance + +Once you have the configuration for a client, to get a new client you can use the following functions: + +- `ClientManager.get_algod_client(config)` - Returns an Algod client for the given configuration; the client automatically retries on transient HTTP errors +- `ClientManager.get_indexer_client(config)` - Returns an Indexer client for given configuration +- `ClientManager.get_kmd_client(config)` - Returns a Kmd client for the given configuration + +You can also shortcut needing to write the likes of `ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())` with environment shortcut methods: + +- `ClientManager.get_algod_client_from_environment()` - Returns an Algod client by loading the config from environment variables +- `ClientManager.get_indexer_client_from_environment()` - Returns an indexer client by loading the config from environment variables +- `ClientManager.get_kmd_client_from_environment()` - Returns a kmd client by loading the config from environment variables + +### Accessing SDK clients via ClientManager instance + +Once you have a `ClientManager` instance, you can access the SDK clients: + +```python +client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) + +algod_client = client_manager.algod +indexer_client = client_manager.indexer +kmd_client = client_manager.kmd +``` + +If the method to create the `ClientManager` doesn’t configure indexer or kmd (both of which are optional), then accessing those clients will trigger an error. + +### Creating a TestNet dispenser API client instance + +You can also create a [TestNet dispenser API client instance](dispenser-client.md) from `ClientManager` too. + +## Automatic retry + +When receiving an Algod or Indexer client from AlgoKit Utils, it will be a special wrapper client that handles retrying transient failures. + +## Network information + +You can get information about the current network you are connected to: + +```python +# Get network information +network = client_manager.network() +print(f"Is mainnet: {network.is_mainnet}") +print(f"Is testnet: {network.is_testnet}") +print(f"Is localnet: {network.is_localnet}") +print(f"Genesis ID: {network.genesis_id}") +print(f"Genesis hash: {network.genesis_hash}") + +# Convenience methods +is_mainnet = client_manager.is_mainnet() +is_testnet = client_manager.is_testnet() +is_localnet = client_manager.is_localnet() +``` -- `algokit_utils.get_algod_client(config)`: Returns an Algod client for the given configuration or if none is provided retrieves a configuration from the environment using `ALGOD_SERVER`, `ALGOD_TOKEN` and optionally `ALGOD_PORT`. -- `algokit_utils.get_indexer_client(config)`: Returns an Indexer client for given configuration or if none is provided retrieves a configuration from the environment using `INDEXER_SERVER`, `INDEXER_TOKEN` and optionally `INDEXER_PORT` -- `algokit_utils.get_kmd_client_from_algod_client(config)`: - Returns a Kmd client based on the provided algod client configuration, with the assumption the KMD services is running on the same host but a different port (either `KMD_PORT` environment variable or `4002` by default) +The first time `network()` is called it will make a HTTP call to algod to get the network parameters, but from then on it will be cached within that `ClientManager` instance for subsequent calls. diff --git a/docs/markdown/capabilities/debugger.md b/docs/markdown/capabilities/debugger.md deleted file mode 100644 index ac23a73e..00000000 --- a/docs/markdown/capabilities/debugger.md +++ /dev/null @@ -1,45 +0,0 @@ -# Debugger - -The AlgoKit Python Utilities package provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AlgoKit AVM Debugger extension](https://marketplace.visualstudio.com/items?itemName=algorandfoundation.algokit-avm-vscode-debugger). - -## Configuration - -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: - -- `debug`: Indicates whether debug mode is enabled. -- `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. -- `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. -- `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. -- `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. - -The `configure` method can be used to set these attributes. - -To enable debug mode in your project you can configure it as follows: - -```py -from algokit_utils.config import config - -config.configure(debug=True) -``` - -## Debugging Utilities - -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: - -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. - -### Trace filename format - -The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: - -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; -``` - -Where: - -- `timestamp`: The time when the trace file was created, in ISO 8601 format, with colons and periods removed. -- `last_round`: The last round when the simulation was performed. -- `transaction_types`: A string representing the types and counts of transactions in the atomic group. Each transaction type is represented as `${count}${type}`, and different transaction types are separated by underscores. - -For example, a trace file might be named `20220301T123456Z_lr1000_2pay_1axfer.trace.avm.json`, indicating that the trace file was created at `2022-03-01T12:34:56Z`, the last round was `1000`, and the atomic group contained 2 payment transactions and 1 asset transfer transaction. diff --git a/docs/html/_sources/capabilities/debugger.md.txt b/docs/markdown/capabilities/debugging.md similarity index 63% rename from docs/html/_sources/capabilities/debugger.md.txt rename to docs/markdown/capabilities/debugging.md index ac23a73e..3230212b 100644 --- a/docs/html/_sources/capabilities/debugger.md.txt +++ b/docs/markdown/capabilities/debugging.md @@ -4,36 +4,64 @@ The AlgoKit Python Utilities package provides a set of debugging tools that can ## Configuration -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: +The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. - `debug`: Indicates whether debug mode is enabled. - `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. - `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. - `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. - `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. +- `populate_app_call_resources`: Indicates whether to populate app call resources. Defaults to false, which means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will not populate app call resources. The `configure` method can be used to set these attributes. To enable debug mode in your project you can configure it as follows: -```py +```python from algokit_utils.config import config -config.configure(debug=True) +config.configure( + debug=True, + project_root=Path("./my-project"), + trace_all=True, + trace_buffer_size_mb=512, + max_search_depth=15, + populate_app_call_resources=True, +) ``` ## Debugging Utilities -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: - -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. +When debug mode is enabled, AlgoKit Utils will automatically: + +- Generate transaction traces compatible with the AVM Debugger +- Manage trace file storage with automatic cleanup +- Provide source map generation for TEAL contracts + +The following methods are provided for manual debugging operations: + +- `persist_sourcemaps`: Persists sourcemaps for given TEAL contracts as AVM Debugger-compliant artifacts. Parameters: + - `sources`: List of TEAL sources to generate sourcemaps for + - `project_root`: Project root directory for storage + - `client`: AlgodClient instance + - `with_sources`: Whether to include TEAL source files (default: True) +- `simulate_and_persist_response`: Simulates transactions and persists debug traces. Parameters: + - `atc`: AtomicTransactionComposer containing transactions + - `project_root`: Project root directory for storage + - `algod_client`: AlgodClient instance + - `buffer_size_mb`: Maximum trace storage in MB (default: 256) + - `allow_empty_signatures`: Allow unsigned transactions (default: True) + - `allow_unnamed_resources`: Allow unnamed resources (default: True) + - `extra_opcode_budget`: Additional opcode budget + - `exec_trace_config`: Custom trace configuration + - `simulation_round`: Specific round to simulate ### Trace filename format The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; +```default +${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json ``` Where: diff --git a/docs/markdown/capabilities/dispenser-client.md b/docs/markdown/capabilities/dispenser-client.md index 9a8d4893..b04f8ef6 100644 --- a/docs/markdown/capabilities/dispenser-client.md +++ b/docs/markdown/capabilities/dispenser-client.md @@ -7,54 +7,85 @@ The TestNet Dispenser Client is a utility for interacting with the AlgoKit TestN To create a Dispenser Client, you need to provide an authorization token. This can be done in two ways: 1. Pass the token directly to the client constructor as `auth_token`. -2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) on how to obtain the token). +2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) on how to obtain the token). If both methods are used, the constructor argument takes precedence. -```py +```python +import algokit_utils + +# With auth token +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", +) + +# With auth token and timeout +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", + request_timeout=2, # seconds +) + +# From environment variables +# i.e. os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' +dispenser = algorand.client.get_testnet_dispenser_from_environment() + +# Alternatively, you can construct it directly from algokit_utils import TestNetDispenserApiClient # Using constructor argument - client = TestNetDispenserApiClient(auth_token="your_auth_token") # Using environment variable - import os -os.environ["ALGOKIT_DISPENSER_ACCESS_TOKEN"] = "your_auth_token" +os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' client = TestNetDispenserApiClient() ``` ## Funding an Account -To fund an account with Algos from the dispenser API, use the `fund` method. This method requires the receiver’s address, the amount to be funded, and the asset ID. +To fund an account with Algo from the dispenser API, use the `fund` method. This method requires the receiver’s address and the amount to be funded. -```py -response = client.fund(address="receiver_address", amount=1000, asset_id=0) +```python +response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, # Amount in microAlgos +) ``` -The `fund` method returns a `FundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. +The `fund` method returns a `DispenserFundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. ## Registering a Refund To register a refund for a transaction with the dispenser API, use the `refund` method. This method requires the transaction ID of the refund transaction. -```py -client.refund(refund_txn_id="transaction_id") +```python +dispenser.refund("transaction_id") ``` -> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by send funds back to TestNet Dispenser, then you can invoke this `refund` endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the `sender` field of any issued `fund` transaction initiated via [`fund`](). +> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by sending funds back to TestNet Dispenser, then you can invoke this refund endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the sender field of any issued fund transaction initiated via [fund](). ## Getting Current Limit -To get the current limit for an account with Algos from the dispenser API, use the `get_limit` method. This method requires the account address. +To get the current limit for an account with Algo from the dispenser API, use the `get_limit` method. -```py -response = client.get_limit(address="account_address") +```python +response = dispenser.get_limit() ``` -The `get_limit` method returns a `LimitResponse` object, which contains the current limit amount. +The `get_limit` method returns a `DispenserLimitResponse` object, which contains the current limit amount. ## Error Handling If an error occurs while making a request to the dispenser API, an exception will be raised with a message indicating the type of error. Refer to [Error Handling docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) for details on how you can handle each individual error `code`. + +Here’s an example of handling errors: + +```python +try: + response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, + ) +except Exception as e: + print(f"Error occurred: {str(e)}") +``` diff --git a/docs/markdown/capabilities/testing.md b/docs/markdown/capabilities/testing.md new file mode 100644 index 00000000..857c7ad8 --- /dev/null +++ b/docs/markdown/capabilities/testing.md @@ -0,0 +1,204 @@ +# Testing + +The following is a collection of useful snippets that can help you get started with testing your Algorand applications using AlgoKit utils. For the sake of simplicity, we’ll use [pytest](https://docs.pytest.org/en/latest/) in the examples below. + +## Basic Test Setup + +Here’s a basic test setup using pytest fixtures that provides common testing utilities: + +```python +import pytest +from algokit_utils import Account, SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.amount import AlgoAmount + +@pytest.fixture +def algorand() -> AlgorandClient: + """Get an AlgorandClient instance configured for LocalNet""" + return AlgorandClient.default_localnet() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + """Create and fund a test account with ALGOs""" + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_algos(100), + min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account +``` + +Refer to [pytest fixture scopes](https://docs.pytest.org/en/latest/how-to/fixtures.html#fixture-scopes) for more information on how to control lifecycle of fixtures. + +## Creating Test Assets + +Here’s a helper function to create test ASAs (Algorand Standard Assets): + +```python +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None = None) -> int: + """Create a test asset and return its ID""" + if total is None: + total = random.randint(20, 120) + + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TST", + asset_name=f"Test Asset {random.randint(1,100)}", + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) + ) + + return int(create_result.confirmation["asset-index"]) +``` + +## Testing Application Deployments + +Here’s how one can test smart contract application deployments: + +```python +def test_app_deployment(algorand: AlgorandClient, funded_account: SigningAccount): + """Test deploying a smart contract application""" + + # Load the application spec + app_spec = Path("artifacts/application.json").read_text() + + # Create app factory + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + + # Deploy the app + app_client, deploy_response = factory.deploy( + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, + ) + + # Verify deployment + assert deploy_response.app.app_id > 0 + assert deploy_response.app.app_address +``` + +## Testing Asset Transfers + +Here’s how one can test ASA transfers between accounts: + +```python +def test_asset_transfer(algorand: AlgorandClient, funded_account: SigningAccount): + """Test ASA transfers between accounts""" + + # Create receiver account + receiver = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=receiver, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1) + ) + + # Create test asset + asset_id = generate_test_asset(algorand, funded_account, 100) + + # Opt receiver into asset + algorand.send.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer + ) + ) + + # Transfer asset + transfer_amount = 5 + result = algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=receiver.address, + asset_id=asset_id, + amount=transfer_amount + ) + ) + + # Verify transfer + receiver_balance = algorand.asset.get_account_information(receiver, asset_id) + assert receiver_balance.balance == transfer_amount +``` + +## Testing Application Calls + +Here’s how to test application method calls: + +```python +def test_app_method_call(algorand: AlgorandClient, funded_account: SigningAccount): + """Test calling ABI methods on an application""" + + # Deploy application first + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Call application method + result = app_client.send.call( + AppClientMethodCallParams( + method="hello", + args=["world"] + ) + ) + + # Verify result + assert result.abi_return == "Hello, world" +``` + +## Testing Box Storage + +Here’s how to test application box storage: + +```python +def test_box_storage(algorand: AlgorandClient, funded_account: SigningAccount): + """Test application box storage""" + + # Deploy application + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Fund app account for box storage MBR + app_client.fund_app_account( + FundAppAccountParams(amount=AlgoAmount.from_algos(1)) + ) + + # Store value in box + box_name = b"test_box" + box_value = "test_value" + app_client.send.call( + AppClientMethodCallParams( + method="set_box", + args=[box_name, box_value], + box_references=[box_name] + ) + ) + + # Verify box value + stored_value = app_client.get_box_value(box_name) + assert stored_value == box_value.encode() +``` diff --git a/docs/markdown/capabilities/transaction-composer.md b/docs/markdown/capabilities/transaction-composer.md new file mode 100644 index 00000000..c859f81f --- /dev/null +++ b/docs/markdown/capabilities/transaction-composer.md @@ -0,0 +1,228 @@ +# Transaction composer + +The `TransactionComposer` class allows you to easily compose one or more compliant Algorand transactions and execute and/or simulate them. + +It’s the core of how the `AlgorandClient` class composes and sends transactions. + +```python +from algokit_utils import TransactionComposer, AppManager +from algokit_utils.transactions import ( + PaymentParams, + AppCallMethodCallParams, + AssetCreateParams, + AppCreateParams, + # ... other transaction parameter types +) +``` + +To get an instance of `TransactionComposer` you can either get it from an app client, from an `AlgorandClient`, or by instantiating via the constructor. + +```python +# From AlgorandClient +composer_from_algorand = algorand.new_group() + +# From AppClient +composer_from_app_client = app_client.algorand.new_group() + +# From constructor +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer +) + +# From constructor with optional params +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer, + # Custom function to get suggested params + get_suggested_params=lambda: algod.suggested_params(), + # Number of rounds the transaction should be valid for + default_validity_window=1000, + # Optional AppManager instance for TEAL compilation + app_manager=AppManager(algod) +) +``` + +## Constructing a transaction + +To construct a transaction you need to add it to the composer, passing in the relevant params object for that transaction. Params are Python dataclasses aavailable for import from `algokit_utils.transactions`. + +Parameter types include: + +- `PaymentParams` - For ALGO transfers +- `AssetCreateParams` - For creating ASAs +- `AssetConfigParams` - For reconfiguring ASAs +- `AssetTransferParams` - For ASA transfers +- `AssetOptInParams` - For opting in to ASAs +- `AssetOptOutParams` - For opting out of ASAs +- `AssetDestroyParams` - For destroying ASAs +- `AssetFreezeParams` - For freezing ASA balances +- `AppCreateParams` - For creating applications +- `AppCreateMethodCallParams` - For creating applications with ABI method calls +- `AppCallParams` - For calling applications +- `AppCallMethodCallParams` - For calling ABI methods on applications +- `AppUpdateParams` - For updating applications +- `AppUpdateMethodCallParams` - For updating applications with ABI method calls +- `AppDeleteParams` - For deleting applications +- `AppDeleteMethodCallParams` - For deleting applications with ABI method calls +- `OnlineKeyRegistrationParams` - For online key registration transactions +- `OfflineKeyRegistrationParams` - For offline key registration transactions + +The methods to construct a transaction are all named `add_{transaction_type}` and return an instance of the composer so they can be chained together fluently to construct a transaction group. + +For example: + +```python +from algokit_utils import AlgoAmount +from algokit_utils.transactions import AppCallMethodCallParams, PaymentParams + +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100), + note=b"Payment note" + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + boxes=[box_reference] # Optional box references + )) +) +``` + +## Simulating a transaction + +Transactions can be simulated using the simulate endpoint in algod, which enables evaluating the transaction on the network without it actually being committed to a block. +This is a powerful feature, which has a number of options which are detailed in the [simulate API docs](https://developer.algorand.org/docs/rest-apis/algod/#post-v2transactionssimulate). + +The `simulate()` method accepts several optional parameters that are passed through to the algod simulate endpoint: + +- `allow_more_logs: bool | None` - Allow more logs than standard +- `allow_empty_signatures: bool | None` - Allow transactions without signatures +- `allow_unnamed_resources: bool | None` - Allow unnamed resources in app calls +- `extra_opcode_budget: int | None` - Additional opcode budget +- `exec_trace_config: SimulateTraceConfig | None` - Execution trace configuration +- `simulation_round: int | None` - Round to simulate at +- `skip_signatures: int | None` - Skip signature verification + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate() +) + +# Access simulation results +simulate_response = result.simulate_response +confirmations = result.confirmations +transactions = result.transactions +returns = result.returns # ABI returns if any +``` + +### Simulate without signing + +There are situations where you may not be able to (or want to) sign the transactions when executing simulate. +In these instances you should set `skip_signatures=True` which automatically builds empty transaction signers and sets both `fix-signers` and `allow-empty-signatures` to `True` when sending the algod API call. + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate( + skip_signatures=True, + allow_more_logs=True, # Optional: allow more logs + extra_opcode_budget=700 # Optional: increase opcode budget + ) +) +``` + +### Resource Population + +The `TransactionComposer` includes automatic resource population capabilities for application calls. When sending or simulating transactions, it can automatically detect and populate required references for: + +- Account references +- Application references +- Asset references +- Box references + +This happens automatically when either: + +1. The global `algokit_utils.config` instance is set to `populate_app_call_resources=True` (default is `False`) +2. The `populate_app_call_resources` parameter is explicitly passed as `True` when sending transactions + +```python +# Automatic resource population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + # Resources will be automatically populated! + )) + .send(send_params=SendParams(populate_app_call_resources=True)) +) + +# Or disable automatic population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + # Explicitly specify required resources + account_references=["ACCOUNT"], + app_references=[456], + asset_references=[789], + box_references=[box_reference] + )) + .send(send_params=SendParams(populate_app_call_resources=False)) +) +``` + +The resource population: + +- Respects the maximum limits (4 for accounts, 8 for foreign references) +- Handles cross-reference resources efficiently (e.g., asset holdings and local state) +- Automatically distributes resources across multiple transactions in a group when needed +- Raises descriptive errors if resource limits are exceeded + +This feature is particularly useful when: + +- Working with complex smart contracts that access various resources +- Building transaction groups where resources need to be coordinated +- Developing applications where resource requirements may change dynamically + +Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. diff --git a/docs/markdown/capabilities/transaction.md b/docs/markdown/capabilities/transaction.md new file mode 100644 index 00000000..c0f692e2 --- /dev/null +++ b/docs/markdown/capabilities/transaction.md @@ -0,0 +1,135 @@ +# Transaction management + +Transaction management is one of the core capabilities provided by AlgoKit Utils. It allows you to construct, simulate and send single or grouped transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, multiple sender account types, and sending behavior. + +## Transaction Results + +All AlgoKit Utils functions that send transactions return either a `SendSingleTransactionResult` or `SendAtomicTransactionComposerResults`, providing consistent mechanisms to interpret transaction outcomes. + +### SendSingleTransactionResult + +The base `SendSingleTransactionResult` class is used for single transactions: + +```python +@dataclass(frozen=True, kw_only=True) +class SendSingleTransactionResult: + transaction: TransactionWrapper # Last transaction + confirmation: AlgodResponseType # Last confirmation + group_id: str + tx_id: str | None = None # Transaction ID of the last transaction + tx_ids: list[str] # All transaction IDs in the group + transactions: list[TransactionWrapper] + confirmations: list[AlgodResponseType] + returns: list[ABIReturn] | None = None # ABI returns if applicable +``` + +Common variations include: + +- `SendSingleAssetCreateTransactionResult` - Adds `asset_id` +- `SendAppTransactionResult` - Adds `abi_return` +- `SendAppUpdateTransactionResult` - Adds compilation results +- `SendAppCreateTransactionResult` - Adds `app_id` and `app_address` + +### SendAtomicTransactionComposerResults + +When using the atomic transaction composer directly via `TransactionComposer.send()` or `TransactionComposer.simulate()`, you’ll receive a `SendAtomicTransactionComposerResults`: + +```python +@dataclass +class SendAtomicTransactionComposerResults: + group_id: str # The group ID if this was a transaction group + confirmations: list[AlgodResponseType] # The confirmation info for each transaction + tx_ids: list[str] # The transaction IDs that were sent + transactions: list[TransactionWrapper] # The transactions that were sent + returns: list[ABIReturn] # The ABI return values from any ABI method calls + simulate_response: dict[str, Any] | None = None # Simulation response if simulated +``` + +### Application-specific Result Types + +When working with applications via `AppClient` or `AppFactory`, you’ll get enhanced result types that provide direct access to parsed ABI values: + +- `SendAppFactoryTransactionResult` +- `SendAppUpdateFactoryTransactionResult` +- `SendAppCreateFactoryTransactionResult` + +These types extend the base transaction results to add an `abi_value` field that contains the parsed ABI return value according to the ARC-56 specification. The `Arc56ReturnValueType` can be: + +- A primitive ABI value (bool, int, str, bytes) +- An ABI struct (as a Python dict) +- None (for void returns) + +### Where You’ll Encounter Each Result Type + +Different interfaces return different result types: + +1. **Direct Transaction Composer** + - `TransactionComposer.send()` → `SendAtomicTransactionComposerResults` + - `TransactionComposer.simulate()` → `SendAtomicTransactionComposerResults` +2. **AlgorandClient Methods** + - `.send.payment()` → `SendSingleTransactionResult` + - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` (contains raw ABI return) + - `.send.app_create()` → `SendAppCreateTransactionResult` (with app ID/address) + - `.send.app_update()` → `SendAppUpdateTransactionResult` (with compilation info) +3. **AppClient Methods** + - `.call()` → `SendAppTransactionResult` + - `.create()` → `SendAppCreateTransactionResult` + - `.update()` → `SendAppUpdateTransactionResult` +4. **AppFactory Methods** + - `.create()` → `SendAppCreateFactoryTransactionResult` + - `.call()` → `SendAppFactoryTransactionResult` + - `.update()` → `SendAppUpdateFactoryTransactionResult` + +Example usage with AppFactory for easy access to ABI returns: + +```python +# Using AppFactory +result = app_factory.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Access the parsed ABI return value directly +parsed_value = result.abi_value # Already decoded per ARC-56 spec + +# Compared to base AppClient where you need to parse manually +base_result = app_client.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Need to manually handle ABI return parsing +if base_result.abi_return: + parsed_value = base_result.abi_return.value +``` + +Key differences between result types: + +1. **Base Transaction Results** (`SendSingleTransactionResult`) + - Focus on transaction confirmation details + - Include group support but optimized for single transactions + - No direct ABI value parsing +2. **Atomic Transaction Results** (`SendAtomicTransactionComposerResults`) + - Built for transaction groups + - Include simulation support + - Raw ABI returns via `.returns` + - No single transaction convenience fields +3. **Application Results** (`SendAppTransactionResult` family) + - Add application-specific fields (`app_id`, compilation results) + - Include raw ABI returns via `.abi_return` + - Base application transaction support +4. **Factory Results** (`SendAppFactoryTransactionResult` family) + - Highest level of abstraction + - Direct access to parsed ABI values via `.abi_value` + - Automatic ARC-56 compliant value parsing + - Combines app-specific fields with parsed ABI returns + +## Further reading + +To understand how to create, simulate and send transactions consult: + +- The [`TransactionComposer`](transaction-composer.md) documentation for composing transaction groups +- The [`AlgorandClient`](algorand-client.md) documentation for a high-level interface to send transactions + +The transaction composer documentation covers the details of constructing transactions and transaction groups, while the Algorand client documentation covers the high-level interface for sending transactions. diff --git a/docs/markdown/capabilities/transfer.md b/docs/markdown/capabilities/transfer.md index 0462a051..088f50a5 100644 --- a/docs/markdown/capabilities/transfer.md +++ b/docs/markdown/capabilities/transfer.md @@ -1,58 +1,151 @@ -# Algo transfers - -Algo transfers is a higher-order use case capability provided by AlgoKit Utils allows you to easily initiate algo transfers between accounts, including dispenser management and -idempotent account funding. - -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_transfer.py). - -## Transferring Algos - -The key function to facilitate Algo transfers is `algokit.transfer(algod_client, transfer_parameters)`, which returns the underlying `EnsureFundedResponse` and takes a `TransferParameters` - -The following fields on `TransferParameters` are required to transfer ALGOs: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `micro_algos`: The amount of micro ALGOs to send - -## Ensuring minimum Algos - -The ability to automatically fund an account to have a minimum amount of disposable ALGOs to spend is incredibly useful for automation and deployment scripts. -The function to facilitate this is `ensure_funded(client, parameters)`, which takes an `EnsureBalanceParameters` instance and returns the underlying `EnsureFundedResponse` if a payment was made, a string if the dispenser API was used, or None otherwise. - -The following fields on `EnsureBalanceParameters` are required to ensure minimum ALGOs: - -- `account_to_fund`: The account address that will receive the ALGOs. This can be an `Account` instance, an `AccountTransactionSigner` instance, or a string. -- `min_spending_balance_micro_algos`: The minimum balance of micro ALGOs that the account should have available to spend (i.e. on top of minimum balance requirement). -- `min_funding_increment_micro_algos`: When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets called often on an active account). Default is 0. -- `funding_source`: The account (with private key) or signer that will send the ALGOs. If not set, it will use `get_dispenser_account`. This can be an `Account` instance, an `AccountTransactionSigner` instance, [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) instance, or None. -- `suggested_params`: (optional) Transaction parameters, an instance of `SuggestedParams`. -- `note`: (optional) The transaction note, default is “Funding account to meet minimum requirement”. -- `fee_micro_algos`: (optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call. -- `max_fee_micro_algos`: (optional) The maximum fee that you are happy to pay (default: unbounded). If this is set it’s possible the transaction could get rejected during network congestion. - -The function calls Algod to find the current balance and minimum balance requirement, gets the difference between those two numbers and checks to see if it’s more than the `min_spending_balance_micro_algos`. If so, it will send the difference, or the `min_funding_increment_micro_algos` if that is specified. If the account is on TestNet and `use_dispenser_api` is True, the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md) will be used to fund the account. - -> Please note, if you are attempting to fund via Dispenser API, make sure to set `ALGOKIT_DISPENSER_ACCESS_TOKEN` environment variable prior to invoking `ensure_funded`. To generate the token refer to [AlgoKit CLI documentation](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) - -## Transfering Assets - -The key function to facilitate asset transfers is `transfer_asset(algod_client, transfer_parameters)`, which returns a `AssetTransferTxn` and takes a `TransferAssetParameters`: - -The following fields on `TransferAssetParameters` are required to transfer assets: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `asset_id`: The asset id that will be transfered -- `amount`: The amount to send as the smallest divisible unit value +# Algo transfers (payments) + +Algo transfers, or [payments](https://developer.algorand.org/docs/get-details/transactions/#payment-transaction), is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [Algo amount handling](amount.md) and [Transaction management](transaction.md). It allows you to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding. + +To see some usage examples check out the automated tests in the repository. + +## `payment` + +The key function to facilitate Algo transfers is `algorand.send.payment(params)` (immediately send a single payment transaction), `algorand.create_transaction.payment(params)` (construct a payment transaction), or `algorand.new_group().add_payment(params)` (add payment to a group of transactions) per [`AlgorandClient`](algorand-client.md) [transaction semantics](algorand-client.md#creating-and-issuing-transactions). + +The base type for specifying a payment transaction is `PaymentParams`, which has the following parameters in addition to the [common transaction parameters](algorand-client.md#transaction-parameters): + +- `receiver: str` - The address of the account that will receive the Algo +- `amount: AlgoAmount` - The amount of Algo to send +- `close_remainder_to: Optional[str]` - If given, close the sender account and send the remaining balance to this address (**warning:** use this carefully as it can result in loss of funds if used incorrectly) + +```python +# Minimal example +result = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo") + ) +) + +# Advanced example +result2 = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo"), + close_remainder_to="CLOSEREMAINDERTOADDRESS", + lease="lease", + note=b"note", + # Use this with caution, it's generally better to use algorand_client.account.rekey_account + rekey_to="REKEYTOADDRESS", + # You wouldn't normally set this field + first_valid_round=1000, + validity_window=10, + extra_fee=AlgoAmount(1000, "microalgo"), + static_fee=AlgoAmount(1000, "microalgo"), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee=AlgoAmount(3000, "microalgo"), + # Signer only needed if you want to provide one, + # generally you'd register it with AlgorandClient + # against the sender and not need to pass it in + signer=transaction_signer, + ), + send_params=SendParams( + max_rounds_to_wait=5, + suppress_log=True, + ) +) +``` + +## `ensure_funded` + +The `ensure_funded` function automatically funds an account to maintain a minimum amount of [disposable Algo](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance). This is particularly useful for automation and deployment scripts that get run multiple times and consume Algo when run. + +There are 3 variants of this function: + +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + - **Note:** requires environment variables to be set. + - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` + if it’s a rekeyed account, or against default LocalNet if no environment variables present. +- `algorand_client.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +The general structure of these calls is similar, they all take: + +- `account_to_fund: str | Account` - Address or signing account of the account to fund +- The source (dispenser): + - In `ensure_funded`: `dispenser_account: str | Account` - the address or signing account of the account to use as a dispenser + - In `ensure_funded_from_environment`: Not specified, loaded automatically from the ephemeral environment + - In `ensure_funded_from_testnet_dispenser_api`: `dispenser_client: TestNetDispenserApiClient` - a client instance of the TestNet dispenser API +- `min_spending_balance: AlgoAmount` - The minimum balance of Algo that the account should have available to spend (i.e., on top of the minimum balance requirement) +- An `options` object, which has: + - [Common transaction parameters](algorand-client.md#transaction-parameters) (not for TestNet Dispenser API) + - [Execution parameters](algorand-client.md#sending-a-single-transaction) (not for TestNet Dispenser API) + - `min_funding_increment: Optional[AlgoAmount]` - When issuing a funding amount, the minimum amount to transfer; this avoids many small transfers if this function gets called often on an active account + +### Examples + +```python +# From account + +# Basic example +algorand_client.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded( + "ACCOUNTADDRESS", + "DISPENSERADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# From environment + +# Basic example +algorand_client.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded_from_environment( + "ACCOUNTADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# TestNet Dispenser API + +# Basic example +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo") +) +# With configuration +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), +) +``` + +All 3 variants return an `EnsureFundedResponse` (and the first two also return a [single transaction result](algorand-client.md#sending-a-single-transaction)) if a funding transaction was needed, or `None` if no transaction was required: + +- `amount_funded: AlgoAmount` - The number of Algo that was paid +- `transaction_id: str` - The ID of the transaction that funded the account + +If you are using the TestNet Dispenser API then the `transaction_id` is useful if you want to use the [refund functionality](dispenser-client.md#registering-a-refund). ## Dispenser -If you want to programmatically send funds then you will often need a “dispenser” account that has a store of ALGOs that can be sent and a private key available for that dispenser account. +If you want to programmatically send funds to an account so it can transact then you will often need a “dispenser” account that has a store of Algo that can be sent and a private key available for that dispenser account. -There is a standard AlgoKit Utils function to get access to a [dispenser account](account.md#id1): `get_dispenser_account`. When running against -[LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md), the dispenser account can be automatically determined using the -[Kmd API](https://developer.algorand.org/docs/rest-apis/kmd). When running against other networks like TestNet or MainNet the mnemonic of the dispenser account can be provided via environment -variable `DISPENSER_MNEMONIC` +There’s a number of ways to get a dispensing account in AlgoKit Utils: -Please note that this does not refer to the [AlgoKit TestNet Dispenser API](dispenser-client.md) which is a separate abstraction that can be used to fund accounts on TestNet via dedicated API service. +- Get a dispenser via [account manager](account.md#dispenser) - either automatically from [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) or from the environment +- By programmatically creating one of the many account types via [account manager](account.md#accounts) +- By programmatically interacting with [KMD](account.md#kmd-account-management) if running against LocalNet +- By using the [AlgoKit TestNet Dispenser API client](dispenser-client.md) which can be used to fund accounts on TestNet via a dedicated API service diff --git a/docs/markdown/capabilities/typed-app-clients.md b/docs/markdown/capabilities/typed-app-clients.md new file mode 100644 index 00000000..099f3c50 --- /dev/null +++ b/docs/markdown/capabilities/typed-app-clients.md @@ -0,0 +1,194 @@ +# Typed application clients + +Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) or [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract. + +Typed application clients are the recommended way of interacting with smart contracts. If you don’t have/want a typed client, but have an ARC-56/ARC-32 app spec then you can use the [non-typed application clients](app-client.md) and if you want to call a smart contract you don’t have an app spec file for you can use the underlying [app management](app.md) and [app deployment](app-deploy.md) functionality to manually construct transactions. + +## Generating an app spec + +You can generate an app spec file: + +- Using [Algorand Python](https://algorandfoundation.github.io/puya/#quick-start) +- Using [TEALScript](https://tealscript.netlify.app/tutorials/hello-world/0004-artifacts/) +- By hand by following the specification [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258)/[ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) +- Using [Beaker](https://algorand-devrel.github.io/beaker/html/usage.html) (PyTEAL) *(DEPRECATED)* + +## Generating a typed client + +To generate a typed client from an app spec file you can use [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients): + +```default +> algokit generate client application.json --output /absolute/path/to/client.py +``` + +Note: AlgoKit Utils >= 3.0.0 is compatible with the older 1.x.x generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 2.x.x. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version. + +## Getting a typed client instance + +To get an instance of a typed client you can use an [`AlgorandClient`](algorand-client.md) instance or a typed app [`Factory`]() instance. + +The approach to obtaining a client instance depends on how many app clients you require for a given app spec and if the app has already been deployed, which is summarised below: + +### App is deployed + + + + + + + + + + + + + + + + + + + + + + +
Resolve App by IDResolve App by Creator and Name
Single App Client InstanceMultiple App Client InstancesSingle App Client InstanceMultiple App Client Instances
+```python +app_client = algorand.client.get_typed_app_client_by_id(MyContractClient, { + app_id=1234, + # ... +}) +# or +app_client = MyContractClient({ + algorand, + app_id=1234, + # ... +}) +``` + + +```python +app_client1 = factory.get_app_client_by_id( + app_id=1234, + # ... +) +app_client2 = factory.get_app_client_by_id( + app_id=4321, + # ... +) +``` + + +```python +app_client = algorand.client.get_typed_app_client_by_creator_and_name( + MyContractClient, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +# or +app_client = MyContractClient.from_creator_and_name( + algorand, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +``` + + +```python +app_client1 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +app_client2 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name-2", + # ... +) +``` + +
+ +To understand the difference between resolving by ID vs by creator and name see the underlying [app client documentation](app-client.md#appclient). + +### App is not deployed + + + + + + + + + + + + + + +
Deploy a New AppDeploy or Resolve App Idempotently by Creator and Name
+```python +app_client, response = factory.deploy( + args=[], + # ... +) +# or +app_client, response = factory.send.create.METHODNAME( + args=[], + # ... +) +``` + + +```python +app_client, response = factory.deploy( + app_name="contract-name", + # ... +) +``` + +
+ +### Creating a typed factory instance + +If your scenario calls for an app factory, you can create one using the below: + +```python +factory = algorand.client.get_typed_app_factory(MyContractFactory) +# or +factory = MyContractFactory(algorand) +``` + +## Client usage + +See the [official usage docs](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/usage.md) for full details. + +For a simple example that deploys a contract and calls a `"hello"` method, see below: + +```python +# A similar working example can be seen in the AlgoKit init production smart contract templates, when using Python deployment +# In this case the generated factory is called `HelloWorldAppFactory` and is in `./artifacts/HelloWorldApp/client.py` +from artifacts.hello_world_app.client import HelloWorldAppClient, HelloArgs +from algokit_utils import AlgorandClient + +# These require environment variables to be present, or it will retrieve from default LocalNet +algorand = AlgorandClient.from_environment() +deployer = algorand.account.from_environment("DEPLOYER", AlgoAmount.from_algo(1)) + +# Create the typed app factory +factory = algorand.client.get_typed_app_factory(HelloWorldAppFactory, + creator_address=deployer, + default_sender=deployer, +) + +# Create the app and get a typed app client for the created app (note: this creates a new instance of the app every time, +# you can use .deploy() to deploy idempotently if the app wasn't previously +# deployed or needs to be updated if that's allowed) +app_client, response = factory.send.create() + +# Make a call to an ABI method and print the result +response = app_client.send.hello(args=HelloArgs(name="world")) +print(response) +``` diff --git a/docs/markdown/index.md b/docs/markdown/index.md index 71972566..f3ecf895 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -1,73 +1,128 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. -This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. -Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. #### NOTE - If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). -[Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]() +[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Config and logging](#config-logging) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) # Contents -- [Account management](capabilities/account.md) - - [`Account`](capabilities/account.md#account) -- [Client management](capabilities/client.md) - - [Network configuration](capabilities/client.md#network-configuration) - - [Clients](capabilities/client.md#clients) -- [App client](capabilities/app-client.md) - - [Design](capabilities/app-client.md#design) - - [Creating an application client](capabilities/app-client.md#creating-an-application-client) - - [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) - - [Composing calls](capabilities/app-client.md#composing-calls) - - [Reading state](capabilities/app-client.md#reading-state) - - [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) -- [App deployment](capabilities/app-deploy.md) - - [Design](capabilities/app-deploy.md#design) - - [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) - - [Deploying an application](capabilities/app-deploy.md#deploying-an-application) -- [Algo transfers](capabilities/transfer.md) - - [Transferring Algos](capabilities/transfer.md#transferring-algos) - - [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) - - [Transfering Assets](capabilities/transfer.md#transfering-assets) - - [Dispenser](capabilities/transfer.md#dispenser) -- [TestNet Dispenser Client](capabilities/dispenser-client.md) - - [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) - - [Funding an Account](capabilities/dispenser-client.md#funding-an-account) - - [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) - - [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) - - [Error Handling](capabilities/dispenser-client.md#error-handling) -- [Debugger](capabilities/debugger.md) - - [Configuration](capabilities/debugger.md#configuration) - - [Debugging Utilities](capabilities/debugger.md#debugging-utilities) -- [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) - - [Data](apidocs/algokit_utils/algokit_utils.md#data) - - [Classes](apidocs/algokit_utils/algokit_utils.md#classes) - - [Functions](apidocs/algokit_utils/algokit_utils.md#functions) +* [Account management](capabilities/account.md) + * [`AccountManager`](capabilities/account.md#accountmanager) + * [`TransactionSignerAccountProtocol`](capabilities/account.md#transactionsigneraccountprotocol) + * [Registering a signer](capabilities/account.md#registering-a-signer) + * [Default signer](capabilities/account.md#default-signer) + * [Get a signer](capabilities/account.md#get-a-signer) + * [Accounts](capabilities/account.md#accounts) + * [Rekey account](capabilities/account.md#rekey-account) + * [KMD account management](capabilities/account.md#kmd-account-management) +* [Algorand client](capabilities/algorand-client.md) + * [Accessing SDK clients](capabilities/algorand-client.md#accessing-sdk-clients) + * [Accessing manager class instances](capabilities/algorand-client.md#accessing-manager-class-instances) + * [Creating and issuing transactions](capabilities/algorand-client.md#creating-and-issuing-transactions) +* [Algo amount handling](capabilities/amount.md) + * [`AlgoAmount`](capabilities/amount.md#algoamount) +* [App client and App factory](capabilities/app-client.md) + * [`AppFactory`](capabilities/app-client.md#appfactory) + * [`AppClient`](capabilities/app-client.md#appclient) + * [Dynamically creating clients for a given app spec](capabilities/app-client.md#dynamically-creating-clients-for-a-given-app-spec) + * [Creating and deploying an app](capabilities/app-client.md#creating-and-deploying-an-app) + * [Updating and deleting an app](capabilities/app-client.md#updating-and-deleting-an-app) + * [Calling the app](capabilities/app-client.md#calling-the-app) + * [Funding the app account](capabilities/app-client.md#funding-the-app-account) + * [Reading state](capabilities/app-client.md#reading-state) + * [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) + * [Default arguments](capabilities/app-client.md#default-arguments) +* [App deployment](capabilities/app-deploy.md) + * [Smart contract development lifecycle](capabilities/app-deploy.md#smart-contract-development-lifecycle) + * [`AppDeployer`](capabilities/app-deploy.md#appdeployer) + * [Deployment metadata](capabilities/app-deploy.md#deployment-metadata) + * [Lookup deployed apps by name](capabilities/app-deploy.md#lookup-deployed-apps-by-name) + * [Performing a deployment](capabilities/app-deploy.md#performing-a-deployment) +* [App management](capabilities/app.md) + * [`AppManager`](capabilities/app.md#appmanager) + * [Calling apps](capabilities/app.md#calling-apps) + * [Accessing state](capabilities/app.md#accessing-state) + * [Getting app information](capabilities/app.md#getting-app-information) + * [Box references](capabilities/app.md#box-references) + * [Common app parameters](capabilities/app.md#common-app-parameters) +* [Assets](capabilities/asset.md) + * [`AssetManager`](capabilities/asset.md#assetmanager) + * [Asset Information](capabilities/asset.md#asset-information) + * [Bulk Operations](capabilities/asset.md#bulk-operations) + * [Get Asset Information](capabilities/asset.md#get-asset-information) +* [Client management](capabilities/client.md) + * [`ClientManager`](capabilities/client.md#clientmanager) + * [Network configuration](capabilities/client.md#network-configuration) + * [Clients](capabilities/client.md#clients) + * [Automatic retry](capabilities/client.md#automatic-retry) + * [Network information](capabilities/client.md#network-information) +* [Debugger](capabilities/debugging.md) + * [Configuration](capabilities/debugging.md#configuration) + * [Debugging Utilities](capabilities/debugging.md#debugging-utilities) +* [TestNet Dispenser Client](capabilities/dispenser-client.md) + * [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) + * [Funding an Account](capabilities/dispenser-client.md#funding-an-account) + * [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) + * [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) + * [Error Handling](capabilities/dispenser-client.md#error-handling) +* [Testing](capabilities/testing.md) + * [Basic Test Setup](capabilities/testing.md#basic-test-setup) + * [Creating Test Assets](capabilities/testing.md#creating-test-assets) + * [Testing Application Deployments](capabilities/testing.md#testing-application-deployments) + * [Testing Asset Transfers](capabilities/testing.md#testing-asset-transfers) + * [Testing Application Calls](capabilities/testing.md#testing-application-calls) + * [Testing Box Storage](capabilities/testing.md#testing-box-storage) +* [Transaction composer](capabilities/transaction-composer.md) + * [Constructing a transaction](capabilities/transaction-composer.md#constructing-a-transaction) + * [Simulating a transaction](capabilities/transaction-composer.md#simulating-a-transaction) +* [Transaction management](capabilities/transaction.md) + * [Transaction Results](capabilities/transaction.md#transaction-results) + * [Further reading](capabilities/transaction.md#further-reading) +* [Algo transfers (payments)](capabilities/transfer.md) + * [`payment`](capabilities/transfer.md#payment) + * [`ensure_funded`](capabilities/transfer.md#ensure-funded) + * [Dispenser](capabilities/transfer.md#dispenser) +* [Typed application clients](capabilities/typed-app-clients.md) + * [Generating an app spec](capabilities/typed-app-clients.md#generating-an-app-spec) + * [Generating a typed client](capabilities/typed-app-clients.md#generating-a-typed-client) + * [Getting a typed client instance](capabilities/typed-app-clients.md#getting-a-typed-client-instance) + * [Client usage](capabilities/typed-app-clients.md#client-usage) +* [Migration Guide - v3](v3-migration-guide.md) + * [Migration Steps](v3-migration-guide.md#migration-steps) + * [Breaking Changes](v3-migration-guide.md#breaking-changes) + * [Best Practices](v3-migration-guide.md#best-practices) + * [Troubleshooting](v3-migration-guide.md#troubleshooting) +* [API Reference](autoapi/index.md) + * [composer](autoapi/composer/index.md) + * [algokit_utils](autoapi/algokit_utils/index.md) + * [client_manager](autoapi/client_manager/index.md) + * [algorand_client](autoapi/algorand_client/index.md) + * [account_manager](autoapi/account_manager/index.md) # Core principles -This library is designed with the following principles: +This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles) and is designed with the following principles: -- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are - exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. -- **Type-safety** - This library provides strong TypeScript support with effort put into creating types that provide good type safety and intellisense. -- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write +- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. +- **Type-safety** - This library provides strong type hints with effort put into creating types that provide good type safety and intellisense when used with tools like MyPy. +- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write. # Installation -This library can be installed from PyPi using pip or poetry, e.g.: +This library can be installed from PyPi using pip or poetry: -```default +```bash pip install algokit-utils +# or poetry add algokit-utils ``` @@ -75,50 +130,96 @@ poetry add algokit-utils # Usage -To use this library simply include the following at the top of your file: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: ```python -import algokit_utils +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=...) +# Point to a pre-created algod and indexer client +algorand = AlgorandClient.from_clients(algod=..., indexer=..., kmd=...) +# Point to custom configuration for algod +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod and indexer and kmd +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +indexer_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +kmd_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) ``` -Then you can use intellisense to auto-complete the various functions and types that are available by typing `algokit_utils.` in your favourite Integrated Development Environment (IDE), -or you can refer to the [reference documentation](apidocs/algokit_utils/algokit_utils.md). +# Testing -## Types +AlgoKit Utils provides a dedicated documentation page on various useful snippets that can be reused for testing with tools like [Pytest](https://docs.pytest.org/en/latest/): -The library contains extensive type hinting combined with a tool like MyPy this can help identify issues where incorrect types have been used, or used incorrectly. +- [Testing](capabilities/testing.md) - +# Types -# Capabilities +The library leverages Python’s native type hints and is fully compatible with [MyPy](https://mypy-lang.org/) for static type checking. -The library helps you with the following capabilities: +All public abstractions and methods are organized in logical modules matching their domain functionality. You can import types either directly from the root module or from their source submodules. Refer to [API documentation](autoapi/index.md) for more details. -- Core capabilities - - [**Client management**](capabilities/client.md) - Creation of algod, indexer and kmd clients against various networks resolved from environment or specified configuration - - [**Account management**](capabilities/account.md) - Creation and use of accounts including mnemonic, multisig, transaction signer, idempotent KMD accounts and environment variable injected -- Higher-order use cases - - [**ARC-0032 Application Spec client**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities to provide a high productivity application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker) - - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution - - [**Algo transfers**](capabilities/transfer.md) - Ability to easily initiate algo transfers between accounts, including dispenser management and idempotent account funding - - [**Debugger**](capabilities/debugger.md) - Provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AVM Debugger extension](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). + - +# Config and logging -# Reference documentation +To configure the AlgoKit Utils library you can make use of the [`Config`](autoapi/algokit_utils/config/index.md) object, which has a configure method that lets you configure some or all of the configuration options. + +## Config singleton + +The AlgoKit Utils configuration singleton can be updated using `config.configure()`. Refer to the [Config API documentation](autoapi/algokit_utils/config/index.md) for more details. + +## Logging + +AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`]() class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. + +Each method supports optional suppression of output using the `suppress_log` parameter. -We have [auto-generated reference documentation for the code](apidocs/algokit_utils/algokit_utils.md). +## Debug mode -# Roadmap +To turn on debug mode you can use the following: -This library will naturally evolve with any logical developer experience improvements needed to facilitate the [AlgoKit](https://github.com/algorandfoundation/algokit-cli) roadmap as it evolves. +```python +from algokit_utils.config import config +config.configure(debug=True) +``` + +To retrieve the current debug state you can use `debug` property. + +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](). It’s likely this option will result in extra HTTP calls to algod os worth being careful when it’s turned on. + + -Likely future capability additions include: +# Capabilities -- Typed application client -- Asset management -- Expanded indexer API wrapper support +The library helps you interact with and develop against the Algorand blockchain with a series of end-to-end capabilities as described below: + +- [**AlgorandClient**](capabilities/algorand-client.md) - The key entrypoint to the AlgoKit Utils functionality +- **Core capabilities** + - [**Client management**](capabilities/client.md) - Creation of (auto-retry) algod, indexer and kmd clients against various networks resolved from environment or specified configuration, and creation of other API clients (e.g. TestNet Dispenser API and app clients) + - [**Account management**](capabilities/account.md) - Creation, use, and management of accounts including mnemonic, rekeyed, multisig, transaction signer, idempotent KMD accounts and environment variable injected + - [**Algo amount handling**](capabilities/amount.md) - Reliable, explicit, and terse specification of microAlgo and Algo amounts and safe conversion between them + - [**Transaction management**](capabilities/transaction.md) - Ability to construct, simulate and send transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, validity, signing, and sending behaviour +- **Higher-order use cases** + - [**Asset management**](capabilities/asset.md) - Creation, transfer, destroying, opting in and out and managing Algorand Standard Assets + - [**Typed application clients**](capabilities/typed-app-clients.md) - Type-safe application clients that are [generated](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients) from ARC-56 or ARC-32 application spec files and allow you to intuitively and productively interact with a deployed app, which is the recommended way of interacting with apps and builds on top of the following capabilities: + - [**ARC-56 / ARC-32 App client and App factory**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities (below) to provide a high productivity application client that works with ARC-56 and ARC-32 application spec defined smart contracts + - [**App management**](capabilities/app.md) - Creation, updating, deleting, calling (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes) + - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution + - [**Algo transfers (payments)**](capabilities/transfer.md) - Ability to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding + - [**Automated testing**](capabilities/testing.md) - Reusable snippets to leverage AlgoKit Utils abstractions in a manner that are useful for when writing tests in tools like [Pytest](https://docs.pytest.org/en/latest/). -# Indices and tables + + +# Reference documentation -- [Index](genindex.md) +For detailed API documentation, see the [auto-generated reference documentation](). diff --git a/docs/markdown/v3-migration-guide.md b/docs/markdown/v3-migration-guide.md new file mode 100644 index 00000000..95a4ba28 --- /dev/null +++ b/docs/markdown/v3-migration-guide.md @@ -0,0 +1,304 @@ +# Migration Guide - v3 + +Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: + +- Easier and simpler consumption experience guided by IDE autocompletion +- Less redundant parameter passing (e.g., `algod` client) +- Better performance through caching of commonly retrieved values like transaction parameters +- More consistent and intuitive API design +- Stronger type safety and better error messages +- Improved ARC-56 compatibility +- Feature parity with `algokit-utils-ts` >= `v7` interfaces + +The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. + +The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, _all v2 abstractions are available_ with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. + +> BREAKING CHANGE: the `beta` module is now removed, any imports from `algokit_utils.beta` will now raise an error with a link to a new expected import path. This is due to the fact that the interfaces introduced in `beta` are now refined and available in the main module. + +## Migration Steps + +In general, your codebase might fall into one of the following migration scenarios: + +- Using `algokit-utils-py` v2.x only without use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x only and with use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x with `algokit-client-generator-py` v1.x +- Using `algokit-client-generator-py` v1.x only (implies implicit dependency on `algokit-utils-py` v2.x) + +Given that `algokit-utils-py` v3.x is backwards compatible with `algokit-client-generator-py` v1.x, the following general guidelines are applicable to all scenarios (note that the order of operations is important to ensure straight-forward migration): + +1. Upgrade to `algokit-utils-py` v3.x + - 1.1 (If used) Update imports from `algokit_utils.beta` to `algokit_utils` + - 1.2 Follow hints in deprecation warnings to update your codebase to rely on latest v3 interfaces +2. Upgrade to `algokit-client-generator-py` v2.x and regenerate typed clients + - 2.1 Follow `algokit-client-generator-py` [v2.x migration guide](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/v2-migration-guide.md) + +The remaining set of guidelines are outlining migrations for specific abstractions that had direct equivalents in `algokit-utils-py` v2.x. + +### Prerequisites + +It is important to reiterate that if you have previously relied on `beta` versions of `algokit-utils-py` v2.x, you will need to update your imports to rely on the new interfaces. Errors thrown during import from `beta` will provide a description of the new expected import path. + +> As with `v2.x` all public abstractions in `algokit_utils` are available for direct imports `from algokit_utils import ...`, however underlying modules have been refined to be structured loosely around common AVM domains such as `applications`, `transactions`, `accounts`, `assets`, etc. See [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) for latest and detailed overview. + +### Step 1 - Replace SDK Clients with AlgorandClient + +First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: + +```python +"""Before""" +import algokit_utils +algod = algokit_utils.get_algod_client() +indexer = algokit_utils.get_indexer_client() + +"""After""" +from algokit_utils import AlgorandClient +algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. +``` + +During migration, you can still access SDK clients if needed: + +```python +algod = algorand.client.algod +indexer = algorand.client.indexer +kmd = algorand.client.kmd +``` + +### Step 2 - Update Account Management + +Account management has moved to `algorand.account`: + +#### Before: + +```python +account = algokit_utils.get_account_from_mnemonic( + mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), +) +dispenser = algokit_utils.get_dispenser_account(algod) +``` + +#### After: + +```python +account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) +dispenser = algorand.account.dispenser_from_environment() +``` + +Key changes: + +- `get_account` → `account.from_environment` +- `get_account_from_mnemonic` → `account.from_mnemonic` +- `get_dispenser_account` → `account.dispenser_from_environment` +- `get_localnet_default_account` → `account.localnet_dispenser` + +### Step 3 - Update Transaction Management + +Transaction creation and sending is now more structured: + +#### Before: + +```python +# Single transaction +result = algokit_utils.transfer_algos( + from_account=account, + to_addr="RECEIVER", + amount=algokit_utils.algos(1), + algod_client=algod, +) + +# Transaction groups +atc = AtomicTransactionComposer() +# ... add transactions ... +result = algokit_utils.execute_atc_with_logic_error(atc, algod) +``` + +#### After: + +```python +# Single transaction +result = algorand.send.payment( + sender=account.address, + receiver="RECEIVER", + amount=AlgoAmount.from_algo(1), +) + +# Transaction groups +composer = algorand.new_group() +# ... add transactions ... +result = composer.send() +``` + +Key changes: + +- `transfer_algos` → `algorand.send.payment` +- `transfer_asset` → `algorand.send.asset_transfer` +- `execute_atc_with_logic_error` → `composer.send()` +- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) +- Improved amount handling with dedicated `AlgoAmount` class (e.g., `AlgoAmount.from_algo(1)`) + +### Step 4 - Update `ApplicationSpecification` usage + +`ApplicationSpecification` abstraction is largely identical to v2, however it’s been renamed to `Arc32Contract` to better reflect the fact that it’s a contract specification for a specific ARC and addition of `Arc56Contract` supporting the latest recommended conventions. Hence the main actionable change is to update your import to `from algokit_utils import Arc32Contract` and rename `ApplicationSpecification` to `Arc32Contract`. + +You can instantiate an `Arc56Contract` instance from an `Arc32Contract` instance using the `Arc56Contract.from_arc32` method. For instance: + +```python +testing_app_arc32_app_spec = Arc32Contract.from_json(app_spec_json) +arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) +``` + +> Despite auto conversion of ARC-32 to ARC-56, we recommend recompiling your contract to a fully compliant ARC-56 specification given that auto conversion would skip populating information that can’t be parsed from raw ARC-32. + +### Step 5 - Update `ApplicationClient` usage + +The application client has been in v2 has been responsible for instantiation, deployment and calling of the application. In v3, this has been split into `AppClient`, `AppDeployer` and `AppFactory` to better reflect the different responsibilities: + +```python +"""Before (v2 deployment)""" +from algokit_utils import ApplicationClient, OnUpdate, OnSchemaBreak + +# Initialize client with manual configuration +app_client = ApplicationClient( + algod_client=algod, + app_spec=app_spec, + creator=creator, + app_name="MyApp" +) + +# Deployment with versioning and update policies +deploy_result = app_client.deploy( + version="1.0", + allow_update=True, + allow_delete=False, + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail +) + +# Post-deployment calls +response = app_client.call("initialize", args=["config"]) + + +"""After (v3 factory-based deployment)""" +from algokit_utils import AppFactory, OnUpdate, OnSchemaBreak + +# Factory-based deployment with compiled parameters +app_factory = AppFactory( + AppFactoryParams( + algorand=algorand, + app_spec=app_spec, + app_name="MyApp", + compilation_params=AppClientCompilationParams( + deploy_time_params={"VERSION": 1}, + updatable=True, # Replaces allow_update + deletable=False # Replaces allow_delete + ) + ) +) + +app_client, deploy_result = app_factory.deploy( + version="1.0", + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail, +) # Returns a tuple of (app_client, deploy_result) + +# Type-safe post-deployment calls +response = app_client.send.call("setup", args=[{"max_users": 100}]) +``` + +Notable changes: + +- Split between `AppClient`, `AppDeployer` (for raw creation/deployment) and `AppFactory` (for creation/deployment using factory patterns). In majority of cases, you will only need `AppFactory` as it provides convenience methods for instantiation of `AppClient` and mediates calls to `AppDeployer`. +- More structured transaction building with `.params`, `.create_transaction`, and `.send` +- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) +- ARC-56 support for state management +- Improved error handling and debugging support + +### Step 6 - Update `AppClient` State Management + +State management is now more structured and type-safe: + +```python +"""Before""" +global_state = app_client.get_global_state() +local_state = app_client.get_local_state(account_address) +box_value = app_client.get_box_value("box_name") + +"""After""" +# Global state +global_state = app_client.state.global_state.get_all() +value = app_client.state.global_state.get_value("key_name") +map_value = app_client.state.global_state.get_map_value("map_name", "key") + +# Local state +local_state = app_client.state.local_state(account_address).get_all() +value = app_client.state.local_state(account_address).get_value("key_name") +map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") + +# Box storage +box_value = app_client.state.box.get_value("box_name") +boxes = app_client.state.box.get_all() +map_value = app_client.state.box.get_map_value("map_name", "key") +``` + +### Step 7 - Update Asset Management + +Asset management is now more consistent: + +```python +"""Before""" +result = algokit_utils.opt_in(algod, account, [asset_id]) + +"""After""" +result = algorand.send.asset_opt_in( + params=AssetOptInParams( + sender=account.address, + asset_id=asset_id, + ) +) +``` + +## Breaking Changes + +1. **Client Management** + - Removal of standalone client creation functions + - All clients now accessed through `AlgorandClient` +2. **Account Management** + - Account creation functions moved to `AccountManager` accessible via `algorand.account` property + - Unified `TransactionSignerAccountProtocol` with compliant and typed `SigningAccount`, `TransactionSignerAccount`, `LogicSigAccount`, `MultiSigAccount` classes encapsulating low level `algosdk` abstractions. + - Improved typing for account operations, such as obtaining account information from `algod`, returning a typed information object. +3. **Transaction Management** + - Consistent and intuitive transaction creation and sending interface accessible via `algorand.{send|params|create_transaction}` properties + - New transaction composition interface accessible via `algorand.new_group` + - Removing necessity to interact with low level and untyped `algosdk` abstractions for assembling, signing and sending transaction(s). +4. **Application Client** + - Split into `AppClient`, `AppDeployer` and `AppFactory` + - New intuitive structured interface for creating or sending `AppCall`|`AppMethodCall` transactions + - ARC-56 support along with automatic conversion of specs from ARC-32 to ARC-56 +5. **State Management** + - New hierarchical state access available via `app_client.state.{global_state|local_state|box}` properties + - Improved typing for state values + - Support for ARC-56 state schemas +6. **Asset Management** + - Dedicated `AssetManager` class for asset management accessible via `algorand.asset` property + - Improved typing for asset operations, such as obtaining asset information from `algod`, returning a typed information object. + - Consistent interface for asset opt-in, transfer, freeze, etc. + +## Best Practices + +1. Use the new `AlgorandClient` as the main entry point +2. Leverage IDE autocompletion to discover available functionality, consult with [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) when unsure +3. Use the transaction parameter builders for type-safe transaction creation (`algorand.params.{}`) +4. Use the state accessor patterns for cleaner state management {`algorand.state.{}`} +5. Use high level `TransactionComposer` interface over low level `algosdk` abstractions (where possible) +6. Use source maps and debug mode to quickly troubleshoot on-chain errors +7. Use idempotent deployment patterns with versioning + +## Troubleshooting + +### A v2 interface/method/class does not display a deprecation warning correctly or at all + +Submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a description of the problem and the code that is causing it. + +### Useful scenario of converting v2 to v3 not covered in generic migration guide + +If you have a scenario that you think is useful and not covered in the generic migration guide, please submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a scenario. diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index 3864be4f..8db435d9 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -4,43 +4,56 @@ Account management is one of the core capabilities provided by AlgoKit Utils. It ## `AccountManager` -The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the `TransactionComposer` to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! +The [`AccountManager`](../apidocs/algokit_utils/accounts/index) is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](./transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! -To get an instance of `AccountManager`, you can use either `AlgorandClient` via `algorand.account` or instantiate it directly: +To get an instance of `AccountManager`, you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.account` or instantiate it directly: ```python -from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils import AccountManager account_manager = AccountManager(client_manager) ``` -## `TransactionSignerAccount` +## `TransactionSignerAccountProtocol` -The core internal type that holds information about a signer/sender pair for a transaction is `TransactionSignerAccount`, which represents an `algosdk.TransactionSigner` (`signer`) along with a sender address (`addr`) as the encoded string address. +The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](../apidocs/algokit_utils/protocols/account/index), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. -Many methods in `AccountManager` expose a `TransactionSignerAccount`. `TransactionSignerAccount` can be used with `AtomicTransactionComposer`, `TransactionComposer` and other Algorand SDK tools. +The following conform to `TransactionSignerAccountProtocol`: + +- [`TransactionSignerAccount`](../apidocs/algokit_utils/models/account/index) - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- [`SigningAccount`](../apidocs/algokit_utils/models/account/index) - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- [`LogicSigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- [`MultisigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` ## Registering a signer -The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by `AlgorandClient` to automatically sign transactions by that sender. Any of the methods within `AccountManager` that return an account will automatically register the signer with the sender. If however, you are creating a signer external to the `AccountManager`, then you need to register the signer with the `AccountManager` if you want it to be able to automatically sign transactions from that sender. +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by [`AlgorandClient`](./algorand-client.md) to automatically sign transactions by that sender. Any of the [methods](#accounts) within `AccountManager` that return an account will automatically register the signer with the sender. -There are two methods that can be used for this, `set_signer_from_account`, which takes any number of account based objects that combine signer and sender (`TransactionSignerAccount`, `SigningAccount`, `LogicSigAccount`, `MultiSigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of [account based objects](#underlying-account-classes) that combine signer and sender (`TransactionSignerAccount` | `SigningAccount` | `LogicSigAccount` | `MultisigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: ```python -algorand.account\ - .set_signer_from_account(SigningAccount.new_account())\ - .set_signer_from_account(LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)))\ - .set_signer_from_account(MultiSigAccount( - MultisigMetadata(version=1, threshold=1, addresses=["ADDRESS1...", "ADDRESS2..."]), - [account1, account2] - ))\ - .set_signer_from_account(TransactionSignerAccount(address="SENDERADDRESS", signer=transaction_signer))\ - .set_signer("SENDERADDRESS", transaction_signer) +algorand.account + .set_signer_from_account(TransactionSignerAccount(your_address, your_signer)) + .set_signer_from_account(SigningAccount.new_account()) + .set_signer_from_account( + LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)) + ) + .set_signer_from_account( + MultisigAccount( + MultisigMetadata( + version = 1, + threshold = 1, + addresses = ["ADDRESS1...", "ADDRESS2..."] + ), + [account1, account2] + ) + ) + .set_signer("SENDERADDRESS", transaction_signer) ``` ## Default signer -If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can register a default signer: +If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can [register a default signer](../code/classes/types_account_manager.AccountManager.md#setdefaultsigner): ```python algorand.account.set_default_signer(my_default_signer) @@ -48,39 +61,41 @@ algorand.account.set_default_signer(my_default_signer) ## Get a signer -`AlgorandClient` will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: +[`AlgorandClient`](./algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer](../apidocs/algokit_utils/accounts/account_manager/index#getsigner) for a given sender address: ```python signer = algorand.account.get_signer("SENDER_ADDRESS") ``` -If there is no signer registered for that sender address it will either return the default signer (if registered) or raise a `ValueError`. +If there is no signer registered for that sender address it will either return the default signer ([if registered](#default-signer)) or throw an exception. ## Accounts -In order to get/register accounts for signing operations you can use the following methods on `AccountManager`: +In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`](#accountmanager) (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](./algorand-client.md)): -- `algorand.account.from_environment(name, fund_with)` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `os.getenv('{NAME}_MNEMONIC')` and (optionally) `os.getenv('{NAME}_SENDER')` (if account is rekeyed) -- `algorand.account.from_mnemonic(mnemonic=mnemonic, sender=None)` - Registers and returns an account with secret key loaded by taking the mnemonic secret -- `algorand.account.multisig(metadata, signing_accounts)` - Registers and returns a multisig account with one or more signing keys loaded -- `algorand.account.rekeyed(sender=sender, account=account)` - Registers and returns an account representing the given rekeyed sender/signer combination -- `algorand.account.random()` - Returns a new, cryptographically randomly generated account with private key loaded -- `algorand.account.from_kmd(name, predicate=None, sender=None)` - Returns an account with private key loaded from the given KMD wallet (identified by name) -- `algorand.account.logicsig(program, args=None)` - Returns an account that represents a logic signature +- [`algorand.account.from_environment(name, fund_with)`](../apidocs/algokit_utils/accounts/account_manager/index#from_environment) - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) + - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code + - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD +- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`](../apidocs/algokit_utils/accounts/account_manager/index#from_mnemonic) - Registers and returns an account with secret key loaded by taking the mnemonic secret +- [`algorand.account.multisig(multisig_params, signing_accounts)`](../apidocs/algokit_utils/accounts/account_manager/index#multisig) - Registers and returns a multisig account with one or more signing keys loaded +- [`algorand.account.rekeyed(sender, signer)`](../apidocs/algokit_utils/accounts/account_manager/index#rekeyed) - Registers and returns an account representing the given rekeyed sender/signer combination +- [`algorand.account.random()`](../apidocs/algokit_utils/accounts/account_manager/index#random) - Returns a new, cryptographically randomly generated account with private key loaded +- [`algorand.account.from_kmd()`](../apidocs/algokit_utils/accounts/account_manager/index#from_kmd) - Returns an account with private key loaded from the given KMD wallet (identified by name) +- [`algorand.account.logicsig(program, args?)`](../apidocs/algokit_utils/accounts/account_manager/index#logicsig) - Returns an account that represents a logic signature ### Underlying account classes While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. -- `SigningAccount` - A class that holds the private key and address for an account, with support for rekeyed accounts -- `LogicSigAccount` - A wrapper around `algosdk.transaction.LogicSigAccount` object -- `MultiSigAccount` - A wrapper around Algorand SDK's multisig functionality that supports multisig accounts with one or more signers present -- `MultisigMetadata` - A dataclass containing the version, threshold and addresses for a multisig account +- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created +- [`SigningAccount`](../code/classes/types_account.SigningAccount.md) - An abstraction around `algosdk.Account` that supports rekeyed accounts +- `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object +- [`MultisigAccount`](../code/classes/types_account.MultisigAccount.md) - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present ### Dispenser -- `algorand.account.dispenser_from_environment()` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present -- `algorand.account.localnet_dispenser()` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account +- [`algorand.account.dispenserFromEnvironment()`](../code/classes/types_account_manager.AccountManager.md#dispenserfromenvironment) - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- [`algorand.account.localNetDispenser()`](../code/classes/types_account_manager.AccountManager.md#localnetdispenser) - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account ## Rekey account @@ -89,53 +104,61 @@ One of the unique features of Algorand is the ability to change the private key > [!WARNING] > Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. -You can issue a transaction to rekey an account by using the `algorand.account.rekey_account(account, rekey_to, **kwargs)` function: +You can issue a transaction to rekey an account by using the [`algorand.account.rekeyAccount(account, rekeyTo, options)`](../code/classes/types_account_manager.AccountManager.md#rekeyaccount) function: + +- `account: string | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekeyTo: string | TransactionSignerAccount` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- An `options` object, which has: + - [Common transaction parameters](./algorand-client.md#transaction-parameters) + - [Execution parameters](./algorand-client.md#sending-a-single-transaction) -- `account: str | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed -- `rekey_to: str | TransactionSignerAccountProtocol` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. -- Optional keyword arguments: - - Common transaction parameters - - Execution parameters +You can also pass in `rekeyTo` as a [common transaction parameter](./algorand-client.md#transaction-parameters) to any transaction. ### Examples ```python # Basic example (with string addresses) -algorand.account.rekey_account( - account="ACCOUNTADDRESS", - rekey_to="NEWADDRESS" -) + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", +}) # Basic example (with signer accounts) -algorand.account.rekey_account( - account=account1, - rekey_to=new_signer_account -) + +algorand.account.rekey_account({ + account: account1, + rekey_to: new_signer_account, +}) # Advanced example -algorand.account.rekey_account( - account="ACCOUNTADDRESS", - rekey_to="NEWADDRESS", - lease=b"lease", - note=b"note", - first_valid_round=1000, - validity_window=10, - extra_fee=AlgoAmount.from_micro_algos(1000), - static_fee=AlgoAmount.from_micro_algos(1000), - # Max fee doesn't make sense with extraFee AND staticFee - # already specified, but here for completeness - max_fee=AlgoAmount.from_micro_algos(3000), - suppress_log=True -) + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", + lease: "lease", + note: "note", + first_valid_round: 1000, + validity_window: 10, + extra_fee: AlgoAmount.from_micro_algos(1000), + static_fee: AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee: AlgoAmount.from_micro_algos(3000), + max_rounds_to_wait_for_confirmation: 5, + suppress_log: True, +}) + # Using a rekeyed account -# Note: if a signing account is passed into algorand.account.rekey_account -# then you don't need to call rekeyed to register the new signer -rekeyed_account = algorand.account.rekeyed(sender=account, account=new_account) + +Note: if a signing account is passed into `algorand.account.rekey_account` then you don't need to call `rekeyed_account` to register the new signer + +rekeyed_account = algorand.account.rekey_account(account, new_account) # rekeyed_account can be used to sign transactions on behalf of account... ``` -# KMD account management +## KMD account management When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for: @@ -144,20 +167,19 @@ When running LocalNet, you have an instance of the [Key Management Daemon](https The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that's needed. This code has been abstracted away into the `KmdAccountManager` class. -To get an instance of the `KmdAccountManager` class you can access it from `AlgorandClient` via `algorand.account.kmd` or instantiate it directly: +To get an instance of the `KmdAccountManager` class you can access it from [`AlgorandClient`](./algorand-client.md) via `algorand.account.kmd` or instantiate it directly (passing in a [`ClientManager`](./client.md)): ```python -from algokit_utils.accounts.kmd_account_manager import KmdAccountManager +from algokit_utils import KmdAccountManager -# Algod client only kmd_account_manager = KmdAccountManager(client_manager) ``` The methods that are available are: -- `get_wallet_account(wallet_name, predicate=None, sender=None)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). -- `get_or_create_wallet_account(name, fund_with=None)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. -- `get_localnet_dispenser_account()` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) +- [`get_wallet_account(wallet_name, predicate?, sender?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_wallet_account)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- [`get_or_create_wallet_account(name, fund_with?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_or_create_wallet_account)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- [`get_localnet_dispenser_account()`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_localnet_dispenser_account)` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) ```python # Get a wallet account that seeded the LocalNet network @@ -166,24 +188,26 @@ default_dispenser_account = kmd_account_manager.get_wallet_account( lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 ) # Same as above, but dedicated method call for convenience -localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +local_net_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() # Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD # if creating it then fund it with 2 ALGO from the default dispenser account new_account = kmd_account_manager.get_or_create_wallet_account( - "account1", - AlgoAmount.from_algos(2) + "account1", + AlgoAmount.from_algos(2) ) # This will return the same account as above since the name matches -existing_account = kmd_account_manager.get_or_create_wallet_account("account1") +existing_account = kmd_account_manager.get_or_create_wallet_account( + "account1" +) ``` -Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via `AlgorandClient`: +Some of this functionality is directly exposed from [`AccountManager`](#accountmanager), which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via [`AlgorandClient`](./algorand-client.md): ```python # Get and register LocalNet dispenser -localnet_dispenser = algorand.account.localnet_dispenser() +local_net_dispenser = algorand.account.localnet_dispenser() # Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD dispenser = algorand.account.dispenser_from_environment() # Get / create and register account from KMD idempotently by name -account1 = algorand.account.from_kmd("account1", fund_with=AlgoAmount.from_algos(2)) +account1 = algorand.account.from_kmd("account1", AlgoAmount.from_algos(2)) ``` diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md index 3e7c517f..3cf93ee4 100644 --- a/docs/source/capabilities/algorand-client.md +++ b/docs/source/capabilities/algorand-client.md @@ -1,8 +1,8 @@ # Algorand client -`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the [default entrypoint](../../../README.md) into AlgoKit Utils functionality. +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the [default entrypoint](../index.md#usage) into AlgoKit Utils functionality. -The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](todo_paste_url), e.g.: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](./capabilities/algorand-client.md), e.g.: ```python # Point to the network configured through environment variables or @@ -10,7 +10,7 @@ The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `Al # configuration algorand = AlgorandClient.from_environment() # Point to default LocalNet configuration -algorand = AlgorandClient.default_local_net() +algorand = AlgorandClient.default_localnet() # Point to TestNet using AlgoNode free tier algorand = AlgorandClient.testnet() # Point to MainNet using AlgoNode free tier @@ -25,7 +25,7 @@ algorand = AlgorandClient.from_config(algod_config=algod_config) algorand = AlgorandClient.from_config( algod_config=algod_config, indexer_config=indexer_config, - kmd_config=kmd_config, + kmd_config=kmd_config ) ``` @@ -33,8 +33,8 @@ algorand = AlgorandClient.from_config( Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. -```python -algorand = AlgorandClient.default_local_net() +```py +algorand = AlgorandClient.default_localnet() algod_client = algorand.client.algod indexer_client = algorand.client.indexer @@ -45,59 +45,68 @@ kmd_client = algorand.client.kmd The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. -- [`AccountManager`](todo_paste_url) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: - - `algorand.set_default_signer(signer)` - - - `algorand.set_signer_from_account(account)` - - - `algorand.set_signer(sender, signer)` -- [`AssetManager`](todo_paste_url) via `algorand.asset` -- [`ClientManager`](todo_paste_url) via `algorand.client` +- [`AccountManager`](./account.md) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.setDefaultSigner(signer)` - + - `algorand.setSignerFromAccount(account)` - + - `algorand.setSigner(sender, signer)` +- [`AssetManager`](./asset.md) via `algorand.asset` +- [`ClientManager`](./client.md) via `algorand.client` ## Creating and issuing transactions -`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](todo_paste_url)). +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](./transaction-composer.md)). ### Creating transactions -You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](todo_paste_url) class. Intellisense will guide you on the different options. +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](../autoapi/algokit_utils/applications/app_client.md#algokit_utils.applications.app_client.AlgorandClientTransactionCreator) class. Intellisense will guide you on the different options. The signature for the calls to send a single transaction usually look like: ```python -def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> Transaction: - """ - params: ComposerTransactionTypeParams - Transaction type specific parameters - common_params: CommonTransactionParams - Common transaction parameters - returns: Transaction - An unsigned algosdk.Transaction object, ready to be signed and sent - """ +algorand.create_transaction.{method}(params=TxnParams(...), send_params=SendParams(...)) -> Transaction: ``` -- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space or cmd+space). -- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) -- [`CommonTransactionParams`](todo_paste_url) are the [common transaction parameters](todo_paste_url) that can be specified for every single transaction -- `Transaction` is an unsigned `algosdk.Transaction` object, ready to be signed and sent +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils` and consist of: + - `AppCallParams`, + - `AppCreateParams`, + - `AppDeleteParams`, + - `AppUpdateParams`, + - `AssetConfigParams`, + - `AssetCreateParams`, + - `AssetDestroyParams`, + - `AssetFreezeParams`, + - `AssetOptInParams`, + - `AssetOptOutParams`, + - `AssetTransferParams`, + - `OfflineKeyRegistrationParams`, + - `OnlineKeyRegistrationParams`, + - `PaymentParams`, +- `SendParams` is a typed dictionary exposing setting to apply during send operation: + - `max_rounds_to_wait_for_confirmation: int | None` - The number of rounds to wait for confirmation. By default until the latest lastValid has past. + - `suppress_log: bool | None` - Whether to suppress log messages from transaction send, default: do not suppress. + - `populate_app_call_resources: bool | None` - Whether to use simulate to automatically populate app call resources in the txn objects. Defaults to `Config.populateAppCallResources`. + - `cover_app_call_inner_transaction_fees: bool | None` - Whether to use simulate to automatically calculate required app call inner transaction fees and cover them in the parent app call transaction fee The return type for the ABI method call methods are slightly different: ```python -def app_call_type_method_call(self, *, params: ComposerTransactionTypeParams, **common_params) -> BuiltTransactions: - """ - params: ComposerTransactionTypeParams - Transaction type specific parameters - common_params: CommonTransactionParams - Common transaction parameters - returns: BuiltTransactions - Container for transactions, method calls and signers - """ +algorand.createTransaction.app{call_type}_method_call(params=MethodCallParams(...), send_params=SendParams(...)) -> BuiltTransactions ``` +MethodCallParams is a union type that can be any of the Algorand method call types, exact dataclasses can be imported from `algokit_utils` and consist of: + +- `AppCreateMethodCallParams`, +- `AppCallMethodCallParams`, +- `AppDeleteMethodCallParams`, +- `AppUpdateMethodCallParams`, + Where `BuiltTransactions` looks like this: ```python -@dataclass +@dataclass(frozen=True) class BuiltTransactions: - """Container for built transactions and associated metadata""" - # The built transactions - transactions: list[Transaction] - # Any ABIMethod objects associated with any of the transactions in a dict keyed by transaction index - method_calls: dict[int, ABIMethod] - # Any TransactionSigner objects associated with any of the transactions in a dict keyed by transaction index + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] signers: dict[int, TransactionSigner] ``` @@ -105,133 +114,78 @@ This signifies the fact that an ABI method call can actually result in multiple ### Sending a single transaction -You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`](todo_paste_url) class. Intellisense will guide you on the different options. +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`](../autoapi/algokit_utils/applications/app_client.md#algokit_utils.applications.app_client.AlgorandClientTransactionSender) class. Intellisense will guide you on the different options. Further documentation is present in the related capabilities: -- [App management](todo_paste_url) -- [Asset management](todo_paste_url) -- [Algo transfers](todo_paste_url) +- [App management](./app.md) +- [Asset management](./asset.md) +- [Algo transfers](./transfer.md) The signature for the calls to send a single transaction usually look like: -```python -def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> SingleSendTransactionResult: - """ - params: ComposerTransactionTypeParams - Transaction type specific parameters - common_params: CommonAppCallParams & SendParams - Common parameters for app calls and transaction sending - returns: SingleSendTransactionResult - Result of sending a single transaction - """ -``` +`algorand.send.{method}(params=TxnParams, send_params=SendParams) -> SingleSendTransactionResult` - To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space). -- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) -- [`CommonAppCallParams`](todo_paste_url) are the [common app call transaction parameters](todo_paste_url) that can be specified for every single app transaction -- [`SendParams`](todo_paste_url) are the [parameters](todo_paste_url) that control execution semantics when sending transactions to the network -- [`SendSingleTransactionResult`](todo_paste_url) is all of the information that is relevant when [sending a single transaction to the network](todo_paste_url) +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils`. +- [`SendParams`](../autoapi/algokit_utils/models/transaction/SendParams.md) a typed dictionary exposing setting to apply during send operation. +- [`SendSingleTransactionResult`](../autoapi/algokit_utils/models/transaction/SendSingleTransactionResult.md) is all of the information that is relevant when [sending a single transaction to the network](./transaction.md#sending-a-transaction) -Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppress_log=True`. +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppressLog: true`. ### Composing a group of transactions -You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{type}()` methods on [`TransactionComposer`](todo_paste_url) to add a series of transactions. +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{Type}()` methods on [`TransactionComposer`](./transaction-composer.md) to add a series of transactions. -```python -result = ( - algorand +```typescript +result = (algorand .new_group() .add_payment( - sender="SENDERADDRESS", - receiver="RECEIVERADDRESS", - amount=microalgos(1) + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=1_000_000 # 1 Algo in microAlgos + ) ) - .add_asset_opt_in(sender="SENDERADDRESS", asset_id=12345) - .send() -) + .add_asset_opt_in( + AssetOptInParams( + sender="SENDERADDRESS", + asset_id=12345 + ) + ) + .send()) ``` -`new_group()` returns a new [`TransactionComposer`](todo_paste_url) instance, which can also return the group of transactions, simulate them and other things. +`new_group()` returns a new [`TransactionComposer`](./transaction-composer.md) instance, which can also return the group of transactions, simulate them and other things. ### Transaction parameters -To create a transaction you define a set of parameters as a Python params dataclass instance. - -The type [`TxnParams`](todo_paste_url) is a union type representing all of the transaction parameters that can be specified for constructing any Algorand transaction type. - -- `sender: str` - The address of the account sending the transaction -- `signer: TransactionSigner | TransactionSignerAccountProtocol | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured) -- `rekey_to: Optional[str]` - Change the signing key of the sender to the given address. **Warning:** Please be careful and read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/) -- `note: Optional[bytes | str]` - Note to attach to transaction (UTF-8 encoded if string). Max 1000 bytes -- `lease: Optional[bytes | str]` - Prevent duplicate transactions with same lease (max 32 bytes). [Lease documentation](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) -- Fee management: - - `static_fee: Optional[AlgoAmount]` - Fixed transaction fee (use `extra_fee` instead unless setting to 0) - - `extra_fee: Optional[AlgoAmount]` - Additional fee to cover inner transactions - - `max_fee: Optional[AlgoAmount]` - Maximum allowed fee (prevents overspending during congestion) -- Validity management: - - - `validity_window: Optional[int]` - Number of rounds transaction is valid (default: 10) - - `first_valid_round: Optional[int]` - Explicit first valid round (use with caution) - - `last_valid_round: Optional[int]` - Explicit last valid round (prefer `validity_window`) - -- [`SendParams`](todo_paste_url) - - `max_rounds_to_wait_for_confirmation: Optional[int]` - Maximum rounds to wait for confirmation - - `suppress_log: bool` - Suppress log messages (default: False) - - `populate_app_call_resources: bool` - Auto-populate app call resources using simulation (default: from config) - - `cover_app_call_inner_transaction_fees: bool` - Automatically cover inner transaction fees via simulation - -Some more transaction-specific parameters extend these base types: +To create a transaction you instantiate a relevant Transaction parameters dataclass from `algokit_utils.transactions import *` or `from algokit_utils import PaymentParams, AssetOptInParams, etc`. -#### Payment Transactions (`PaymentParams`) - -- `receiver: str` - Recipient address -- `amount: AlgoAmount` - Amount to send -- `close_remainder_to: Optional[str]` - Address to send remaining funds to (for account closure) - -#### Asset Transactions - -- `AssetTransferParams`: Asset transfers including opt-in -- `AssetCreateParams`: Asset creation -- `AssetConfigParams`: Asset configuration -- `AssetFreezeParams`: Asset freezing -- `AssetDestroyParams`: Asset destruction - -#### Application Transactions - -- `AppCallParams`: Generic application calls -- `AppCreateParams`: Application creation -- `AppUpdateParams`: Application update -- `AppDeleteParams`: Application deletion - -#### Key Registration - -- `OnlineKeyRegistrationParams`: Register online participation keys -- `OfflineKeyRegistrationParams`: Take account offline - -Usage example with `AlgorandClient`: - -```python -# Create transaction -payment = client.create_transaction.payment( - PaymentParams(sender=account.address, receiver=receiver, amount=AlgoAmount(1)) -) - -# Send transaction -result = client.send.send_transaction(payment, SendParams()) -``` +All transaction parameters share the following common base parameters: -These parameters are used with the [`TransactionComposer`](todo_paste_url) class which handles: +- [`CommonTransactionParams`](../autoapi/algokit_utils/models/transaction/CommonTransactionParams.md) + - `sender: str` - The address of the account sending the transaction. + - `signer: algosdk.TransactionSigner | TransactionSignerAccount | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured). + - `rekey_to: string | None` - Change the signing key of the sender to the given address. **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + - `note: bytes | str | None` - Note to attach to the transaction. Max of 1000 bytes. + - `lease: bytes | str | None` - Prevent multiple transactions with the same lease being included within the validity window. A [lease](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). + - Fee management + - `static_fee: AlgoAmount | None` - The static transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. + - `extra_fee: AlgoAmount | None` - The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. + - `max_fee: AlgoAmount | None` - Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. + - Round validity management + - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. + - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. + - `last_valid_round: bigint | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. -- Automatic fee calculation -- Validity window management -- Transaction grouping -- ABI method handling -- Simulation-based resource population +Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](./transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. ### Transaction configuration AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: -- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it's set to `1000`. -- `algorand.set_suggested_params(suggested_params, until=None)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to 10, except in [automated testing](./testing.md) where it's set to 1000 when targeting LocalNet. +- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) - `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) - `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md index 4478c918..f1d11807 100644 --- a/docs/source/capabilities/amount.md +++ b/docs/source/capabilities/amount.md @@ -2,7 +2,7 @@ Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. -Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the [modularity principle](todo_paste_url)) you can safely and explicitly convert to microAlgo or Algo. +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the {ref}`modularity principle `) you can safely and explicitly convert to microAlgo or Algo. To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 4c446ca3..fa202f7e 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -3,7 +3,7 @@ > [!NOTE] > This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](../../markdown/capabilities/app-deploy.md) and [App management](../../markdown/capabilities/app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](./app-deploy.md) and [App management](./app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. > [!NOTE] > If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don't know the app ID (deferred knowledge or the instance doesn't exist yet on the blockchain) or you have multiple app IDs @@ -113,8 +113,10 @@ app_client6 = factory.get_app_client_by_creator_and_name( Once you have an app factory you can perform the following actions: -- `factory.send.bare.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app -- `factory.deploy(params)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app +- `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. + +> See [API docs](../api/app-factory.md#deploy) for details on parameter signatures. ### Create @@ -122,7 +124,7 @@ The create method is a wrapper over the `app_create` (bare calls) and `app_creat - You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec - `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used -- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs](../api/app-factory.md#deploy) for details. ```python # Use no-argument bare-call @@ -156,7 +158,7 @@ result, app_client = factory.send.create( ## Updating and deleting an app -Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. ## Calling the app @@ -273,20 +275,20 @@ map_dict = app_client.state.global_state.get_map("myMap") There are various methods defined that let you read state from the smart contract app: -- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](todo_paste_url) -- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](todo_paste_url). -- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](todo_paste_url). -- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](todo_paste_url). -- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](todo_paste_url). -- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](todo_paste_url). -- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](todo_paste_url). +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](../api/app.md#get_global_state) +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](../api/app.md#get_local_state). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](../api/app.md#get_box_names). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](../api/app.md#get_box_value). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](../api/app.md#get_box_value_from_abi_type). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](../api/app.md#get_box_values). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](../api/app.md#get_box_values_from_abi_type). ```python global_state = app_client.get_global_state() local_state = app_client.get_local_state("ACCOUNTADDRESS") -box_name: BoxReference = "my-box" -box_name2: BoxReference = "my-box2" +box_name: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box") +box_name2: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box2") box_names = app_client.get_box_names() box_value = app_client.get_box_value(box_name) @@ -307,7 +309,7 @@ Often when calling a smart contract during development you will get logic errors When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](todo_paste_url) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. +The information in that error message can be parsed and when combined with the [source map from compilation](../api/app-deploy.md#compilation-and-template-substitution) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. @@ -328,17 +330,17 @@ Note: This information will only show if the app client / app factory has a sour - You have called `create`, `update` or `deploy` - You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) -- You had source maps present in an app factory and then used it to [create an app client](todo_paste_url) (they are automatically passed through) +- You had source maps present in an app factory and then used it to [create an app client](#dynamically-creating-clients-for-a-given-app-spec) (they are automatically passed through) If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: ```python -Config.configure({"debug": True}) +config.configure(debug=True) ``` If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. -When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](todo_paste_url). +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](./debugging.md). ## Default arguments diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index cd95d4e7..1028f32c 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -1,17 +1,16 @@ # App deployment -Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution +AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet). -App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, -particularly [App management](./app-client.md). It allows you to idempotently (with safe retryability) deploy an app, including deploy-time immutability and permanence control and -TEAL template substitution. +It's optional to use this functionality, since you can construct your own deployment logic using create / update / delete calls and your own mechanism to maintaining app metadata (like app IDs etc.), but this capability is an opinionated out-of-the-box solution that takes care of the heavy lifting for you. + +App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App management](./app.md). To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -## Design +## Smart contract development lifecycle -The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). -While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. +The design behind the deployment capability is unique. The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. Namely, it described the concept of a smart contract development lifecycle: @@ -34,87 +33,182 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) and - [ARC-0004](https://arc.algorand.foundation/ARCs/arc-0004) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be - different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance -- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier - development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) -- Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID - instead +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md), [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150) and [ARC-4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) +- Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead + +This design allows you to have the same deployment code across environments without having to specify an ID for each environment. This makes it really easy to apply [continuous delivery](https://continuousdelivery.com/) practices to your smart contract deployment and make the deployment process completely automated. + +## `AppDeployer` + +The [`AppDeployer`](../apidocs/algokit_utils/algokit_utils.md#appdeployer) is a class that is used to manage app deployments and deployment metadata. -## Finding apps by creator +To get an instance of `AppDeployer` you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](./app.md#appmanager), [`AlgorandClientTransactionSender`](./algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): -The `AppDeployer.get_creator_apps_by_name()` method performs indexer lookups to find all apps created by an account that were deployed using this framework. The results are cached in an `ApplicationLookup` object. +```python +from algokit_utils.app_deployer import AppDeployer +app_deployer = AppDeployer(app_manager, transaction_sender, indexer) ``` -ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} + +## Deployment metadata + +When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. + +The deployment metadata is defined in [`AppDeployMetadata`](../apidocs/algokit_utils/algokit_utils.md#appdeploymetadata), which is an object with: + +- `name: str` - The unique name identifier of the app within the creator account +- `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) +- `deletable: bool | None` - Whether or not the app is deletable (`true`) / permanent (`false`) / unspecified (`None`) +- `updatable: bool | None` - Whether or not the app is updatable (`true`) / immutable (`false`) / unspecified (`None`) + +An example of the ARC-2 transaction note that is attached as an app creation / update transaction note to specify this metadata is: + +``` +ALGOKIT_DEPLOYER:j{name:"MyApp",version:"1.0",updatable:true,deletable:false} ``` -Any creation transactions or update transactions are then retrieved and processed in chronological order to result in an `AppLookup` object +## Lookup deployed apps by name -Given there are a number of indexer calls to retrieve this data it's a non-trivial object to create, and it's recommended that for the duration you are performing a single deployment -you hold a value of it rather than recalculating it. Most AlgoKit Utils functions that need it will also take an optional value of it that will be used in preference to retrieving a -fresh version. +In order to resolve what apps have been previously deployed and their metadata, AlgoKit provides a method that does a series of indexer lookups and returns a map of name to app metadata via `get_creator_apps_by_name(creator_address)`. -## Deploying an application +```python +app_lookup = algorand.app_deployer.get_creator_apps_by_name("CREATORADDRESS") +app1_metadata = app_lookup.apps["app1"] +``` -The class that performs the deployment logic is `AppDeployer` with the `deploy` method. It performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn't it will create it. If the app does already exist then it will: +This method caches the result of the lookup, since it's a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -- Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the deployment configuration. +The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](../apidocs/algokit_utils/algokit_utils.md#applicationlookup): -It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. +```python +@dataclasses.dataclass +class ApplicationLookup: + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) +``` -`deploy` automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability. +The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](../apidocs/algokit_utils/algokit_utils.md#applicationmetadata). + +> Refer to the [API docs](../apidocs/algokit_utils/algokit_utils.md#applicationlookup) for latest information on exact types. + +## Performing a deployment + +In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. + +For example: + +```python +deployment_result = algorand.app_deployer.deploy( + AppDeployParams( + metadata=AppDeploymentMetaData( + name="MyApp", + version="1.0.0", + deletable=False, + updatable=False, + ), + create_params=AppCreateParams( + sender="CREATORADDRESS", + approval_program=approval_teal_template_or_byte_code, + clear_state_program=clear_state_teal_template_or_byte_code, + schema=StateSchema( + global_ints=1, + global_byte_slices=2, + local_ints=3, + local_byte_slices=4, + ), + # Other parameters if a create call is made... + ), + update_params=AppUpdateParams( + sender="SENDERADDRESS", + # Other parameters if an update call is made... + ), + delete_params=AppDeleteParams( + sender="SENDERADDRESS", + # Other parameters if a delete call is made... + ), + deploy_time_params={ + "VALUE": 1, # TEAL template variables to replace + }, + on_schema_break=OnSchemaBreak.Append, + on_update=OnUpdate.Update, + send_params=SendParams( + populate_app_call_resources=True, + # Other execution control parameters + ), + ) +) +``` -### Input parameters +This method performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn't it will create it. If the app does already exist then it will: + +- Detect if the app has been updated (i.e. the program logic has changed) and either fail, perform an update, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than were originally requested) and either fail, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. -The `AppDeployParams` dataclass accepts these key parameters: +It will automatically [add metadata to the transaction note of the create or update transactions](#deployment-metadata) that indicates the name, version, updatability and deletability of the contract. This metadata works in concert with [`appDeployer.get_creator_apps_by_name`](#lookup-deployed-apps-by-name) to allow the app to be reliably retrieved against that creator in it's currently deployed state. It will automatically update it's lookup cache so subsequent calls to `get_creator_apps_by_name` or `deploy` will use the latest metadata without needing to call indexer again. -- `metadata`: Required AppDeploymentMetaData containing name, version, deletable and updatable flags -- `deploy_time_params`: Optional TealTemplateParams for TEAL template substitution -- `on_schema_break`: Optional behavior for schema breaks - can be string literal "replace", "fail", "append" or OnSchemaBreak enum -- `on_update`: Optional behavior for updates - can be string literal "update", "replace", "fail", "append" or OnUpdate enum -- `create_params`: AppCreateParams or AppCreateMethodCallParams specifying app creation parameters -- `update_params`: AppUpdateParams or AppUpdateMethodCallParams specifying app update parameters -- `delete_params`: AppDeleteParams or AppDeleteMethodCallParams specifying app deletion parameters -- `existing_deployments`: Optional ApplicationLookup to cache and reduce indexer calls -- `ignore_cache`: When true, forces fresh indexer lookup even if creator apps are cached -- `max_fee`: Maximum microalgos to spend on any single transaction -- `send_params`: Additional transaction sending parameters (fee, signer, etc.) +`deploy` also automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability if the requisite template parameters are specified in the provided TEAL template. -### Error Handling +### Input parameters -Specific error cases that throw ValueError: +The first parameter `deployment` is an [`AppDeployParams`](../apidocs/algokit_utils/algokit_utils.md#appdeployparams), which is an object with: -- Schema break with on_schema_break=fail -- Update attempt on non-updatable app -- Replacement attempt on non-deletable app -- Invalid `existing_deployments` cache provided +- `metadata: AppDeployMetadata` - determines the [deployment metadata](#deployment-metadata) of the deployment +- `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](./app.md#creation) (raw parameters or ABI method call) +- `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](./app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic +- `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](./app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter +- `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution](#compilation-and-template-substitution) + - [`TealTemplateParams`](../apidocs/algokit_utils/algokit_utils.md#tealtemplateparams) is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onschemabreak) if schema requirements increase (values: 'replace', 'fail', 'append') +- `on_update: OnUpdate | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onupdate) if contract logic changes (values: 'update', 'replace', 'fail', 'append') +- `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries +- `ignore_cache: bool | None` - if True, bypasses cached deployment metadata +- Additional fields from [`SendParams`](../apidocs/algokit_utils/algokit_utils.md#sendparams) - transaction execution parameters ### Idempotency -`deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will -do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. +`deploy` is idempotent which means you can safely call it again multiple times and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. ### Compilation and template substitution -When compiling TEAL template code, the capabilities described in the [design above](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. +When compiling TEAL template code, the capabilities described in the [above design](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. -In order for a smart contract to be able to use this functionality, it must have a TEAL Template that contains the following: +In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn't (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn't (permanent) -If you are building a smart contract using the [Python AlgoKit template](https://github.com/algorandfoundation/algokit-python-template) it provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. + +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result](../apidocs/algokit_utils/algokit_utils.md#compiledteal) of substituting then compiling the TEAL template(s) in the following properties of the return value: + +- `compiled_approval: CompiledTeal | None` +- `compiled_clear: CompiledTeal | None` + +Template substitution is done by executing `algorand.app.compile_teal_template(teal_template_code, template_params, deployment_metadata)`, which in turn calls the following in order and returns the compilation result per above (all of which can also be invoked directly): + +- `AppManager.strip_teal_comments(teal_code)` - Strips out any TEAL comments to reduce the payload that is sent to algod and reduce the likelihood of hitting the max payload limit +- `AppManager.replace_template_variables(teal_template_code, template_values)` - Replaces the template variables by looking for `TMPL_{key}` +- `AppManager.replace_teal_template_deploy_time_control_params(teal_template_code, params)` - If `params` is provided, it allows for deploy-time immutability and permanence control by replacing `TMPL_UPDATABLE` with `params.get("updatable")` if not `None` and replacing `TMPL_DELETABLE` with `params.get("deletable")` if not `None` +- `algorand.app.compile_teal(teal_code)` - Sends the final TEAL to algod for compilation and returns the result including the source map and caches the compilation result within the `AppManager` instance ### Return value -`deploy` returns an `AppDeployResult` object containing: +When `deploy` executes it will return a [comprehensive result](../apidocs/algokit_utils/algokit_utils.md#appdeployresult) object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. + +The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): + +- `OperationPerformed.CREATE` - The smart contract app was created +- `OperationPerformed.UPDATE` - The smart contract app was updated +- `OperationPerformed.REPLACE` - The smart contract app was deleted and created again (in an atomic transaction) +- `OperationPerformed.NOTHING` - Nothing was done since it was detected the existing smart contract app deployment was up to date + +As well as the `operation_performed` parameter and the [optional compilation result](#compilation-and-template-substitution), the return value will have the [`ApplicationMetaData`](../code/classes/algokit_utils.applications.app_deployer.ApplicationMetaData.md) [fields](#deployment-metadata) present. + +Based on the value of `operation_performed`, there will be other data available in the return value: -- `operation_performed`: Enum indicating action taken (Create/Update/Replace/Nothing) -- `app`: ApplicationMetaData with final app state -- `create_result`: Transaction result if creation occurred -- `update_result`: Transaction result if update occurred -- `delete_result`: Transaction result if replacement occurred +- If `CREATE`, `UPDATE` or `REPLACE` then it will have the relevant [`SendAppTransactionResult`](./app.md#calling-an-app) values: + - `create_result` for create operations + - `update_result` for update operations +- If `REPLACE` then it will also have `delete_result` to capture the result of deleting the existing app diff --git a/docs/source/capabilities/app-manager.md b/docs/source/capabilities/app.md similarity index 100% rename from docs/source/capabilities/app-manager.md rename to docs/source/capabilities/app.md diff --git a/docs/source/capabilities/asset.md b/docs/source/capabilities/asset.md new file mode 100644 index 00000000..731af016 --- /dev/null +++ b/docs/source/capabilities/asset.md @@ -0,0 +1,134 @@ +# Assets + +The Algorand Standard Asset (ASA) management functions include creating, opting in and transferring assets, which are fundamental to asset interaction in a blockchain environment. + +## `AssetManager` + +The `AssetManager` class provides functionality for managing Algorand Standard Assets (ASAs). It can be accessed through the `AlgorandClient` via `algorand.asset` or instantiated directly: + +```python +from algokit_utils import AssetManager, TransactionComposer +from algosdk.v2client import algod + +asset_manager = AssetManager( + algod_client=algod_client, + new_group=lambda: TransactionComposer() +) +``` + +## Asset Information + +The `AssetManager` provides two key data classes for asset information: + +### `AssetInformation` + +Contains details about an Algorand Standard Asset (ASA): + +```python +@dataclass +class AssetInformation: + asset_id: int # The ID of the asset + creator: str # Address of the creator account + total: int # Total units created + decimals: int # Number of decimal places + default_frozen: bool | None = None # Whether asset is frozen by default + manager: str | None = None # Optional manager address + reserve: str | None = None # Optional reserve address + freeze: str | None = None # Optional freeze address + clawback: str | None = None # Optional clawback address + unit_name: str | None = None # Optional unit name (e.g. ticker) + asset_name: str | None = None # Optional asset name + url: str | None = None # Optional URL for more info + metadata_hash: bytes | None = None # Optional 32-byte metadata hash +``` + +### `AccountAssetInformation` + +Contains information about an account's holding of a particular asset: + +```python +@dataclass +class AccountAssetInformation: + asset_id: int # The ID of the asset + balance: int # Amount held by the account + frozen: bool # Whether frozen for this account + round: int # Round this info was retrieved at +``` + +## Bulk Operations + +The `AssetManager` provides methods for bulk opt-in/opt-out operations: + +### Bulk Opt-In + +```python +# Basic example +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + signer=transaction_signer, + note=b"opt-in note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +### Bulk Opt-Out + +```python +# Basic example +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + ensure_zero_balance=True, + signer=transaction_signer, + note=b"opt-out note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +The bulk operations return a list of `BulkAssetOptInOutResult` objects containing: + +- `asset_id`: The ID of the asset opted into/out of +- `transaction_id`: The transaction ID of the opt-in/out + +## Get Asset Information + +### Getting Asset Parameters + +You can get the current parameters of an asset from algod using `get_by_id()`: + +```python +asset_info = asset_manager.get_by_id(12345) +``` + +### Getting Account Holdings + +You can get an account's current holdings of an asset using `get_account_information()`: + +```python +address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +asset_id = 12345 +account_info = asset_manager.get_account_information(address, asset_id) +``` diff --git a/docs/source/capabilities/client.md b/docs/source/capabilities/client.md index 206c3c47..00b92731 100644 --- a/docs/source/capabilities/client.md +++ b/docs/source/capabilities/client.md @@ -13,24 +13,19 @@ The `ClientManager` is a class that is used to manage client instances. To get an instance of `ClientManager` you can instantiate it directly: ```python -from algokit_utils import ClientManager +from algokit_utils import ClientManager, AlgoSdkClients, AlgoClientConfigs from algosdk.v2client.algod import AlgodClient +# Using AlgoSdkClients algod_client = AlgodClient(...) algorand_client = ... # Get AlgorandClient instance from somewhere +clients = AlgoSdkClients(algod=algod_client, indexer=indexer_client, kmd=kmd_client) +client_manager = ClientManager(clients, algorand_client) -# Using existing client instances -client_manager = ClientManager( - {"algod": algod_client, "indexer": indexer_client, "kmd": kmd_client}, - algorand_client=algorand_client -) - -# Using configs -algod_config = {"server": "https://..."} -client_manager = ClientManager( - {"algod_config": algod_config}, - algorand_client=algorand_client -) +# Using AlgoClientConfigs +algod_config = AlgoClientNetworkConfig(server="https://...", token="") +configs = AlgoClientConfigs(algod_config=algod_config) +client_manager = ClientManager(configs, algorand_client) ``` ## Network configuration @@ -39,18 +34,17 @@ The network configuration is specified using the `AlgoClientConfig` type. This s There are a number of ways to produce one of these configuration objects: -- Manually specifying a dictionary that conforms with the type, e.g. +- Manually specifying a dataclass, e.g. + ```python - { - "server": "https://myalgodnode.com" - } - # Or with the optional values: - { - "server": "https://myalgodnode.com", - "port": 443, - "token": "SECRET_TOKEN" - } + from algokit_utils import AlgoClientNetworkConfig + + config = AlgoClientNetworkConfig( + server="https://myalgodnode.com", + token="SECRET_TOKEN" # optional + ) ``` + - `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change - `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables - `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change @@ -102,11 +96,13 @@ You can get information about the current network you are connected to: ```python # Get network information network = client_manager.network() -print(f"Connected to: {network.name}") # e.g., "mainnet", "testnet", "localnet" +print(f"Is mainnet: {network.is_mainnet}") +print(f"Is testnet: {network.is_testnet}") +print(f"Is localnet: {network.is_localnet}") print(f"Genesis ID: {network.genesis_id}") print(f"Genesis hash: {network.genesis_hash}") -# Check specific network types +# Convenience methods is_mainnet = client_manager.is_mainnet() is_testnet = client_manager.is_testnet() is_localnet = client_manager.is_localnet() diff --git a/docs/source/capabilities/debugger.md b/docs/source/capabilities/debugging.md similarity index 75% rename from docs/source/capabilities/debugger.md rename to docs/source/capabilities/debugging.md index 948fed17..66ec29db 100644 --- a/docs/source/capabilities/debugger.md +++ b/docs/source/capabilities/debugging.md @@ -11,6 +11,7 @@ The `config.py` file contains the `UpdatableConfig` class which manages and upda - `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. - `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. - `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. +- `populate_app_call_resources`: Indicates whether to populate app call resources. Defaults to false, which means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will not populate app call resources. The `configure` method can be used to set these attributes. @@ -19,28 +20,13 @@ To enable debug mode in your project you can configure it as follows: ```python from algokit_utils.config import config -config.configure(debug=True) -``` - -## Configuration Options - -The `UpdatableConfig` class provides several configuration options that affect debugging behavior: - -- `debug` (bool): Indicates whether debug mode is enabled. -- `project_root` (Path | None): The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with AlgoKit AVM Debugger. Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. -- `trace_all` (bool): Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. -- `trace_buffer_size_mb` (float): The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. -- `max_search_depth` (int): The maximum depth to search for an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. - -You can configure these options as follows: - -```python config.configure( debug=True, project_root=Path("./my-project"), trace_all=True, trace_buffer_size_mb=512, - max_search_depth=15 + max_search_depth=15, + populate_app_call_resources=True, ) ``` diff --git a/docs/source/capabilities/testing.md b/docs/source/capabilities/testing.md new file mode 100644 index 00000000..bdc6ff7a --- /dev/null +++ b/docs/source/capabilities/testing.md @@ -0,0 +1,204 @@ +# Testing + +The following is a collection of useful snippets that can help you get started with testing your Algorand applications using AlgoKit utils. For the sake of simplicity, we'll use [pytest](https://docs.pytest.org/en/latest/) in the examples below. + +## Basic Test Setup + +Here's a basic test setup using pytest fixtures that provides common testing utilities: + +```python +import pytest +from algokit_utils import Account, SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.amount import AlgoAmount + +@pytest.fixture +def algorand() -> AlgorandClient: + """Get an AlgorandClient instance configured for LocalNet""" + return AlgorandClient.default_localnet() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + """Create and fund a test account with ALGOs""" + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_algos(100), + min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account +``` + +Refer to [pytest fixture scopes](https://docs.pytest.org/en/latest/how-to/fixtures.html#fixture-scopes) for more information on how to control lifecycle of fixtures. + +## Creating Test Assets + +Here's a helper function to create test ASAs (Algorand Standard Assets): + +```python +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None = None) -> int: + """Create a test asset and return its ID""" + if total is None: + total = random.randint(20, 120) + + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TST", + asset_name=f"Test Asset {random.randint(1,100)}", + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) + ) + + return int(create_result.confirmation["asset-index"]) +``` + +## Testing Application Deployments + +Here's how one can test smart contract application deployments: + +```python +def test_app_deployment(algorand: AlgorandClient, funded_account: SigningAccount): + """Test deploying a smart contract application""" + + # Load the application spec + app_spec = Path("artifacts/application.json").read_text() + + # Create app factory + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + + # Deploy the app + app_client, deploy_response = factory.deploy( + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, + ) + + # Verify deployment + assert deploy_response.app.app_id > 0 + assert deploy_response.app.app_address +``` + +## Testing Asset Transfers + +Here's how one can test ASA transfers between accounts: + +```python +def test_asset_transfer(algorand: AlgorandClient, funded_account: SigningAccount): + """Test ASA transfers between accounts""" + + # Create receiver account + receiver = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=receiver, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1) + ) + + # Create test asset + asset_id = generate_test_asset(algorand, funded_account, 100) + + # Opt receiver into asset + algorand.send.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer + ) + ) + + # Transfer asset + transfer_amount = 5 + result = algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=receiver.address, + asset_id=asset_id, + amount=transfer_amount + ) + ) + + # Verify transfer + receiver_balance = algorand.asset.get_account_information(receiver, asset_id) + assert receiver_balance.balance == transfer_amount +``` + +## Testing Application Calls + +Here's how to test application method calls: + +```python +def test_app_method_call(algorand: AlgorandClient, funded_account: SigningAccount): + """Test calling ABI methods on an application""" + + # Deploy application first + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Call application method + result = app_client.send.call( + AppClientMethodCallParams( + method="hello", + args=["world"] + ) + ) + + # Verify result + assert result.abi_return == "Hello, world" +``` + +## Testing Box Storage + +Here's how to test application box storage: + +```python +def test_box_storage(algorand: AlgorandClient, funded_account: SigningAccount): + """Test application box storage""" + + # Deploy application + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Fund app account for box storage MBR + app_client.fund_app_account( + FundAppAccountParams(amount=AlgoAmount.from_algos(1)) + ) + + # Store value in box + box_name = b"test_box" + box_value = "test_value" + app_client.send.call( + AppClientMethodCallParams( + method="set_box", + args=[box_name, box_value], + box_references=[box_name] + ) + ) + + # Verify box value + stored_value = app_client.get_box_value(box_name) + assert stored_value == box_value.encode() +``` diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md index 0e6a3331..baba7dd3 100644 --- a/docs/source/capabilities/transaction-composer.md +++ b/docs/source/capabilities/transaction-composer.md @@ -68,6 +68,7 @@ Parameter types include: - `AppDeleteParams` - For deleting applications - `AppDeleteMethodCallParams` - For deleting applications with ABI method calls - `OnlineKeyRegistrationParams` - For online key registration transactions +- `OfflineKeyRegistrationParams` - For offline key registration transactions The methods to construct a transaction are all named `add_{transaction_type}` and return an instance of the composer so they can be chained together fluently to construct a transaction group. @@ -190,7 +191,7 @@ result = ( args=[1, 2, 3] # Resources will be automatically populated! )) - .send(populate_app_call_resources=True) + .send(send_params=SendParams(populate_app_call_resources=True)) ) # Or disable automatic population @@ -207,7 +208,7 @@ result = ( asset_references=[789], box_references=[box_reference] )) - .send(populate_app_call_resources=False) + .send(send_params=SendParams(populate_app_call_resources=False)) ) ``` diff --git a/docs/source/capabilities/transfer.md b/docs/source/capabilities/transfer.md index f2299170..2f61e8e2 100644 --- a/docs/source/capabilities/transfer.md +++ b/docs/source/capabilities/transfer.md @@ -47,6 +47,8 @@ result2 = algorand_client.send.payment( # generally you'd register it with AlgorandClient # against the sender and not need to pass it in signer=transaction_signer, + ), + send_params=SendParams( max_rounds_to_wait=5, suppress_log=True, ) @@ -59,8 +61,8 @@ The `ensure_funded` function automatically funds an account to maintain a minimu There are 3 variants of this function: -- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). -- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). - **Note:** requires environment variables to be set. - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` if it's a rekeyed account, or against default LocalNet if no environment variables present. @@ -93,7 +95,9 @@ algorand_client.account.ensure_funded( AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), fee=AlgoAmount(1000, "microalgo"), - suppress_log=True, + send_params=SendParams( + suppress_log=True, + ), ) # From environment @@ -106,7 +110,9 @@ algorand_client.account.ensure_funded_from_environment( AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), fee=AlgoAmount(1000, "microalgo"), - suppress_log=True, + send_params=SendParams( + suppress_log=True, + ), ) # TestNet Dispenser API diff --git a/docs/source/capabilities/typed-app-clients.md b/docs/source/capabilities/typed-app-clients.md new file mode 100644 index 00000000..710323c1 --- /dev/null +++ b/docs/source/capabilities/typed-app-clients.md @@ -0,0 +1,200 @@ +# Typed application clients + +Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) or [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract. + +Typed application clients are the recommended way of interacting with smart contracts. If you don't have/want a typed client, but have an ARC-56/ARC-32 app spec then you can use the [non-typed application clients](./app-client.md) and if you want to call a smart contract you don't have an app spec file for you can use the underlying [app management](./app.md) and [app deployment](./app-deploy.md) functionality to manually construct transactions. + +## Generating an app spec + +You can generate an app spec file: + +- Using [Algorand Python](https://algorandfoundation.github.io/puya/#quick-start) +- Using [TEALScript](https://tealscript.netlify.app/tutorials/hello-world/0004-artifacts/) +- By hand by following the specification [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258)/[ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) +- Using [Beaker](https://algorand-devrel.github.io/beaker/html/usage.html) (PyTEAL) _(DEPRECATED)_ + +## Generating a typed client + +To generate a typed client from an app spec file you can use [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients): + +``` +> algokit generate client application.json --output /absolute/path/to/client.py +``` + +Note: AlgoKit Utils >= 3.0.0 is compatible with the older 1.x.x generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 2.x.x. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version. + +## Getting a typed client instance + +To get an instance of a typed client you can use an [`AlgorandClient`](./algorand-client.md) instance or a typed app [`Factory`](#creating-a-typed-factory-instance) instance. + +The approach to obtaining a client instance depends on how many app clients you require for a given app spec and if the app has already been deployed, which is summarised below: + +### App is deployed + + + + + + + + + + + + + + + + + + + + + + +
Resolve App by IDResolve App by Creator and Name
Single App Client InstanceMultiple App Client InstancesSingle App Client InstanceMultiple App Client Instances
+ +```python +app_client = algorand.client.get_typed_app_client_by_id(MyContractClient, { + app_id=1234, + # ... +}) +# or +app_client = MyContractClient({ + algorand, + app_id=1234, + # ... +}) +``` + + + +```python +app_client1 = factory.get_app_client_by_id( + app_id=1234, + # ... +) +app_client2 = factory.get_app_client_by_id( + app_id=4321, + # ... +) +``` + + + +```python +app_client = algorand.client.get_typed_app_client_by_creator_and_name( + MyContractClient, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +# or +app_client = MyContractClient.from_creator_and_name( + algorand, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +``` + + + +```python +app_client1 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +app_client2 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name-2", + # ... +) +``` + +
+ +To understand the difference between resolving by ID vs by creator and name see the underlying [app client documentation](./app-client.md#appclient). + +### App is not deployed + + + + + + + + + + + + + + +
Deploy a New AppDeploy or Resolve App Idempotently by Creator and Name
+ +```python +app_client, response = factory.deploy( + args=[], + # ... +) +# or +app_client, response = factory.send.create.METHODNAME( + args=[], + # ... +) +``` + + + +```python +app_client, response = factory.deploy( + app_name="contract-name", + # ... +) +``` + +
+ +### Creating a typed factory instance + +If your scenario calls for an app factory, you can create one using the below: + +```python +factory = algorand.client.get_typed_app_factory(MyContractFactory) +# or +factory = MyContractFactory(algorand) +``` + +## Client usage + +See the [official usage docs](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/usage.md) for full details. + +For a simple example that deploys a contract and calls a `"hello"` method, see below: + +```python +# A similar working example can be seen in the AlgoKit init production smart contract templates, when using Python deployment +# In this case the generated factory is called `HelloWorldAppFactory` and is in `./artifacts/HelloWorldApp/client.py` +from artifacts.hello_world_app.client import HelloWorldAppClient, HelloArgs +from algokit_utils import AlgorandClient + +# These require environment variables to be present, or it will retrieve from default LocalNet +algorand = AlgorandClient.from_environment() +deployer = algorand.account.from_environment("DEPLOYER", AlgoAmount.from_algo(1)) + +# Create the typed app factory +factory = algorand.client.get_typed_app_factory(HelloWorldAppFactory, + creator_address=deployer, + default_sender=deployer, +) + +# Create the app and get a typed app client for the created app (note: this creates a new instance of the app every time, +# you can use .deploy() to deploy idempotently if the app wasn't previously +# deployed or needs to be updated if that's allowed) +app_client, response = factory.send.create() + +# Make a call to an ABI method and print the result +response = app_client.send.hello(args=HelloArgs(name="world")) +print(response) +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 35b6a76e..72fa0770 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,75 +1,3 @@ -from __future__ import annotations - -import typing as t - -import typing_extensions as te -from autodoc2.render.myst_ import MystRenderer -from autodoc2.utils import ItemData -from sphinx.domains.python import PythonDomain - - -class AlgoKitRenderer(MystRenderer): - """Render the documentation as MyST. - - Based on the code in - https://github.com/bytewax/bytewax/blob/58bc1d9517f11578c914407a057960b76d8d9b1b/docs/renderer.py#L16 - - """ - - @te.override - def render_package(self, item: ItemData) -> t.Iterable[str]: # noqa: C901 - if self.standalone and self.is_hidden(item): - yield from ["---", "orphan: true", "---", ""] - - full_name = item["full_name"] - - yield f"# {{py:mod}}`{full_name}`" - yield "" - - yield f"```{{py:module}} {full_name}" - if self.no_index(item): - yield ":noindex:" - if self.is_module_deprecated(item): - yield ":deprecated:" - yield from ["```", ""] - - if self.show_docstring(item): - yield f"```{{autodoc2-docstring}} {item['full_name']}" - if parser_name := self.get_doc_parser(item["full_name"]): - yield f":parser: {parser_name}" - yield ":allowtitles:" - yield "```" - yield "" - - visible_submodules = [i["full_name"] for i in self.get_children(item, {"module", "package"})] - if visible_submodules: - yield "## Submodules" - yield "" - yield "```{toctree}" - yield ":titlesonly:" - yield "" - yield from sorted(visible_submodules) - yield "```" - yield "" - - visible_children = [i["full_name"] for i in self.get_children(item) if i["type"] not in ("package", "module")] - if not visible_children: - return - - for heading, types in [ - ("Data", {"data"}), - ("Classes", {"class"}), - ("Functions", {"function"}), - ("External", {"external"}), - ]: - visible_items = list(self.get_children(item, types)) - if visible_items: - yield from [f"## {heading}", ""] - for i in visible_items: - yield from self.render_item(i["full_name"]) - yield "" - - # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -77,74 +5,52 @@ def render_package(self, item: ItemData) -> t.Iterable[str]: # noqa: C901 # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from __future__ import annotations -project = "algokit-utils" -copyright = "2023, Algorand Foundation" # noqa: A001 -author = "Algorand Foundation" -release = "1.0" +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + from sphinx.domains.python import PyObject + +project = 'algokit-utils-py' +copyright = '2025, Algorand Foundation' +author = 'Algorand Foundation' +release = '3.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [ - "sphinx.ext.githubpages", - "sphinx.ext.intersphinx", - "myst_parser", - "autodoc2", -] -templates_path = ["_templates"] -exclude_patterns = [] # type: ignore -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "algosdk": ("https://py-algorand-sdk.readthedocs.io/en/latest", None), - "pyteal": ("https://pyteal.readthedocs.io/en/stable/", None), -} -# allows type aliases to be used as type references -PythonDomain.object_types["data"].roles = ("data", "class", "obj") - +extensions = ['myst_parser', 'autoapi.extension', "sphinx.ext.autosectionlabel", "sphinx.ext.githubpages"] + +templates_path = ['_templates'] +exclude_patterns = [] + +autoapi_dirs = ['../../src/algokit_utils'] +autoapi_options = ['members', + 'undoc-members', + 'show-inheritance', + 'show-module-summary'] +autoapi_ignore = ['*algokit_utils/beta/__init__.py', + '*algokit_utils/asset.py', + '*algokit_utils/deploy.py', + "*algokit_utils/network_clients.py", + "*algokit_utils/common.py", + "*algokit_utils/account.py", + "*algokit_utils/application_client.py", + "*algokit_utils/application_specification.py", + "*algokit_utils/logic_error.py", + "*algokit_utils/dispenser_api.py"] + +myst_heading_anchors = 5 +myst_all_links_external = False +autosectionlabel_prefix_document = True # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "sphinx_rtd_theme" -html_static_path = [] # type: ignore - - -# -- Options for myst --- -myst_enable_extensions = [ - "colon_fence", - "fieldlist", - "deflist", - "tasklist", - "attrs_inline", - "attrs_block", - "substitution", - "linkify", -] - -myst_heading_anchors = 3 -myst_all_links_external = False - -# -- Options for autodoc2 --- -autodoc2_packages = [ - { - "path": "../../src/algokit_utils", - }, -] -autodoc2_skip_module_regexes = [r"algokit_utils\..*"] -autodoc2_module_all_regexes = [ - r"algokit_utils", -] -autodoc2_docstring_parser_regexes = [ - # this will render all docstrings as Markdown - (r".*", "myst"), -] -autodoc2_hidden_objects = [ - "undoc", # undocumented objects - "dunder", # double-underscore methods, e.g. __str__ - "private", # single-underscore methods, e.g. _private - "inherited", -] -autodoc2_render_plugin = AlgoKitRenderer -autodoc2_sort_names = True -autodoc2_index_template = None +html_theme = 'furo' +html_static_path = ['_static'] +pygments_style = "sphinx" +pygments_dark_style = "monokai" +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] diff --git a/docs/source/index.md b/docs/source/index.md index 50bb0ea8..1cc22eff 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -8,7 +8,7 @@ The goal of this library is to provide intuitive, productive utility functions t If you prefer TypeScript there's an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). ``` -[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Config and logging](#config-and-logging) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) +{ref}`Core principles ` | {ref}`Installation ` | {ref}`Usage ` | {ref}`Config and logging ` | {ref}`Capabilities ` | {ref}`Reference docs ` ```{toctree} --- @@ -17,19 +17,21 @@ caption: Contents --- capabilities/account -capabilities/client +capabilities/algorand-client +capabilities/amount capabilities/app-client capabilities/app-deploy -capabilities/transfer -capabilities/dispenser-client -capabilities/debugger +capabilities/app capabilities/asset +capabilities/client +capabilities/debugging +capabilities/dispenser-client capabilities/testing -capabilities/indexer +capabilities/transaction-composer capabilities/transaction -capabilities/amount -capabilities/app -apidocs/algokit_utils/algokit_utils +capabilities/transfer +capabilities/typed-app-clients +v3-migration-guide ``` (core-principles)= @@ -70,106 +72,84 @@ algorand = AlgorandClient.default_localnet() algorand = AlgorandClient.testnet() # Point to MainNet using AlgoNode free tier algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=...) +# Point to a pre-created algod and indexer client +algorand = AlgorandClient.from_clients(algod=..., indexer=..., kmd=...) +# Point to custom configuration for algod +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod and indexer and kmd +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +indexer_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +kmd_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) ``` -# Config and logging +# Testing -The library provides configuration and logging capabilities through the `config` module: +AlgoKit Utils provides a dedicated documentation page on various useful snippets that can be reused for testing with tools like [Pytest](https://docs.pytest.org/en/latest/): -```python -from algokit_utils.config import config +- [Testing](capabilities/testing) -# Enable debug mode -config.configure(debug=True) -# Configure project root for debug traces -config.configure(project_root=Path("./my-project")) -# Enable tracing of all operations -config.configure(trace_all=True) -``` - -(capabilities)= +# Types -# Capabilities +The library leverages Python's native type hints and is fully compatible with [MyPy](https://mypy-lang.org/) for static type checking. -The library provides a comprehensive set of capabilities to interact with Algorand: +All public abstractions and methods are organized in logical modules matching their domain functionality. You can import types either directly from the root module or from their source submodules. Refer to [API documentation](autoapi/index) for more details. -## Core capabilities +(config-logging)= -### Client Management - -- Create and manage algod, indexer and kmd clients -- Auto-retry functionality for transient errors -- Environment-based configuration -- Network detection and information - -### Account Management +# Config and logging -- Create and manage various account types (mnemonic, multisig, rekeyed) -- Transaction signing and management -- KMD integration for LocalNet -- Environment variable injection +To configure the AlgoKit Utils library you can make use of the [`Config`](autoapi/algokit_utils/config/index) object, which has a configure method that lets you configure some or all of the configuration options. -### Transaction Management +## Config singleton -- Atomic transaction composition -- Transaction simulation -- Automatic resource population -- Fee management -- ABI method call support +The AlgoKit Utils configuration singleton can be updated using `config.configure()`. Refer to the [Config API documentation](autoapi/algokit_utils/config/index) for more details. -### Amount Handling +## Logging -- Safe Algo amount manipulation -- Explicit microAlgo/Algo conversion -- Arithmetic operations -- Comparison operations +AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`](apidocs/algokit_utils/config/index) class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. -## Higher-order Use Cases +Each method supports optional suppression of output using the `suppress_log` parameter. -### Application Management +## Debug mode -- Smart contract deployment -- ARC-32/56 application clients -- State management -- Box storage -- Application calls +To turn on debug mode you can use the following: -### Asset Management +```python +from algokit_utils.config import config +config.configure(debug=True) +``` -- ASA creation and configuration -- Asset transfers -- Opt-in/out management -- Asset destruction +To retrieve the current debug state you can use `debug` property. -### Testing and Debugging +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](capabilities/debugger). It's likely this option will result in extra HTTP calls to algod os worth being careful when it's turned on. -- Transaction simulation -- AVM Debugger support -- Trace management +(capabilities)= -### Utility Functions +# Capabilities -- Algo transfers -- Account funding -- TestNet dispenser integration -- Indexer pagination +The library helps you interact with and develop against the Algorand blockchain with a series of end-to-end capabilities as described below: + +- [**AlgorandClient**](./capabilities/algorand-client.md) - The key entrypoint to the AlgoKit Utils functionality +- **Core capabilities** + - [**Client management**](./capabilities/client.md) - Creation of (auto-retry) algod, indexer and kmd clients against various networks resolved from environment or specified configuration, and creation of other API clients (e.g. TestNet Dispenser API and app clients) + - [**Account management**](./capabilities/account.md) - Creation, use, and management of accounts including mnemonic, rekeyed, multisig, transaction signer, idempotent KMD accounts and environment variable injected + - [**Algo amount handling**](./capabilities/amount.md) - Reliable, explicit, and terse specification of microAlgo and Algo amounts and safe conversion between them + - [**Transaction management**](./capabilities/transaction.md) - Ability to construct, simulate and send transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, validity, signing, and sending behaviour +- **Higher-order use cases** + - [**Asset management**](./capabilities/asset.md) - Creation, transfer, destroying, opting in and out and managing Algorand Standard Assets + - [**Typed application clients**](./capabilities/typed-app-clients.md) - Type-safe application clients that are [generated](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients) from ARC-56 or ARC-32 application spec files and allow you to intuitively and productively interact with a deployed app, which is the recommended way of interacting with apps and builds on top of the following capabilities: + - [**ARC-56 / ARC-32 App client and App factory**](./capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities (below) to provide a high productivity application client that works with ARC-56 and ARC-32 application spec defined smart contracts + - [**App management**](./capabilities/app.md) - Creation, updating, deleting, calling (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes) + - [**App deployment**](./capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution + - [**Algo transfers (payments)**](./capabilities/transfer.md) - Ability to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding + - [**Automated testing**](./capabilities/testing.md) - Reusable snippets to leverage AlgoKit Utils abstractions in a manner that are useful for when writing tests in tools like [Pytest](https://docs.pytest.org/en/latest/). (reference-documentation)= # Reference documentation For detailed API documentation, see the [auto-generated reference documentation](apidocs/algokit_utils/algokit_utils.md). - -# Contributing - -This is an open source project managed by the Algorand Foundation. See the [AlgoKit contributing page](https://github.com/algorandfoundation/algokit-cli/blob/main/CONTRIBUTING.MD) to learn about making improvements. - -To successfully run the tests in this repository you need to be running LocalNet via [AlgoKit](https://github.com/algorandfoundation/algokit-cli): - -```bash -algokit localnet start -``` - -# Indices and tables - -- {ref}`genindex` diff --git a/docs/source/migration-guide.md b/docs/source/migration-guide.md deleted file mode 100644 index a766ff33..00000000 --- a/docs/source/migration-guide.md +++ /dev/null @@ -1,252 +0,0 @@ -# v3 Migration Guide - -Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: - -- Easier and simpler consumption experience guided by IDE autocompletion -- Less redundant parameter passing (e.g., `algod` client) -- Better performance through caching of commonly retrieved values like transaction parameters -- More consistent and intuitive API design -- Stronger type safety and better error messages -- Improved ARC-56 compatibility -- Feature parity with `algokit-utils-ts` >= `v7` interfaces - -The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. - -The old version (in `algokit_utils._legacy_v2`) will still work until at least v4 (we have maintained backwards compatibility), but it exposes an older, function-based interface that is deprecated. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. - -## Migration Steps - -### Prerequisites - -If you have previously relied on `beta` versions of ` - -### Step 1 - Replace SDK Clients with AlgorandClient - -First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: - -```python -"""Before""" -import algokit_utils -algod = algokit_utils.get_algod_client() -indexer = algokit_utils.get_indexer_client() - -"""After""" -from algokit_utils import AlgorandClient -algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. -``` - -During migration, you can still access SDK clients if needed: - -```python -algod = algorand.client.algod -indexer = algorand.client.indexer -kmd = algorand.client.kmd -``` - -### Step 2 - Update Account Management - -Account management has moved to `algorand.account`: - -```python -"""Before""" -account = algokit_utils.get_account_from_mnemonic( - mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), -) -dispenser = algokit_utils.get_dispenser_account(algod) - -"""After""" -account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) -dispenser = algorand.account.dispenser_from_environment() -``` - -Key changes: - -- `get_account` → `account.from_environment` -- `get_account_from_mnemonic` → `account.from_mnemonic` -- `get_dispenser_account` → `account.dispenser_from_environment` -- `get_localnet_default_account` → `account.localnet_dispenser` - -### Step 3 - Update Transaction Management - -Transaction creation and sending is now more structured: - -```python -"""Before""" -result = algokit_utils.transfer_algos( - from_account=account, - to_addr="RECEIVER", - amount=algokit_utils.algos(1), - algod_client=algod, -) - -"""After""" -result = algorand.send.payment( - sender=account.address, - receiver="RECEIVER", - amount=(1).algo(), -) - -# For transaction groups -"""Before""" -atc = AtomicTransactionComposer() -# ... add transactions ... -result = algokit_utils.execute_atc_with_logic_error(atc, algod) - -"""After""" -composer = algorand.new_group() -# ... add transactions ... -result = composer.send() -``` - -Key changes: - -- `transfer_algos` → `send.payment` -- `transfer_asset` → `send.asset_transfer` -- `execute_atc_with_logic_error` → `composer.send()` -- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) -- Amount handling uses extension methods (e.g., `(1).algo()` instead of `algos(1)`) - -### Step 4 - Update Application Client Usage - -The application client has been split into `AppClient` and `AppFactory`: - -```python -"""Before""" -app_client = ApplicationClient( - algod_client=algod, - app_spec=app_spec, - app_id=existing_app_id, -) - -"""After""" -# For existing apps -app_client = AppClient( - app_id=existing_app_id, - app_spec=app_spec, - algorand=algorand, -) - -# For creating/deploying apps -app_factory = algorand.get_app_factory( - app_spec=app_spec, - app_name="MyApp", -) -``` - -Key changes in method calls: - -```python -"""Before""" -result = app_client.call( - method="hello", - method_args=["World"], - boxes=[("name", "box1")], -) - -"""After""" -result = app_client.send.call( - app_client.params.call( - method="hello", - args=["World"], - box_references=[("name", "box1")], - ) -) -``` - -Notable changes: - -- Split between `AppClient` (for existing apps) and `AppFactory` (for creation/deployment) -- More structured transaction building with `.params`, `.create_transaction`, and `.send` -- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) -- Better ARC-56 support for state management -- Improved error handling and debugging support - -### Step 5 - Update State Management - -State management is now more structured and type-safe: - -```python -"""Before""" -global_state = app_client.get_global_state() -local_state = app_client.get_local_state(account_address) -box_value = app_client.get_box_value("box_name") - -"""After""" -# Global state -global_state = app_client.state.global_state.get_all() -value = app_client.state.global_state.get_value("key_name") -map_value = app_client.state.global_state.get_map_value("map_name", "key") - -# Local state -local_state = app_client.state.local_state(account_address).get_all() -value = app_client.state.local_state(account_address).get_value("key_name") -map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") - -# Box storage -box_value = app_client.state.box.get_value("box_name") -boxes = app_client.state.box.get_all() -map_value = app_client.state.box.get_map_value("map_name", "key") -``` - -### Step 6 - Update Asset Management - -Asset management is now more consistent: - -```python -"""Before""" -result = algokit_utils.opt_in(algod, account, [asset_id]) - -"""After""" -result = algorand.send.asset_opt_in( - sender=account.address, - asset_id=asset_id, -) -``` - -## Breaking Changes - -1. **Client Management** - - - Removal of standalone client creation functions - - All clients now accessed through `AlgorandClient` - -2. **Account Management** - - - Account creation functions moved to `AccountManager` - - Changed parameter names for consistency - - Improved typing for account operations - -3. **Transaction Management** - - - Restructured transaction creation and sending - - Removed `skip_sending` parameter (use `create_transaction` instead) - - Changed parameter names for consistency - - New transaction composition interface - -4. **Application Client** - - - Split into `AppClient` and `AppFactory` - - New structured interface for transactions - - Changed parameter names for consistency - - Improved ARC-56 support - -5. **State Management** - - - New hierarchical state access - - Improved typing for state values - - Better support for ARC-56 state schemas - -6. **Asset Management** - - Moved to consistent transaction interface - - Changed parameter names for consistency - -## Best Practices - -1. Use the new `AlgorandClient` as the main entry point -2. Leverage IDE autocompletion to discover available functionality -3. Use the new parameter builders for type-safe transaction creation -4. Use the state accessor patterns for cleaner state management -5. Use transaction composition for atomic operations -6. Use source maps and debug mode for development -7. Use idempotent deployment patterns with versioning -8. Properly manage box references to avoid transaction failures diff --git a/docs/source/v3-migration-guide.md b/docs/source/v3-migration-guide.md new file mode 100644 index 00000000..9d64cc16 --- /dev/null +++ b/docs/source/v3-migration-guide.md @@ -0,0 +1,314 @@ +# Migration Guide - v3 + +Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: + +- Easier and simpler consumption experience guided by IDE autocompletion +- Less redundant parameter passing (e.g., `algod` client) +- Better performance through caching of commonly retrieved values like transaction parameters +- More consistent and intuitive API design +- Stronger type safety and better error messages +- Improved ARC-56 compatibility +- Feature parity with `algokit-utils-ts` >= `v7` interfaces + +The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. + +The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, _all v2 abstractions are available_ with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. + +> BREAKING CHANGE: the `beta` module is now removed, any imports from `algokit_utils.beta` will now raise an error with a link to a new expected import path. This is due to the fact that the interfaces introduced in `beta` are now refined and available in the main module. + +## Migration Steps + +In general, your codebase might fall into one of the following migration scenarios: + +- Using `algokit-utils-py` v2.x only without use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x only and with use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x with `algokit-client-generator-py` v1.x +- Using `algokit-client-generator-py` v1.x only (implies implicit dependency on `algokit-utils-py` v2.x) + +Given that `algokit-utils-py` v3.x is backwards compatible with `algokit-client-generator-py` v1.x, the following general guidelines are applicable to all scenarios (note that the order of operations is important to ensure straight-forward migration): + +1. Upgrade to `algokit-utils-py` v3.x + - 1.1 (If used) Update imports from `algokit_utils.beta` to `algokit_utils` + - 1.2 Follow hints in deprecation warnings to update your codebase to rely on latest v3 interfaces +2. Upgrade to `algokit-client-generator-py` v2.x and regenerate typed clients + - 2.1 Follow `algokit-client-generator-py` [v2.x migration guide](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/v2-migration-guide.md) + +The remaining set of guidelines are outlining migrations for specific abstractions that had direct equivalents in `algokit-utils-py` v2.x. + +### Prerequisites + +It is important to reiterate that if you have previously relied on `beta` versions of `algokit-utils-py` v2.x, you will need to update your imports to rely on the new interfaces. Errors thrown during import from `beta` will provide a description of the new expected import path. + +> As with `v2.x` all public abstractions in `algokit_utils` are available for direct imports `from algokit_utils import ...`, however underlying modules have been refined to be structured loosely around common AVM domains such as `applications`, `transactions`, `accounts`, `assets`, etc. See [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) for latest and detailed overview. + +### Step 1 - Replace SDK Clients with AlgorandClient + +First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: + +```python +"""Before""" +import algokit_utils +algod = algokit_utils.get_algod_client() +indexer = algokit_utils.get_indexer_client() + +"""After""" +from algokit_utils import AlgorandClient +algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. +``` + +During migration, you can still access SDK clients if needed: + +```python +algod = algorand.client.algod +indexer = algorand.client.indexer +kmd = algorand.client.kmd +``` + +### Step 2 - Update Account Management + +Account management has moved to `algorand.account`: + +#### Before: + +```python +account = algokit_utils.get_account_from_mnemonic( + mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), +) +dispenser = algokit_utils.get_dispenser_account(algod) +``` + +#### After: + +```python +account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) +dispenser = algorand.account.dispenser_from_environment() +``` + +Key changes: + +- `get_account` → `account.from_environment` +- `get_account_from_mnemonic` → `account.from_mnemonic` +- `get_dispenser_account` → `account.dispenser_from_environment` +- `get_localnet_default_account` → `account.localnet_dispenser` + +### Step 3 - Update Transaction Management + +Transaction creation and sending is now more structured: + +#### Before: + +```python +# Single transaction +result = algokit_utils.transfer_algos( + from_account=account, + to_addr="RECEIVER", + amount=algokit_utils.algos(1), + algod_client=algod, +) + +# Transaction groups +atc = AtomicTransactionComposer() +# ... add transactions ... +result = algokit_utils.execute_atc_with_logic_error(atc, algod) +``` + +#### After: + +```python +# Single transaction +result = algorand.send.payment( + sender=account.address, + receiver="RECEIVER", + amount=AlgoAmount.from_algo(1), +) + +# Transaction groups +composer = algorand.new_group() +# ... add transactions ... +result = composer.send() +``` + +Key changes: + +- `transfer_algos` → `algorand.send.payment` +- `transfer_asset` → `algorand.send.asset_transfer` +- `execute_atc_with_logic_error` → `composer.send()` +- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) +- Improved amount handling with dedicated `AlgoAmount` class (e.g., `AlgoAmount.from_algo(1)`) + +### Step 4 - Update `ApplicationSpecification` usage + +`ApplicationSpecification` abstraction is largely identical to v2, however it's been renamed to `Arc32Contract` to better reflect the fact that it's a contract specification for a specific ARC and addition of `Arc56Contract` supporting the latest recommended conventions. Hence the main actionable change is to update your import to `from algokit_utils import Arc32Contract` and rename `ApplicationSpecification` to `Arc32Contract`. + +You can instantiate an `Arc56Contract` instance from an `Arc32Contract` instance using the `Arc56Contract.from_arc32` method. For instance: + +```python +testing_app_arc32_app_spec = Arc32Contract.from_json(app_spec_json) +arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) +``` + +> Despite auto conversion of ARC-32 to ARC-56, we recommend recompiling your contract to a fully compliant ARC-56 specification given that auto conversion would skip populating information that can't be parsed from raw ARC-32. + +### Step 5 - Update `ApplicationClient` usage + +The application client has been in v2 has been responsible for instantiation, deployment and calling of the application. In v3, this has been split into `AppClient`, `AppDeployer` and `AppFactory` to better reflect the different responsibilities: + +```python +"""Before (v2 deployment)""" +from algokit_utils import ApplicationClient, OnUpdate, OnSchemaBreak + +# Initialize client with manual configuration +app_client = ApplicationClient( + algod_client=algod, + app_spec=app_spec, + creator=creator, + app_name="MyApp" +) + +# Deployment with versioning and update policies +deploy_result = app_client.deploy( + version="1.0", + allow_update=True, + allow_delete=False, + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail +) + +# Post-deployment calls +response = app_client.call("initialize", args=["config"]) + + +"""After (v3 factory-based deployment)""" +from algokit_utils import AppFactory, OnUpdate, OnSchemaBreak + +# Factory-based deployment with compiled parameters +app_factory = AppFactory( + AppFactoryParams( + algorand=algorand, + app_spec=app_spec, + app_name="MyApp", + compilation_params=AppClientCompilationParams( + deploy_time_params={"VERSION": 1}, + updatable=True, # Replaces allow_update + deletable=False # Replaces allow_delete + ) + ) +) + +app_client, deploy_result = app_factory.deploy( + version="1.0", + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail, +) # Returns a tuple of (app_client, deploy_result) + +# Type-safe post-deployment calls +response = app_client.send.call("setup", args=[{"max_users": 100}]) +``` + +Notable changes: + +- Split between `AppClient`, `AppDeployer` (for raw creation/deployment) and `AppFactory` (for creation/deployment using factory patterns). In majority of cases, you will only need `AppFactory` as it provides convenience methods for instantiation of `AppClient` and mediates calls to `AppDeployer`. +- More structured transaction building with `.params`, `.create_transaction`, and `.send` +- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) +- ARC-56 support for state management +- Improved error handling and debugging support + +### Step 6 - Update `AppClient` State Management + +State management is now more structured and type-safe: + +```python +"""Before""" +global_state = app_client.get_global_state() +local_state = app_client.get_local_state(account_address) +box_value = app_client.get_box_value("box_name") + +"""After""" +# Global state +global_state = app_client.state.global_state.get_all() +value = app_client.state.global_state.get_value("key_name") +map_value = app_client.state.global_state.get_map_value("map_name", "key") + +# Local state +local_state = app_client.state.local_state(account_address).get_all() +value = app_client.state.local_state(account_address).get_value("key_name") +map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") + +# Box storage +box_value = app_client.state.box.get_value("box_name") +boxes = app_client.state.box.get_all() +map_value = app_client.state.box.get_map_value("map_name", "key") +``` + +### Step 7 - Update Asset Management + +Asset management is now more consistent: + +```python +"""Before""" +result = algokit_utils.opt_in(algod, account, [asset_id]) + +"""After""" +result = algorand.send.asset_opt_in( + params=AssetOptInParams( + sender=account.address, + asset_id=asset_id, + ) +) +``` + +## Breaking Changes + +1. **Client Management** + + - Removal of standalone client creation functions + - All clients now accessed through `AlgorandClient` + +2. **Account Management** + + - Account creation functions moved to `AccountManager` accessible via `algorand.account` property + - Unified `TransactionSignerAccountProtocol` with compliant and typed `SigningAccount`, `TransactionSignerAccount`, `LogicSigAccount`, `MultiSigAccount` classes encapsulating low level `algosdk` abstractions. + - Improved typing for account operations, such as obtaining account information from `algod`, returning a typed information object. + +3. **Transaction Management** + + - Consistent and intuitive transaction creation and sending interface accessible via `algorand.{send|params|create_transaction}` properties + - New transaction composition interface accessible via `algorand.new_group` + - Removing necessity to interact with low level and untyped `algosdk` abstractions for assembling, signing and sending transaction(s). + +4. **Application Client** + + - Split into `AppClient`, `AppDeployer` and `AppFactory` + - New intuitive structured interface for creating or sending `AppCall`|`AppMethodCall` transactions + - ARC-56 support along with automatic conversion of specs from ARC-32 to ARC-56 + +5. **State Management** + + - New hierarchical state access available via `app_client.state.{global_state|local_state|box}` properties + - Improved typing for state values + - Support for ARC-56 state schemas + +6. **Asset Management** + - Dedicated `AssetManager` class for asset management accessible via `algorand.asset` property + - Improved typing for asset operations, such as obtaining asset information from `algod`, returning a typed information object. + - Consistent interface for asset opt-in, transfer, freeze, etc. + +## Best Practices + +1. Use the new `AlgorandClient` as the main entry point +2. Leverage IDE autocompletion to discover available functionality, consult with [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) when unsure +3. Use the transaction parameter builders for type-safe transaction creation (`algorand.params.{}`) +4. Use the state accessor patterns for cleaner state management {`algorand.state.{}`} +5. Use high level `TransactionComposer` interface over low level `algosdk` abstractions (where possible) +6. Use source maps and debug mode to quickly troubleshoot on-chain errors +7. Use idempotent deployment patterns with versioning + +## Troubleshooting + +### A v2 interface/method/class does not display a deprecation warning correctly or at all + +Submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a description of the problem and the code that is causing it. + +### Useful scenario of converting v2 to v3 not covered in generic migration guide + +If you have a scenario that you think is useful and not covered in the generic migration guide, please submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a scenario. diff --git a/poetry.lock b/poetry.lock index 4fc38827..88a18c0d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "alabaster" -version = "0.7.16" +version = "1.0.0" description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] [[package]] @@ -91,6 +91,27 @@ algokit-utils = ">=2.0.0,<3.0.0" py-algorand-sdk = ">=2.0.0" pyteal = ">=0.24,<0.25" +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "boolean-py" version = "4.0" @@ -558,13 +579,13 @@ files = [ [[package]] name = "docutils" -version = "0.18.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.9" files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -636,6 +657,23 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "furo" +version = "2024.8.6" +description = "A clean customisable Sphinx documentation theme." +optional = false +python-versions = ">=3.8" +files = [ + {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, + {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +pygments = ">=2.7" +sphinx = ">=6.0,<9.0" +sphinx-basic-ng = ">=1.0.0.beta2" + [[package]] name = "gitdb" version = "4.0.12" @@ -988,13 +1026,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] @@ -1007,7 +1045,7 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -1082,21 +1120,21 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.5" +version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, ] [package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" +markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -1266,29 +1304,29 @@ files = [ [[package]] name = "myst-parser" -version = "1.0.0" +version = "4.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "myst-parser-1.0.0.tar.gz", hash = "sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae"}, - {file = "myst_parser-1.0.0-py3-none-any.whl", hash = "sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c"}, + {file = "myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d"}, + {file = "myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531"}, ] [package.dependencies] -docutils = ">=0.15,<0.20" +docutils = ">=0.19,<0.22" jinja2 = "*" -markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.4,<0.4.0" +markdown-it-py = ">=3.0,<4.0" +mdit-py-plugins = ">=0.4.1,<1.0" pyyaml = "*" -sphinx = ">=5,<7" +sphinx = ">=7,<9" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.7.5,<0.8.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] +linkify = ["linkify-it-py (>=2.0,<3.0)"] +rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "nh3" @@ -1952,17 +1990,17 @@ files = [ [[package]] name = "readme-renderer" -version = "43.0" +version = "44.0" description = "readme_renderer is a library for rendering readme descriptions for Warehouse" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"}, - {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"}, + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, ] [package.dependencies] -docutils = ">=0.13.1" +docutils = ">=0.21.2" nh3 = ">=0.2.14" Pygments = ">=2.5.1" @@ -2180,79 +2218,110 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "sphinx" -version = "6.2.1" +version = "8.1.3" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"}, - {file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"}, + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.20" +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" imagesize = ">=1.3" -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.13" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] -name = "sphinx-autodoc2" -version = "0.5.0" -description = "Analyse a python project and create documentation for it." +name = "sphinx-autoapi" +version = "3.4.0" +description = "Sphinx API documentation generator" optional = false python-versions = ">=3.8" files = [ - {file = "sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e"}, - {file = "sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a"}, + {file = "sphinx_autoapi-3.4.0-py3-none-any.whl", hash = "sha256:4027fef2875a22c5f2a57107c71641d82f6166bf55beb407a47aaf3ef14e7b92"}, + {file = "sphinx_autoapi-3.4.0.tar.gz", hash = "sha256:e6d5371f9411bbb9fca358c00a9e57aef3ac94cbfc5df4bab285946462f69e0c"}, +] + +[package.dependencies] +astroid = [ + {version = ">=2.7", markers = "python_version < \"3.12\""}, + {version = ">=3.0.0a1", markers = "python_version >= \"3.12\""}, +] +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=6.1.0" + +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, + {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, ] [package.dependencies] -astroid = ">=2.7,<4" -tomli = {version = "*", markers = "python_version < \"3.11\""} -typing-extensions = "*" +colorama = ">=0.4.6" +sphinx = "*" +starlette = ">=0.35" +uvicorn = ">=0.25" +watchfiles = ">=0.20" +websockets = ">=11" [package.extras] -cli = ["typer[all]"] -docs = ["furo", "myst-parser", "sphinx (>=4.0.0)"] -sphinx = ["sphinx (>=4.0.0)"] -testing = ["pytest", "pytest-cov", "pytest-regressions", "sphinx (>=4.0.0,<7)"] +test = ["httpx", "pytest (>=6)"] [[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." +name = "sphinx-basic-ng" +version = "1.0.0b2" +description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, + {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, + {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, ] [package.dependencies] -sphinx = ">=1.8" +sphinx = ">=4.0" [package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] +docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] [[package]] name = "sphinx-markdown-builder" @@ -2273,25 +2342,6 @@ tabulate = "*" [package.extras] dev = ["black", "bumpver", "coveralls", "flake8", "isort", "pip-tools", "pylint", "pytest", "pytest-cov", "sphinx (>=5.3.0)", "sphinxcontrib-plantuml", "sphinxcontrib.httpdomain"] -[[package]] -name = "sphinx-rtd-theme" -version = "1.3.0" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, - {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, -] - -[package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<8" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] - [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" @@ -2340,20 +2390,6 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -2400,6 +2436,23 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] +[[package]] +name = "starlette" +version = "0.45.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tabulate" version = "0.9.0" @@ -2588,6 +2641,25 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "virtualenv" version = "20.29.1" @@ -2608,6 +2680,89 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchfiles" +version = "1.0.4" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, + {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, + {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, + {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, + {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, + {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, + {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, + {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + [[package]] name = "webencodings" version = "0.5.1" @@ -2619,6 +2774,84 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "websockets" +version = "14.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, + {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, + {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, + {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, + {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, + {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, + {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, + {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, + {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, + {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, + {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, + {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, + {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, + {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, + {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, + {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, + {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, + {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, + {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, + {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, +] + [[package]] name = "wheel" version = "0.45.1" @@ -2655,4 +2888,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "55003b10efa72f9205b31cf879b74048b695d8d58edad56d19b6057018c9211e" +content-hash = "c831facca8536a1b7d018cd049682ee762ee374ed77ddec03c9a0acd62086b19" diff --git a/pyproject.toml b/pyproject.toml index 33a7517f..278a5a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,21 +22,21 @@ python-semantic-release = "^7.34.3" pytest-cov = "^6" pre-commit = "^3.4.0" python-dotenv = "^1.0.0" -sphinx = "^6.1.3" -myst-parser = "^1.0.0" -sphinx-copybutton = "^0.5.1" -sphinx-rtd-theme = "^1.2.0" -sphinx-autodoc2 = ">=0.4.2,<0.6.0" +sphinx = "^8.0.0" poethepoet = ">=0.19,<0.26" beaker-pyteal = "^1.1.1" pytest-httpx = "^0.35" pytest-xdist = "^3.6.1" -sphinx-markdown-builder = "^0.6.6" linkify-it-py = "^2.0.3" setuptools = "^75.2.0" pydoclint = "^0.6.0" pytest-sugar = "^1.0.0" types-deprecated = "^1.2.15.20241117" +sphinx-autobuild = "^2024.10.3" +furo = "^2024.8.6" +myst-parser = "^4.0.0" +sphinx-autoapi = "^3.4.0" +sphinx-markdown-builder = "^0.6.8" [build-system] requires = ["poetry-core"] @@ -138,6 +138,7 @@ docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" docstrings-check = "pydoclint src --style sphinx --arg-type-hints-in-docstring false --check-return-types false --exclude src/algokit_utils/_legacy_v2" +docs-dev = "sphinx-autobuild --ignore '**/_build/**' --ignore '**/autoapi/**' --ignore '**/.doctrees/**' docs/source docs/_build" [tool.pytest.ini_options] pythonpath = ["src", "tests"] diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 127d8551..45a04f3f 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -222,7 +222,7 @@ def set_signer_from_account(self, account: TransactionSignerAccountProtocol) -> :example: >>> account_manager = AccountManager(client_manager) - >>> account_manager.set_signer_from_account(Account.new_account()) + >>> account_manager.set_signer_from_account(SigningAccount.new_account()) >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) """ diff --git a/src/algokit_utils/beta/__init__.py b/src/algokit_utils/beta/__init__.py deleted file mode 100644 index 7e7b6ceb..00000000 --- a/src/algokit_utils/beta/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Any, NoReturn - - -def _deprecated_import_error(old_path: str, new_path: str) -> NoReturn: - """Helper to create consistent deprecation error messages""" - raise ImportError( - f"The module '{old_path}' has been moved in v3. " - f"Please update your imports to use '{new_path}' instead. " - "See the migration guide for more details: " - "https://github.com/algorandfoundation/algokit-utils-py/blob/prerelease/ts-feature-parity/docs/migration-guide.md" - ) - - -class AlgorandClient: - """@deprecated Use algokit_utils.clients.AlgorandClient instead""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - _deprecated_import_error("algokit_utils.beta.AlgorandClient", "algokit_utils.AlgorandClient") - - -class AlgokitComposer: - """@deprecated Use algokit_utils.transactions.TransactionComposer instead""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - _deprecated_import_error("algokit_utils.beta.AlgokitComposer", "algokit_utils.TransactionComposer") - - -class AccountManager: - """@deprecated Use algokit_utils.accounts.AccountManager instead""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - _deprecated_import_error("algokit_utils.beta.AccountManager", "algokit_utils.AccountManager") - - -class ClientManager: - """@deprecated Use algokit_utils.clients.ClientManager instead""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - _deprecated_import_error("algokit_utils.beta.ClientManager", "algokit_utils.ClientManager") - - -# Re-export all the parameter classes with deprecation warnings -def __getattr__(name: str) -> Any: # noqa: ANN401 - """Handle deprecated imports of parameter classes""" - - param_mappings = { - # Transaction params - "PayParams": "algokit_utils.transactions.PaymentParams", - "AssetCreateParams": "algokit_utils.transactions.AssetCreateParams", - "AssetConfigParams": "algokit_utils.transactions.AssetConfigParams", - "AssetFreezeParams": "algokit_utils.transactions.AssetFreezeParams", - "AssetDestroyParams": "algokit_utils.transactions.AssetDestroyParams", - "AssetTransferParams": "algokit_utils.transactions.AssetTransferParams", - "AssetOptInParams": "algokit_utils.transactions.AssetOptInParams", - "AppCallParams": "algokit_utils.transactions.AppCallParams", - "MethodCallParams": "algokit_utils.transactions.MethodCallParams", - "OnlineKeyRegParams": "algokit_utils.transactions.OnlineKeyRegistrationParams", - } - - if name in param_mappings: - _deprecated_import_error(f"algokit_utils.beta.{name}", param_mappings[name]) - - raise AttributeError(f"module 'algokit_utils.beta' has no attribute '{name}'") - - -# Clean up namespace to only show intended exports -__all__ = [ - "AccountManager", - "AlgokitComposer", - "AlgorandClient", - "ClientManager", -] diff --git a/src/algokit_utils/beta/_utils.py b/src/algokit_utils/beta/_utils.py new file mode 100644 index 00000000..f28f96a3 --- /dev/null +++ b/src/algokit_utils/beta/_utils.py @@ -0,0 +1,36 @@ +from typing import NoReturn + + +def deprecated_import_error(old_path: str, new_path: str) -> NoReturn: + """Helper to create consistent deprecation error messages""" + raise ImportError( + f"WARNING: The module '{old_path}' has been removed in algokit-utils v3. " + f"Please update your imports to use '{new_path}' instead. " + "See the migration guide for more details: " + "https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/v3-migration-guide.md" + ) + + +def handle_getattr(name: str) -> NoReturn: + param_mappings = { + "ClientManager": "algokit_utils.ClientManager", + "AlgorandClient": "algokit_utils.AlgorandClient", + "AlgoSdkClients": "algokit_utils.AlgoSdkClients", + "AccountManager": "algokit_utils.AccountManager", + "PayParams": "algokit_utils.transactions.PaymentParams", + "AlgokitComposer": "algokit_utils.TransactionComposer", + "AssetCreateParams": "algokit_utils.transactions.AssetCreateParams", + "AssetConfigParams": "algokit_utils.transactions.AssetConfigParams", + "AssetFreezeParams": "algokit_utils.transactions.AssetFreezeParams", + "AssetDestroyParams": "algokit_utils.transactions.AssetDestroyParams", + "AssetTransferParams": "algokit_utils.transactions.AssetTransferParams", + "AssetOptInParams": "algokit_utils.transactions.AssetOptInParams", + "AppCallParams": "algokit_utils.transactions.AppCallParams", + "MethodCallParams": "algokit_utils.transactions.MethodCallParams", + "OnlineKeyRegParams": "algokit_utils.transactions.OnlineKeyRegistrationParams", + } + + if name in param_mappings: + deprecated_import_error(f"algokit_utils.beta.{name}", param_mappings[name]) + + raise AttributeError(f"module 'algokit_utils.beta' has no attribute '{name}'") diff --git a/src/algokit_utils/beta/account_manager.py b/src/algokit_utils/beta/account_manager.py new file mode 100644 index 00000000..90835e43 --- /dev/null +++ b/src/algokit_utils/beta/account_manager.py @@ -0,0 +1,9 @@ +from typing import Any + +from algokit_utils.beta._utils import handle_getattr + + +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" + + handle_getattr(name) diff --git a/src/algokit_utils/beta/algorand_client.py b/src/algokit_utils/beta/algorand_client.py new file mode 100644 index 00000000..90835e43 --- /dev/null +++ b/src/algokit_utils/beta/algorand_client.py @@ -0,0 +1,9 @@ +from typing import Any + +from algokit_utils.beta._utils import handle_getattr + + +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" + + handle_getattr(name) diff --git a/src/algokit_utils/beta/client_manager.py b/src/algokit_utils/beta/client_manager.py new file mode 100644 index 00000000..90835e43 --- /dev/null +++ b/src/algokit_utils/beta/client_manager.py @@ -0,0 +1,9 @@ +from typing import Any + +from algokit_utils.beta._utils import handle_getattr + + +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" + + handle_getattr(name) diff --git a/src/algokit_utils/beta/composer.py b/src/algokit_utils/beta/composer.py new file mode 100644 index 00000000..90835e43 --- /dev/null +++ b/src/algokit_utils/beta/composer.py @@ -0,0 +1,9 @@ +from typing import Any + +from algokit_utils.beta._utils import handle_getattr + + +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" + + handle_getattr(name) diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index a6ef874e..daa88cb6 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -6,7 +6,13 @@ from algosdk.transaction import LogicSigAccount as AlgosdkLogicSigAccount from algosdk.transaction import Multisig, MultisigTransaction -__all__ = ["DISPENSER_ACCOUNT_NAME", "MultiSigAccount", "MultisigMetadata", "SigningAccount"] +__all__ = [ + "DISPENSER_ACCOUNT_NAME", + "MultiSigAccount", + "MultisigMetadata", + "SigningAccount", + "TransactionSignerAccount", +] DISPENSER_ACCOUNT_NAME = "DISPENSER" From c14941b4c34671d5aede5512427190b611871741 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 29 Jan 2025 18:17:59 +0100 Subject: [PATCH 30/31] docs: add migration note to readme --- README.md | 10 ++++-- .../applications/app_factory/index.md | 34 +++---------------- docs/markdown/v3-migration-guide.md | 2 +- 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b6e83f2e..820cfac7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. > **Note** @@ -19,13 +19,17 @@ This library can be installed using pip, e.g.: pip install algokit-utils ``` +## Migration from `v2.x` to `v3.x` + +Refer to the [v3 migration guide](./docs/source/v3-migration-guide.md) for more information on how to migrate to latest version of `algokit-utils-py`. + ## Guiding principles This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles). ## Contributing -This is an open source project managed by the Algorand Foundation. +This is an open source project managed by the Algorand Foundation. See the [AlgoKit contributing page](https://github.com/algorandfoundation/algokit-cli/blob/main/CONTRIBUTING.MD) to learn about making improvements. To successfully run the tests in this repository you need to be running LocalNet via [AlgoKit](https://github.com/algorandfoundation/algokit-cli): diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md index e0002b87..57bbc718 100644 --- a/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md +++ b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md @@ -4,12 +4,12 @@ | [`AppFactoryParams`](#algokit_utils.applications.app_factory.AppFactoryParams) | | |--------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------| -| [`AppFactoryCreateParams`](#algokit_utils.applications.app_factory.AppFactoryCreateParams) | Schema for application creation. | -| [`AppFactoryCreateMethodCallParams`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams) | Schema for application creation. | +| [`AppFactoryCreateParams`](#algokit_utils.applications.app_factory.AppFactoryCreateParams) | | +| [`AppFactoryCreateMethodCallParams`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams) | | | [`AppFactoryCreateMethodCallResult`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult) | Base class for transaction results. | -| [`SendAppFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | Result of an application transaction. | -| [`SendAppUpdateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | Result of updating an application. | -| [`SendAppCreateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | Result of creating a new application. | +| [`SendAppFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | | +| [`SendAppUpdateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | | +| [`SendAppCreateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | | | [`AppFactoryDeployResult`](#algokit_utils.applications.app_factory.AppFactoryDeployResult) | Result from deploying an application via AppFactory | | [`AppFactory`](#algokit_utils.applications.app_factory.AppFactory) | | @@ -35,22 +35,10 @@ Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientBareCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) -Schema for application creation. - -* **Variables:** - * **extra_program_pages** – Optional number of extra program pages - * **schema** – Optional application creation schema - ### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientMethodCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) -Schema for application creation. - -* **Variables:** - * **extra_program_pages** – Optional number of extra program pages - * **schema** – Optional application creation schema - ### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult Bases: [`algokit_utils.transactions.transaction_sender.SendSingleTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `Generic`[`ABIReturnT`] @@ -73,26 +61,14 @@ Represents the result of sending a single transaction. Bases: [`algokit_utils.transactions.transaction_sender.SendAppTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] -Result of an application transaction. - -Contains the ABI return value if applicable. - ### *class* algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult Bases: [`algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] -Result of updating an application. - -Contains the compiled approval and clear programs. - ### *class* algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult Bases: [`algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] -Result of creating a new application. - -Contains the app ID and address of the newly created application. - ### *class* algokit_utils.applications.app_factory.AppFactoryDeployResult Result from deploying an application via AppFactory diff --git a/docs/markdown/v3-migration-guide.md b/docs/markdown/v3-migration-guide.md index 95a4ba28..710d6850 100644 --- a/docs/markdown/v3-migration-guide.md +++ b/docs/markdown/v3-migration-guide.md @@ -12,7 +12,7 @@ Version 3 of `algokit-utils-ts` moved from a stateless function-based interface The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. -The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, _all v2 abstractions are available_ with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. +The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, *all v2 abstractions are available* with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. > BREAKING CHANGE: the `beta` module is now removed, any imports from `algokit_utils.beta` will now raise an error with a link to a new expected import path. This is due to the fact that the interfaces introduced in `beta` are now refined and available in the main module. From 9af713d685fa381e9367ba88d87851f5eff88e37 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 29 Jan 2025 18:45:46 +0100 Subject: [PATCH 31/31] docs: addressing pr comments --- .github/workflows/check-python.yaml | 9 +++++---- docs/markdown/capabilities/account.md | 4 ++-- docs/markdown/capabilities/algorand-client.md | 4 ++-- docs/markdown/capabilities/app-client.md | 2 +- docs/markdown/capabilities/app-deploy.md | 4 ++-- docs/markdown/capabilities/transaction-composer.md | 4 ++-- docs/source/capabilities/account.md | 4 ++-- docs/source/capabilities/algorand-client.md | 4 ++-- docs/source/capabilities/app-client.md | 2 +- docs/source/capabilities/app-deploy.md | 4 ++-- docs/source/capabilities/transaction-composer.md | 4 ++-- 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index 0a473b53..1e9256f4 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -45,7 +45,8 @@ jobs: - name: Check types with mypy run: poetry run mypy - - name: Check docs are up to date - run: | - poetry run poe docs-md-only - git diff --exit-code ':!docs/markdown/autoapi/index.md' docs + # TODO: Restore before prod release of v3 + # - name: Check docs are up to date + # run: | + # poetry run poe docs-md-only + # git diff --exit-code ':!docs/markdown/autoapi/index.md' ':!docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md' docs diff --git a/docs/markdown/capabilities/account.md b/docs/markdown/capabilities/account.md index a08b215b..cfbc2c71 100644 --- a/docs/markdown/capabilities/account.md +++ b/docs/markdown/capabilities/account.md @@ -188,7 +188,7 @@ default_dispenser_account = kmd_account_manager.get_wallet_account( lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 ) # Same as above, but dedicated method call for convenience -local_net_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() # Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD # if creating it then fund it with 2 ALGO from the default dispenser account new_account = kmd_account_manager.get_or_create_wallet_account( @@ -205,7 +205,7 @@ Some of this functionality is directly exposed from [`AccountManager`](), which ```python # Get and register LocalNet dispenser -local_net_dispenser = algorand.account.localnet_dispenser() +localnet_dispenser = algorand.account.localnet_dispenser() # Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD dispenser = algorand.account.dispenser_from_environment() # Get / create and register account from KMD idempotently by name diff --git a/docs/markdown/capabilities/algorand-client.md b/docs/markdown/capabilities/algorand-client.md index a94de631..9404653e 100644 --- a/docs/markdown/capabilities/algorand-client.md +++ b/docs/markdown/capabilities/algorand-client.md @@ -177,7 +177,7 @@ All transaction parameters share the following common base parameters: - Round validity management - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. - - `last_valid_round: bigint | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. + - `last_valid_round: int | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. @@ -185,7 +185,7 @@ Then on top of that the base type gets extended for the specific type of transac AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: -- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to 10, except in [automated testing](testing.md) where it’s set to 1000 when targeting LocalNet. +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it’s set to `1000`. - `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) - `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) - `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/markdown/capabilities/app-client.md b/docs/markdown/capabilities/app-client.md index c69ce15b..5953e5d2 100644 --- a/docs/markdown/capabilities/app-client.md +++ b/docs/markdown/capabilities/app-client.md @@ -158,7 +158,7 @@ result, app_client = factory.send.create( ## Updating and deleting an app -Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app created via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. ## Calling the app diff --git a/docs/markdown/capabilities/app-deploy.md b/docs/markdown/capabilities/app-deploy.md index 1db5a88c..ff7cb202 100644 --- a/docs/markdown/capabilities/app-deploy.md +++ b/docs/markdown/capabilities/app-deploy.md @@ -33,7 +33,7 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md), [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150) and [ARC-4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) and [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance - There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) - Contracts are resolvable by a string “name” for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead @@ -175,7 +175,7 @@ When compiling TEAL template code, the capabilities described in the [above desi In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which will be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn’t (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn’t (permanent) diff --git a/docs/markdown/capabilities/transaction-composer.md b/docs/markdown/capabilities/transaction-composer.md index c859f81f..330b757b 100644 --- a/docs/markdown/capabilities/transaction-composer.md +++ b/docs/markdown/capabilities/transaction-composer.md @@ -191,7 +191,7 @@ result = ( args=[1, 2, 3] # Resources will be automatically populated! )) - .send(send_params=SendParams(populate_app_call_resources=True)) + .send(params=SendParams(populate_app_call_resources=True)) ) # Or disable automatic population @@ -208,7 +208,7 @@ result = ( asset_references=[789], box_references=[box_reference] )) - .send(send_params=SendParams(populate_app_call_resources=False)) + .send(params=SendParams(populate_app_call_resources=False)) ) ``` diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index 8db435d9..25d87437 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -188,7 +188,7 @@ default_dispenser_account = kmd_account_manager.get_wallet_account( lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 ) # Same as above, but dedicated method call for convenience -local_net_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() # Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD # if creating it then fund it with 2 ALGO from the default dispenser account new_account = kmd_account_manager.get_or_create_wallet_account( @@ -205,7 +205,7 @@ Some of this functionality is directly exposed from [`AccountManager`](#accountm ```python # Get and register LocalNet dispenser -local_net_dispenser = algorand.account.localnet_dispenser() +localnet_dispenser = algorand.account.localnet_dispenser() # Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD dispenser = algorand.account.dispenser_from_environment() # Get / create and register account from KMD idempotently by name diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md index 3cf93ee4..9ed7638c 100644 --- a/docs/source/capabilities/algorand-client.md +++ b/docs/source/capabilities/algorand-client.md @@ -177,7 +177,7 @@ All transaction parameters share the following common base parameters: - Round validity management - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. - - `last_valid_round: bigint | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. + - `last_valid_round: int | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](./transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. @@ -185,7 +185,7 @@ Then on top of that the base type gets extended for the specific type of transac AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: -- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to 10, except in [automated testing](./testing.md) where it's set to 1000 when targeting LocalNet. +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it's set to `1000`. - `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) - `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) - `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index fa202f7e..71357a46 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -158,7 +158,7 @@ result, app_client = factory.send.create( ## Updating and deleting an app -Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app created via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. ## Calling the app diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index 1028f32c..82fc97f4 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -33,7 +33,7 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md), [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150) and [ARC-4](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) and [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance - There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) - Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead @@ -175,7 +175,7 @@ When compiling TEAL template code, the capabilities described in the [above desi In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which will be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn't (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn't (permanent) diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md index baba7dd3..f3c5df0e 100644 --- a/docs/source/capabilities/transaction-composer.md +++ b/docs/source/capabilities/transaction-composer.md @@ -191,7 +191,7 @@ result = ( args=[1, 2, 3] # Resources will be automatically populated! )) - .send(send_params=SendParams(populate_app_call_resources=True)) + .send(params=SendParams(populate_app_call_resources=True)) ) # Or disable automatic population @@ -208,7 +208,7 @@ result = ( asset_references=[789], box_references=[box_reference] )) - .send(send_params=SendParams(populate_app_call_resources=False)) + .send(params=SendParams(populate_app_call_resources=False)) ) ```