From 92774f70995f249e66c7c40e03514883fa54bc0b Mon Sep 17 00:00:00 2001 From: Al Date: Fri, 20 Dec 2024 20:42:04 +0100 Subject: [PATCH] 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