diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c320aa..fdfd6d3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,7 @@ repos: additional_dependencies: [] minimum_pre_commit_version: "0" files: "^(src|tests)/" + exclude: "^tests/artifacts/" - id: mypy name: mypy description: "`mypy` will check Python types for correctness" @@ -33,3 +34,4 @@ repos: additional_dependencies: [] minimum_pre_commit_version: "2.9.2" files: "^(src|tests)/" + exclude: "^tests/artifacts/" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6b9d5948 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Tests", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "purpose": [ + "debug-test" + ], + "console": "integratedTerminal", + "justMyCode": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e570b2a6..a1162966 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,18 +16,24 @@ "**/__pycache__": true, ".idea": true }, - // Python "platformSettings.autoLoad": true, "python.defaultInterpreterPath": "${workspaceFolder}/.venv", - "python.analysis.extraPaths": ["${workspaceFolder}/src"], + "python.analysis.extraPaths": [ + "${workspaceFolder}/src" + ], "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" }, + "python.analysis.exclude": [ + "tests/artifacts/**" + ], "python.analysis.typeCheckingMode": "basic", "ruff.enable": true, "ruff.lint.run": "onSave", - "ruff.lint.args": ["--config=pyproject.toml"], + "ruff.lint.args": [ + "--config=pyproject.toml" + ], "ruff.importStrategy": "fromEnvironment", "ruff.fixAll": true, //lint and fix all files in workspace "ruff.organizeImports": true, //organize imports on save @@ -37,7 +43,6 @@ "ruff.codeAction.fixViolation": { "enable": true }, - "mypy.configFile": "pyproject.toml", // set to empty array to use config from project "mypy.targets": [], @@ -52,11 +57,7 @@ } ] }, - - // PowerShell - "[powershell]": { - "editor.defaultFormatter": "ms-vscode.powershell" - }, - "powershell.codeFormatting.preset": "Stroustrup", - "python.testing.pytestArgs": ["."] + "python.testing.pytestArgs": [ + "." + ], } diff --git a/docs/markdown/index.md b/docs/markdown/index.md index a3fa0518..71972566 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -7,46 +7,47 @@ The goal of this library is to provide intuitive, productive utility functions t Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. #### NOTE + If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). [Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]() # Contents -* [Account management](capabilities/account.md) - * [`Account`](capabilities/account.md#account) -* [Client management](capabilities/client.md) - * [Network configuration](capabilities/client.md#network-configuration) - * [Clients](capabilities/client.md#clients) -* [App client](capabilities/app-client.md) - * [Design](capabilities/app-client.md#design) - * [Creating an application client](capabilities/app-client.md#creating-an-application-client) - * [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) - * [Composing calls](capabilities/app-client.md#composing-calls) - * [Reading state](capabilities/app-client.md#reading-state) - * [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) -* [App deployment](capabilities/app-deploy.md) - * [Design](capabilities/app-deploy.md#design) - * [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) - * [Deploying an application](capabilities/app-deploy.md#deploying-an-application) -* [Algo transfers](capabilities/transfer.md) - * [Transferring Algos](capabilities/transfer.md#transferring-algos) - * [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) - * [Transfering Assets](capabilities/transfer.md#transfering-assets) - * [Dispenser](capabilities/transfer.md#dispenser) -* [TestNet Dispenser Client](capabilities/dispenser-client.md) - * [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) - * [Funding an Account](capabilities/dispenser-client.md#funding-an-account) - * [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) - * [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) - * [Error Handling](capabilities/dispenser-client.md#error-handling) -* [Debugger](capabilities/debugger.md) - * [Configuration](capabilities/debugger.md#configuration) - * [Debugging Utilities](capabilities/debugger.md#debugging-utilities) -* [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) - * [Data](apidocs/algokit_utils/algokit_utils.md#data) - * [Classes](apidocs/algokit_utils/algokit_utils.md#classes) - * [Functions](apidocs/algokit_utils/algokit_utils.md#functions) +- [Account management](capabilities/account.md) + - [`Account`](capabilities/account.md#account) +- [Client management](capabilities/client.md) + - [Network configuration](capabilities/client.md#network-configuration) + - [Clients](capabilities/client.md#clients) +- [App client](capabilities/app-client.md) + - [Design](capabilities/app-client.md#design) + - [Creating an application client](capabilities/app-client.md#creating-an-application-client) + - [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) + - [Composing calls](capabilities/app-client.md#composing-calls) + - [Reading state](capabilities/app-client.md#reading-state) + - [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) +- [App deployment](capabilities/app-deploy.md) + - [Design](capabilities/app-deploy.md#design) + - [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) + - [Deploying an application](capabilities/app-deploy.md#deploying-an-application) +- [Algo transfers](capabilities/transfer.md) + - [Transferring Algos](capabilities/transfer.md#transferring-algos) + - [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) + - [Transfering Assets](capabilities/transfer.md#transfering-assets) + - [Dispenser](capabilities/transfer.md#dispenser) +- [TestNet Dispenser Client](capabilities/dispenser-client.md) + - [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) + - [Funding an Account](capabilities/dispenser-client.md#funding-an-account) + - [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) + - [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) + - [Error Handling](capabilities/dispenser-client.md#error-handling) +- [Debugger](capabilities/debugger.md) + - [Configuration](capabilities/debugger.md#configuration) + - [Debugging Utilities](capabilities/debugger.md#debugging-utilities) +- [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) + - [Data](apidocs/algokit_utils/algokit_utils.md#data) + - [Classes](apidocs/algokit_utils/algokit_utils.md#classes) + - [Functions](apidocs/algokit_utils/algokit_utils.md#functions) diff --git a/legacy_v2_tests/conftest.py b/legacy_v2_tests/conftest.py index dbe4be46..f8989eb8 100644 --- a/legacy_v2_tests/conftest.py +++ b/legacy_v2_tests/conftest.py @@ -8,6 +8,8 @@ import algosdk.transaction import pytest +from dotenv import load_dotenv + from algokit_utils import ( DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, @@ -22,8 +24,6 @@ get_kmd_client_from_algod_client, replace_template_variables, ) -from dotenv import load_dotenv - from legacy_v2_tests import app_client_test if TYPE_CHECKING: @@ -142,7 +142,7 @@ def indexer_client() -> "IndexerClient": return get_indexer_client() -@pytest.fixture() +@pytest.fixture def creator(algod_client: "AlgodClient") -> Account: creator_name = get_unique_name() return get_account(algod_client, creator_name) diff --git a/legacy_v2_tests/test_account.py b/legacy_v2_tests/test_account.py index bb0ee272..e1ee2228 100644 --- a/legacy_v2_tests/test_account.py +++ b/legacy_v2_tests/test_account.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from algokit_utils import get_account - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app.py b/legacy_v2_tests/test_app.py index 07e258a1..1b79f708 100644 --- a/legacy_v2_tests/test_app.py +++ b/legacy_v2_tests/test_app.py @@ -1,4 +1,5 @@ import pytest + from algokit_utils import AppDeployMetaData diff --git a/legacy_v2_tests/test_app_client.py b/legacy_v2_tests/test_app_client.py index 87826175..b6565148 100644 --- a/legacy_v2_tests/test_app_client.py +++ b/legacy_v2_tests/test_app_client.py @@ -1,4 +1,5 @@ import pytest + from algokit_utils import ( DeploymentFailedError, get_next_version, diff --git a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt index 598d4c2f..70d16cc9 100644 --- a/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt +++ b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt @@ -2,6 +2,6 @@ Txn {txn} had error 'assert failed pc=743' at PC 743: Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map \ No newline at end of file + 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2. Set approval_source_map from a previously compiled approval program OR + 3. Import a previously exported source map using import_source_map \ No newline at end of file diff --git a/legacy_v2_tests/test_app_client_call.py b/legacy_v2_tests/test_app_client_call.py index 67acd4d5..14933f1b 100644 --- a/legacy_v2_tests/test_app_client_call.py +++ b/legacy_v2_tests/test_app_client_call.py @@ -1,17 +1,10 @@ from collections.abc import Generator +from hashlib import sha256 from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import Mock, patch -import algokit_utils import pytest -from algokit_utils import ( - Account, - ApplicationClient, - ApplicationSpecification, - CreateCallParameters, - get_account, -) from algosdk.atomic_transaction_composer import ( AccountTransactionSigner, AtomicTransactionComposer, @@ -19,6 +12,16 @@ ) from algosdk.transaction import ApplicationCallTxn, PaymentTxn +import algokit_utils +import algokit_utils._legacy_v2 +import algokit_utils._legacy_v2.logic_error +from algokit_utils import ( + Account, + ApplicationClient, + ApplicationSpecification, + CreateCallParameters, + get_account, +) from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: @@ -84,7 +87,7 @@ def test_abi_call_with_transaction_arg(client_fixture: ApplicationClient, funded sender=funded_account.address, receiver=client_fixture.app_address, amt=1_000_000, - note=b"Payment", + note=sha256(b"self-payment").digest(), sp=client_fixture.algod_client.suggested_params(), ) # type: ignore[no-untyped-call] payment_with_signer = TransactionWithSigner(payment, AccountTransactionSigner(funded_account.private_key)) @@ -186,7 +189,7 @@ def test_readonly_call(client_fixture: ApplicationClient) -> None: def test_readonly_call_with_error(client_fixture: ApplicationClient) -> None: - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -211,7 +214,7 @@ def test_readonly_call_with_error_with_new_client_provided_template_values( ) new_client.approval_source_map = client.approval_source_map - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -234,7 +237,7 @@ def test_readonly_call_with_error_with_new_client_provided_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) new_client.approval_source_map = client.approval_source_map - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -259,7 +262,7 @@ def test_readonly_call_with_error_with_imported_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) new_client.import_source_map(source_map_export) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -281,7 +284,7 @@ def test_readonly_call_with_error_with_new_client_missing_source_map( new_client = ApplicationClient(algod_client, app_spec, app_id=client.app_id, signer=funded_account) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 new_client.call( "readonly", error=1, @@ -292,7 +295,7 @@ def test_readonly_call_with_error_with_new_client_missing_source_map( def test_readonly_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixture: ApplicationClient) -> None: mock_config.debug = False - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -302,7 +305,7 @@ def test_readonly_call_with_error_debug_mode_disabled(mock_config: Mock, client_ def test_readonly_call_with_error_debug_mode_enabled(client_fixture: ApplicationClient) -> None: - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "readonly", error=1, @@ -322,7 +325,7 @@ def test_app_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixtu min_funding_increment_micro_algos=200_000, ), ) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "set_box", name=b"ssss", @@ -342,7 +345,7 @@ def test_app_call_with_error_debug_mode_enabled(client_fixture: ApplicationClien min_funding_increment_micro_algos=200_000, ), ) - with pytest.raises(algokit_utils.LogicError) as ex: + with pytest.raises(algokit_utils._legacy_v2.logic_error.LogicError) as ex: # noqa: SLF001 client_fixture.call( "set_box", name=b"ssss", @@ -350,4 +353,3 @@ def test_app_call_with_error_debug_mode_enabled(client_fixture: ApplicationClien ) assert ex.value.traces is not None - assert ex.value.traces[0].exec_trace["approval-program-trace"] is not None diff --git a/legacy_v2_tests/test_app_client_clear_state.py b/legacy_v2_tests/test_app_client_clear_state.py index f26a7094..1d2f6529 100644 --- a/legacy_v2_tests/test_app_client_clear_state.py +++ b/legacy_v2_tests/test_app_client_clear_state.py @@ -2,12 +2,12 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, ) - from legacy_v2_tests.conftest import is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_close_out.py b/legacy_v2_tests/test_app_client_close_out.py index 5ee5e9c6..81ac5ea9 100644 --- a/legacy_v2_tests/test_app_client_close_out.py +++ b/legacy_v2_tests/test_app_client_close_out.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_create.py b/legacy_v2_tests/test_app_client_create.py index 1da7bbf7..00fd9691 100644 --- a/legacy_v2_tests/test_app_client_create.py +++ b/legacy_v2_tests/test_app_client_create.py @@ -2,6 +2,9 @@ from typing import TYPE_CHECKING import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionSigner +from algosdk.transaction import ApplicationCallTxn, GenericSignedTransaction, OnComplete, Transaction + from algokit_utils import ( Account, ApplicationClient, @@ -10,9 +13,6 @@ get_account, get_app_id_from_tx_id, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer, TransactionSigner -from algosdk.transaction import ApplicationCallTxn, GenericSignedTransaction, OnComplete, Transaction - from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_delete.py b/legacy_v2_tests/test_app_client_delete.py index 353bbfab..d5df42cb 100644 --- a/legacy_v2_tests/test_app_client_delete.py +++ b/legacy_v2_tests/test_app_client_delete.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_deploy.py b/legacy_v2_tests/test_app_client_deploy.py index 4eed49b6..e51392b4 100644 --- a/legacy_v2_tests/test_app_client_deploy.py +++ b/legacy_v2_tests/test_app_client_deploy.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( ABICreateCallArgs, Account, @@ -9,7 +10,6 @@ TransferParameters, transfer, ) - from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: @@ -17,7 +17,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_opt_in.py b/legacy_v2_tests/test_app_client_opt_in.py index 816e96f0..afc1fb1e 100644 --- a/legacy_v2_tests/test_app_client_opt_in.py +++ b/legacy_v2_tests/test_app_client_opt_in.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: @@ -15,7 +15,7 @@ from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/legacy_v2_tests/test_app_client_prepare.py b/legacy_v2_tests/test_app_client_prepare.py index 6c6355b0..affacd50 100644 --- a/legacy_v2_tests/test_app_client_prepare.py +++ b/legacy_v2_tests/test_app_client_prepare.py @@ -1,11 +1,12 @@ import base64 from typing import TYPE_CHECKING +from algosdk.atomic_transaction_composer import AccountTransactionSigner + from algokit_utils import ( ApplicationClient, ApplicationSpecification, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/legacy_v2_tests/test_app_client_resolve.py b/legacy_v2_tests/test_app_client_resolve.py index 6c6023f3..d7e8b1d1 100644 --- a/legacy_v2_tests/test_app_client_resolve.py +++ b/legacy_v2_tests/test_app_client_resolve.py @@ -5,7 +5,6 @@ ApplicationClient, DefaultArgumentDict, ) - from legacy_v2_tests.conftest import read_spec if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_signer_sender.py b/legacy_v2_tests/test_app_client_signer_sender.py index d6c383cb..cfdef0ac 100644 --- a/legacy_v2_tests/test_app_client_signer_sender.py +++ b/legacy_v2_tests/test_app_client_signer_sender.py @@ -3,12 +3,13 @@ from typing import TYPE_CHECKING, Any import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner + from algokit_utils import ( ApplicationClient, ApplicationSpecification, get_sender_from_signer, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner if TYPE_CHECKING: from algosdk import transaction @@ -30,7 +31,7 @@ def sign_transactions( @pytest.mark.parametrize("override_signer", [CustomSigner(), AccountTransactionSigner(fake_key), None]) @pytest.mark.parametrize("default_sender", ["default_sender", None]) @pytest.mark.parametrize("default_signer", [CustomSigner(), AccountTransactionSigner(fake_key), None]) -def test_resolve_signer_sender( # noqa: PLR0913 +def test_resolve_signer_sender( *, algod_client: "AlgodClient", app_spec: ApplicationSpecification, diff --git a/legacy_v2_tests/test_app_client_template_values.py b/legacy_v2_tests/test_app_client_template_values.py index 5b27f320..a01f53d9 100644 --- a/legacy_v2_tests/test_app_client_template_values.py +++ b/legacy_v2_tests/test_app_client_template_values.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING -import algokit_utils import pytest +import algokit_utils from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_app_client_update.py b/legacy_v2_tests/test_app_client_update.py index 60cd10d9..4dc082e0 100644 --- a/legacy_v2_tests/test_app_client_update.py +++ b/legacy_v2_tests/test_app_client_update.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: diff --git a/legacy_v2_tests/test_asset.py b/legacy_v2_tests/test_asset.py index 3d75fa86..c26906ff 100644 --- a/legacy_v2_tests/test_asset.py +++ b/legacy_v2_tests/test_asset.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, EnsureBalanceParameters, @@ -19,7 +20,7 @@ from legacy_v2_tests.conftest import assure_funds, generate_test_asset, get_unique_name -@pytest.fixture() +@pytest.fixture def to_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py index 9b6d8ca8..b827ecd3 100644 --- a/legacy_v2_tests/test_debug_utils.py +++ b/legacy_v2_tests/test_debug_utils.py @@ -3,6 +3,13 @@ from unittest.mock import Mock import pytest +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + from algokit_utils._debugging import ( AVMDebuggerSourceMap, PersistSourceMapInput, @@ -14,20 +21,13 @@ from algokit_utils.application_specification import ApplicationSpecification from algokit_utils.common import Program from algokit_utils.models import Account -from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, - AtomicTransactionComposer, - TransactionWithSigner, -) -from algosdk.transaction import PaymentTxn - from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -@pytest.fixture() +@pytest.fixture def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: creator_name = get_unique_name() creator = get_account(algod_client, creator_name) diff --git a/legacy_v2_tests/test_deploy.py b/legacy_v2_tests/test_deploy.py index 51708f52..4d2cf8c0 100644 --- a/legacy_v2_tests/test_deploy.py +++ b/legacy_v2_tests/test_deploy.py @@ -2,7 +2,6 @@ replace_template_variables, ) from algokit_utils._legacy_v2.deploy import strip_comments - from legacy_v2_tests.conftest import check_output_stability diff --git a/legacy_v2_tests/test_deploy_scenarios.py b/legacy_v2_tests/test_deploy_scenarios.py index 309fe4a3..c230ce37 100644 --- a/legacy_v2_tests/test_deploy_scenarios.py +++ b/legacy_v2_tests/test_deploy_scenarios.py @@ -6,6 +6,7 @@ from unittest.mock import Mock, patch import pytest + from algokit_utils import ( Account, ApplicationClient, @@ -19,7 +20,6 @@ get_indexer_client, get_localnet_default_account, ) - from legacy_v2_tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def __init__( self.creator = creator self.app_name = get_unique_name() - def deploy( # noqa: PLR0913 + def deploy( self, app_spec: ApplicationSpecification, *, @@ -128,12 +128,12 @@ def creator(creator_name: str) -> Account: return get_account(get_algod_client(), creator_name) -@pytest.fixture() +@pytest.fixture def app_name() -> str: return get_unique_name() -@pytest.fixture() +@pytest.fixture def deploy_fixture( caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest, creator_name: str, creator: Account ) -> DeployFixture: diff --git a/legacy_v2_tests/test_dispenser_api_client.py b/legacy_v2_tests/test_dispenser_api_client.py index baa2e1db..ac7fa0f8 100644 --- a/legacy_v2_tests/test_dispenser_api_client.py +++ b/legacy_v2_tests/test_dispenser_api_client.py @@ -1,13 +1,14 @@ import json import pytest +from pytest_httpx import HTTPXMock + from algokit_utils.dispenser_api import ( DISPENSER_ASSETS, DispenserApiConfig, DispenserAssetName, TestNetDispenserApiClient, ) -from pytest_httpx import HTTPXMock class TestDispenserApiTestnetClient: diff --git a/legacy_v2_tests/test_transfer.py b/legacy_v2_tests/test_transfer.py index 8253a5eb..335fcf1a 100644 --- a/legacy_v2_tests/test_transfer.py +++ b/legacy_v2_tests/test_transfer.py @@ -3,6 +3,11 @@ import algosdk import httpx import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.transaction import PaymentTxn +from algosdk.util import algos_to_microalgos +from pytest_httpx import HTTPXMock + from algokit_utils import ( Account, EnsureBalanceParameters, @@ -19,11 +24,6 @@ ) from algokit_utils.dispenser_api import DispenserApiConfig from algokit_utils.network_clients import get_algod_client, get_algonode_config -from algosdk.atomic_transaction_composer import AccountTransactionSigner -from algosdk.transaction import PaymentTxn -from algosdk.util import algos_to_microalgos -from pytest_httpx import HTTPXMock - from legacy_v2_tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name from legacy_v2_tests.test_network_clients import DEFAULT_TOKEN @@ -35,12 +35,12 @@ MINIMUM_BALANCE = 100_000 # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance -@pytest.fixture() +@pytest.fixture def to_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) -@pytest.fixture() +@pytest.fixture def rekeyed_from_account(algod_client: "AlgodClient", kmd_client: "KMDClient") -> Account: account = create_kmd_wallet_account(kmd_client, get_unique_name()) rekey_account = create_kmd_wallet_account(kmd_client, get_unique_name()) @@ -68,7 +68,7 @@ def rekeyed_from_account(algod_client: "AlgodClient", kmd_client: "KMDClient") - return Account(address=account.address, private_key=rekey_account.private_key) -@pytest.fixture() +@pytest.fixture def transaction_signer_from_account( kmd_client: "KMDClient", algod_client: "AlgodClient", @@ -87,7 +87,7 @@ def transaction_signer_from_account( return AccountTransactionSigner(private_key=account.private_key) -@pytest.fixture() +@pytest.fixture def clawback_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) diff --git a/poetry.lock b/poetry.lock index 3544afa0..e173428a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,35 +13,35 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.6" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, - {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, + {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, + {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, ] [package.dependencies] @@ -104,13 +104,13 @@ files = [ [[package]] name = "cachecontrol" -version = "0.14.0" +version = "0.14.1" description = "httplib2 caching for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, + {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, + {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, ] [package.dependencies] @@ -119,7 +119,7 @@ msgpack = ">=0.5.2,<2.0.0" requests = ">=2.16.0" [package.extras] -dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "furo", "mypy", "pytest", "pytest-cov", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] @@ -379,73 +379,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.3" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, - {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, - {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, - {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, - {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, - {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, - {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, - {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, - {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, - {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, - {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, - {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, - {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, - {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, - {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, - {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.dependencies] @@ -456,51 +456,53 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -538,20 +540,20 @@ files = [ [[package]] name = "deprecated" -version = "1.2.14" +version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, + {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, + {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, ] [package.dependencies] wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] [[package]] name = "distlib" @@ -765,13 +767,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.3" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -939,13 +941,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.4.1" +version = "25.5.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, - {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, ] [package.dependencies] @@ -968,13 +970,13 @@ type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "license-expression" -version = "30.3.1" +version = "30.4.0" description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "license_expression-30.3.1-py3-none-any.whl", hash = "sha256:97904b9185c7bbb1e98799606fa7424191c375e70ba63a524b6f7100e42ddc46"}, - {file = "license_expression-30.3.1.tar.gz", hash = "sha256:60d5bec1f3364c256a92b9a08583d7ea933c7aa272c8d36d04144a89a3858c01"}, + {file = "license_expression-30.4.0-py3-none-any.whl", hash = "sha256:7c8f240c6e20d759cb8455e49cb44a923d9e25c436bf48d7e5b8eea660782c04"}, + {file = "license_expression-30.4.0.tar.gz", hash = "sha256:6464397f8ed4353cc778999caec43b099f8d8d5b335f282e26a9eb9435522f05"}, ] [package.dependencies] @@ -1030,72 +1032,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1214,43 +1216,43 @@ files = [ [[package]] name = "mypy" -version = "1.12.0" +version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, - {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, - {file = "mypy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e"}, - {file = "mypy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a"}, - {file = "mypy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff"}, - {file = "mypy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7"}, - {file = "mypy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57"}, - {file = "mypy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309"}, - {file = "mypy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f"}, - {file = "mypy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9"}, - {file = "mypy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164"}, - {file = "mypy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475"}, - {file = "mypy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9"}, - {file = "mypy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642"}, - {file = "mypy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601"}, - {file = "mypy-1.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521"}, - {file = "mypy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893"}, - {file = "mypy-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721"}, - {file = "mypy-1.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3"}, - {file = "mypy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b"}, - {file = "mypy-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f"}, - {file = "mypy-1.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e"}, - {file = "mypy-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7"}, - {file = "mypy-1.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b"}, - {file = "mypy-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0"}, - {file = "mypy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8"}, - {file = "mypy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0"}, - {file = "mypy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1"}, - {file = "mypy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e"}, - {file = "mypy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa"}, - {file = "mypy-1.12.0-py3-none-any.whl", hash = "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266"}, - {file = "mypy-1.12.0.tar.gz", hash = "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] @@ -1260,6 +1262,7 @@ typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -1303,27 +1306,35 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, [[package]] name = "nh3" -version = "0.2.18" +version = "0.2.19" description = "Python bindings to the ammonia HTML sanitization library." optional = false python-versions = "*" files = [ - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86"}, - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204"}, - {file = "nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be"}, - {file = "nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844"}, - {file = "nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4"}, + {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, + {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, + {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, + {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, + {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, + {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, + {file = "nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804"}, ] [[package]] @@ -1339,13 +1350,13 @@ files = [ [[package]] name = "packageurl-python" -version = "0.15.6" +version = "0.16.0" description = "A purl aka. Package URL parser and builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packageurl_python-0.15.6-py3-none-any.whl", hash = "sha256:a40210652c89022772a6c8340d6066f7d5dc67132141e5284a4db7a27d0a8ab0"}, - {file = "packageurl_python-0.15.6.tar.gz", hash = "sha256:cbc89afd15d5f4d05db4f1b61297e5b97a43f61f28799f6d282aff467ed2ee96"}, + {file = "packageurl_python-0.16.0-py3-none-any.whl", hash = "sha256:5c3872638b177b0f1cf01c3673017b7b27ebee485693ae12a8bed70fa7fa7c35"}, + {file = "packageurl_python-0.16.0.tar.gz", hash = "sha256:69e3bf8a3932fe9c2400f56aaeb9f86911ecee2f9398dbe1b58ec34340be365d"}, ] [package.extras] @@ -1356,13 +1367,13 @@ test = ["pytest"] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1378,13 +1389,13 @@ files = [ [[package]] name = "pip" -version = "24.2" +version = "24.3.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" files = [ - {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, - {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, + {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, + {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, ] [[package]] @@ -1450,13 +1461,13 @@ testing = ["aboutcode-toolkit (>=6.0.0)", "black", "pytest (>=6,!=7.0.0)", "pyte [[package]] name = "pkginfo" -version = "1.11.2" +version = "1.12.0" description = "Query metadata from sdists / bdists / installed packages." optional = false python-versions = ">=3.8" files = [ - {file = "pkginfo-1.11.2-py3-none-any.whl", hash = "sha256:9ec518eefccd159de7ed45386a6bb4c6ca5fa2cb3bd9b71154fae44f6f1b36a3"}, - {file = "pkginfo-1.11.2.tar.gz", hash = "sha256:c6bc916b8298d159e31f2c216e35ee5b86da7da18874f879798d0a1983537c86"}, + {file = "pkginfo-1.12.0-py3-none-any.whl", hash = "sha256:dcd589c9be4da8973eceffa247733c144812759aa67eaf4bbf97016a02f39088"}, + {file = "pkginfo-1.12.0.tar.gz", hash = "sha256:8ad91a0445a036782b9366ef8b8c2c50291f83a553478ba8580c73d3215700cf"}, ] [package.extras] @@ -1988,13 +1999,13 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.9.2" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, - {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] @@ -2007,28 +2018,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.4.10" +version = "0.8.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, - {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, - {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, - {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, - {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, - {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, + {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, + {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, + {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, + {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, + {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, + {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, + {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, ] [[package]] @@ -2074,33 +2086,33 @@ files = [ [[package]] name = "setuptools" -version = "75.2.0" +version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"}, - {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"}, + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2394,13 +2406,43 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -2416,20 +2458,21 @@ files = [ [[package]] name = "tqdm" -version = "4.66.5" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, - {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -2459,13 +2502,13 @@ urllib3 = ">=1.26.0" [[package]] name = "types-deprecated" -version = "1.2.9.20240311" +version = "1.2.15.20241117" description = "Typing stubs for Deprecated" optional = false python-versions = ">=3.8" files = [ - {file = "types-Deprecated-1.2.9.20240311.tar.gz", hash = "sha256:0680e89989a8142707de8103f15d182445a533c1047fd9b7e8c5459101e9b90a"}, - {file = "types_Deprecated-1.2.9.20240311-py3-none-any.whl", hash = "sha256:d7793aaf32ff8f7e49a8ac781de4872248e0694c4b75a7a8a186c51167463f9d"}, + {file = "types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e"}, + {file = "types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884"}, ] [[package]] @@ -2512,13 +2555,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -2543,13 +2586,13 @@ files = [ [[package]] name = "wheel" -version = "0.44.0" +version = "0.45.1" description = "A built-package format for Python" optional = false python-versions = ">=3.8" files = [ - {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, - {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, ] [package.extras] @@ -2557,92 +2600,87 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -2656,4 +2694,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "66e85df44cca4d3edccb50f730dfb4e9dccf93582e78fa0074dc9b47baa925e2" +content-hash = "726e13c507aac04c65d86a3ad85222f7c218eaed40689343445e9ff8574e8ec2" diff --git a/pyproject.toml b/pyproject.toml index bd391ae5..f5efc256 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ deprecated = "^1.2.14" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" -ruff = ">=0.1.6,<0.5.0" +ruff = ">=0.1.6,<=0.8.2" pip-audit = "^2.5.6" pytest-mock = "^3.11.1" mypy = "^1.5.1" @@ -93,8 +93,6 @@ lint.select = [ "RUF", # Ruff-specific rules ] lint.ignore = [ - "ANN101", # no type for self - "ANN102", # no type for cls "RET505", # allow else after return "SIM108", # allow if-else in place of ternary "E111", # indentation is not a multiple of four @@ -106,6 +104,7 @@ lint.ignore = [ "Q002", # bad quotes docstring "Q003", # avoidable escaped quotes "W191", # indentation contains tabs + "ERA001", # commented out code ] # Exclude a variety of commonly ignored directories. extend-exclude = [ @@ -113,31 +112,38 @@ extend-exclude = [ ".git", ".mypy_cache", ".ruff_cache", - + "tests/artifacts", ] # Assume Python 3.10. target-version = "py310" +[tool.ruff.lint.pylint] +max-args = 10 + [tool.ruff.lint.flake8-annotations] allow-star-arg-any = true suppress-none-returning = true [tool.ruff.lint.per-file-ignores] "src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"] -"path/to/file.py" = ["E402"] +"src/algokit_utils/applications/app_client.py" = ["SLF001"] +"src/algokit_utils/applications/app_factory.py" = ["SLF001"] "tests/clients/test_algorand_client.py" = ["ERA001"] +"src/algokit_utils/_legacy_v2/**/*" = ["E501"] +"tests/**/*" = ["PLR2004"] [tool.poe.tasks] docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" +"tests/**/*" = ["PLR2004"] [tool.pytest.ini_options] pythonpath = ["src", "tests"] [tool.mypy] files = ["src", "tests"] -exclude = ["dist"] +exclude = ["dist", "tests/artifacts", "src/algokit_utils/_legacy_v2"] python_version = "3.10" warn_unused_ignores = true warn_redundant_casts = true diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index d89bad9b..5b3a1647 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -92,93 +92,93 @@ from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME __all__ = [ - # ==== LEGACY V2 EXPORTS BEGIN ==== - "create_kmd_wallet_account", - "get_account_from_mnemonic", - "get_or_create_kmd_wallet_account", - "get_localnet_default_account", - "get_dispenser_account", - "get_kmd_wallet_account", - "get_account", - "UPDATABLE_TEMPLATE_NAME", "DELETABLE_TEMPLATE_NAME", + "DISPENSER_ACCESS_TOKEN_KEY", + "DISPENSER_REQUEST_TIMEOUT", "NOTE_PREFIX", - "DeploymentFailedError", - "AppReference", - "AppDeployMetaData", - "AppMetaData", - "AppLookup", - "get_creator_apps", - "replace_template_variables", + "UPDATABLE_TEMPLATE_NAME", "ABIArgsDict", "ABICallArgs", "ABICallArgsDict", "ABICreateCallArgs", "ABICreateCallArgsDict", "ABIMethod", + "ABITransactionResponse", + "Account", + "AlgoClientConfig", + "AppDeployMetaData", + "AppLookup", + "AppMetaData", + "AppReference", + "AppSpecStateDict", + "ApplicationClient", + "ApplicationSpecification", + "CallConfig", + "CommonCallParameters", + "CommonCallParametersDict", "CreateCallParameters", "CreateCallParametersDict", "CreateTransactionParameters", - "CommonCallParameters", - "CommonCallParametersDict", + "DefaultArgumentDict", + "DefaultArgumentType", "DeployCallArgs", - "DeployCreateCallArgs", "DeployCallArgsDict", + "DeployCreateCallArgs", "DeployCreateCallArgsDict", + "DeployResponse", + "DeploymentFailedError", + "DispenserFundResponse", + "DispenserLimitResponse", + "EnsureBalanceParameters", + "EnsureFundedResponse", + "LogicError", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", "OnCompleteCallParameters", "OnCompleteCallParametersDict", - "TransactionParameters", - "TransactionParametersDict", - "ApplicationClient", - "DeployResponse", - "OnUpdate", "OnSchemaBreak", + "OnUpdate", "OperationPerformed", + "PersistSourceMapInput", + "Program", "TemplateValueDict", "TemplateValueMapping", - "Program", - "execute_atc_with_logic_error", - "get_app_id_from_tx_id", - "get_next_version", - "get_sender_from_signer", - "num_extra_program_pages", - "AppSpecStateDict", - "ApplicationSpecification", - "CallConfig", - "DefaultArgumentDict", - "DefaultArgumentType", - "MethodConfigDict", - "OnCompleteActionName", - "MethodHints", - "LogicError", - "ABITransactionResponse", - "Account", + "TestNetDispenserApiClient", + "TransactionParameters", + "TransactionParametersDict", "TransactionResponse", - "AlgoClientConfig", + "TransferAssetParameters", + "TransferParameters", + # ==== LEGACY V2 EXPORTS BEGIN ==== + "create_kmd_wallet_account", + "ensure_funded", + "execute_atc_with_logic_error", + "get_account", + "get_account_from_mnemonic", "get_algod_client", "get_algonode_config", + "get_app_id_from_tx_id", + "get_creator_apps", "get_default_localnet_config", + "get_dispenser_account", "get_indexer_client", "get_kmd_client_from_algod_client", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_next_version", + "get_or_create_kmd_wallet_account", + "get_sender_from_signer", "is_localnet", "is_mainnet", "is_testnet", - "TestNetDispenserApiClient", - "DispenserFundResponse", - "DispenserLimitResponse", - "DISPENSER_ACCESS_TOKEN_KEY", - "DISPENSER_REQUEST_TIMEOUT", - "EnsureBalanceParameters", - "EnsureFundedResponse", - "TransferParameters", - "ensure_funded", - "transfer", - "TransferAssetParameters", - "transfer_asset", + "num_extra_program_pages", "opt_in", "opt_out", "persist_sourcemaps", - "PersistSourceMapInput", + "replace_template_variables", "simulate_and_persist_response", + "transfer", + "transfer_asset", # ==== LEGACY V2 EXPORTS END ==== ] diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index de5ed182..0b9f798f 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -143,7 +143,7 @@ def _write_to_file(path: Path, content: str) -> None: path.write_text(content) -def _build_avm_sourcemap( # noqa: PLR0913 +def _build_avm_sourcemap( *, app_name: str, file_name: str, @@ -201,7 +201,18 @@ def persist_sourcemaps( _upsert_debug_sourcemaps(sourcemaps, project_root) -def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse: +def simulate_response( + atc: AtomicTransactionComposer, + algod_client: "AlgodClient", + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, # noqa: ARG001 TODO: revisit + fix_signers: bool | None = None, # noqa: ARG001 TODO: revisit +) -> SimulateAtomicTransactionResponse: """ Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. @@ -221,13 +232,31 @@ def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True, state_change=True) simulate_request = SimulateRequest( - txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config + txn_groups=txn_group, + allow_more_logs=allow_more_logs or True, + round=round, + extra_opcode_budget=extra_opcode_budget or 0, + allow_unnamed_resources=allow_unnamed_resources or True, + allow_empty_signatures=allow_empty_signatures or True, + exec_trace_config=exec_trace_config or trace_config, ) + return atc.simulate(algod_client, simulate_request) -def simulate_and_persist_response( - atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256 +def simulate_and_persist_response( # noqa: PLR0913 TODO: revisit + atc: AtomicTransactionComposer, + project_root: Path, + algod_client: "AlgodClient", + buffer_size_mb: float = 256, + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, + fix_signers: bool | None = None, ) -> SimulateAtomicTransactionResponse: """ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, @@ -252,7 +281,18 @@ def simulate_and_persist_response( txn_with_sign.txn.last_valid_round = sp.last txn_with_sign.txn.genesis_hash = sp.gh - response = simulate_response(atc_to_simulate, algod_client) + response = simulate_response( + atc_to_simulate, + algod_client, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) txn_results = response.simulate_response["txn-groups"] txn_types = [txn_result["txn-results"][0]["txn-result"]["txn"]["txn"]["type"] for txn_result in txn_results] diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index 2db90f36..99409b36 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.account import get_dispenser_account @@ -115,6 +116,10 @@ def _fund_using_transfer( return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt) +@deprecated( + "Use `algorand.account.ensure_funded()`, `algorand.account.ensure_funded_from_environment()`, " + "or `algorand.account.ensure_funded_from_testnet_dispenser_api()` instead" +) def ensure_funded( client: AlgodClient, parameters: EnsureBalanceParameters, diff --git a/src/algokit_utils/_legacy_v2/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py index 6b59cd4c..28de779f 100644 --- a/src/algokit_utils/_legacy_v2/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient -__all__ = ["TransferParameters", "transfer", "TransferAssetParameters", "transfer_asset"] +__all__ = ["TransferAssetParameters", "TransferParameters", "transfer", "transfer_asset"] logger = logging.getLogger(__name__) diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index d98a875a..9da21ca1 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -5,6 +5,7 @@ from algosdk.account import address_from_private_key from algosdk.mnemonic import from_private_key, to_private_key from algosdk.util import algos_to_microalgos +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet @@ -30,13 +31,17 @@ _DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 +@deprecated( + "Use `algorand.account.from_mnemonic()` instead. Example: " "`account = algorand.account.from_mnemonic(mnemonic)`" +) def get_account_from_mnemonic(mnemonic: str) -> Account: """Convert a mnemonic (25 word passphrase) into an Account""" private_key = to_private_key(mnemonic) - address = address_from_private_key(private_key) + address = str(address_from_private_key(private_key)) return Account(private_key=private_key, address=address) +@deprecated("Use `algorand.account.from_kmd()` instead. Example: " "`account = algorand.account.from_kmd(name)`") def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: """Creates a wallet with specified name""" wallet_id = kmd_client.create_wallet(name, "")["id"] @@ -50,6 +55,10 @@ def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " + "`account = algorand.account.from_kmd(name, fund_with=AlgoAmount.from_algo(1000))`" +) def get_or_create_kmd_wallet_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: @@ -90,6 +99,10 @@ def _is_default_account(account: dict[str, Any]) -> bool: return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " + "`account = algorand.account.from_kmd('unencrypted-default-wallet', lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000)`" +) def get_localnet_default_account(client: "AlgodClient") -> Account: """Returns the default Account in a LocalNet instance""" if not is_localnet(client): @@ -102,6 +115,10 @@ def get_localnet_default_account(client: "AlgodClient") -> Account: return account +@deprecated( + "Use `algorand.account.dispenser_from_environment()` or `algorand.account.localnet_dispenser()` instead. " + "Example: `dispenser = algorand.account.dispenser_from_environment()`" +) def get_dispenser_account(client: "AlgodClient") -> Account: """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" if is_localnet(client): @@ -109,6 +126,9 @@ def get_dispenser_account(client: "AlgodClient") -> Account: return get_account(client, "DISPENSER") +@deprecated( + "Use `algorand.account.from_kmd()` instead. Example: " "`account = algorand.account.from_kmd(name, predicate)`" +) def get_kmd_wallet_account( client: "AlgodClient", kmd_client: "KMDClient", @@ -142,6 +162,11 @@ def get_kmd_wallet_account( return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated( + "Use `algorand.account.from_environment()` or `algorand.account.from_kmd()` or `algorand.account.random()` instead. " + "Example: " + "`account = algorand.account.from_environment('ACCOUNT', AlgoAmount.from_algo(1000))`" +) def get_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index a52639d1..0334c832 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -27,6 +27,7 @@ from algosdk.constants import APP_PAGE_MAX_SIZE from algosdk.logic import get_application_address from algosdk.source_map import SourceMap +from typing_extensions import deprecated import algokit_utils._legacy_v2.application_specification as au_spec import algokit_utils._legacy_v2.deploy as au_deploy @@ -83,6 +84,16 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) +@deprecated( + "Use AppClient from algokit_utils.applications instead. Example:\n" + "```python\n" + "from algokit_utils.clients import AlgorandClient\n" + "from algokit_utils.models.application import Arc56Contract\n" + "algorand_client = AlgorandClient.from_environment()\n" + "app_client = AppClient.from_network(app_spec=Arc56Contract.from_json(app_spec_json), " + "algorand=algorand_client, app_id=123)\n" + "```" +) class ApplicationClient: """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" @@ -239,7 +250,7 @@ def prepare( ) return new_client - def _prepare( # noqa: PLR0913 + def _prepare( self, target: "ApplicationClient", *, @@ -913,9 +924,9 @@ def _check_app_id(self) -> None: if self.app_id == 0: raise Exception( "ApplicationClient is not associated with an app instance, to resolve either:\n" - "1.) provide an app_id on construction OR\n" - "2.) provide a creator address so an app can be searched for OR\n" - "3.) create an app first using create or deploy methods" + "1.provide an app_id on construction OR\n" + "2.provide a creator address so an app can be searched for OR\n" + "3.create an app first using create or deploy methods" ) def _resolve_method( @@ -1254,6 +1265,10 @@ def _try_convert_to_logic_error( return None +@deprecated( + "The execute_atc_with_logic_error function is deprecated; use AppClient's error handling and TransactionComposer's " + "send method for equivalent functionality and improved error management." +) def execute_atc_with_logic_error( atc: AtomicTransactionComposer, algod_client: "AlgodClient", diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py index 865dece5..5b034929 100644 --- a/src/algokit_utils/_legacy_v2/application_specification.py +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -8,16 +8,17 @@ from algosdk.abi import Contract from algosdk.abi.method import MethodDict from algosdk.transaction import StateSchema +from typing_extensions import deprecated __all__ = [ + "AppSpecStateDict", + "ApplicationSpecification", "CallConfig", "DefaultArgumentDict", "DefaultArgumentType", "MethodConfigDict", - "OnCompleteActionName", "MethodHints", - "ApplicationSpecification", - "AppSpecStateDict", + "OnCompleteActionName", ] @@ -136,6 +137,10 @@ def _decode_state_schema(data: dict[str, int]) -> StateSchema: ) +@deprecated( + "The ApplicationSpecification class is deprecated. Use Arc56Contract and the TransactionComposer and AppClient " + "classes for modern application development." +) @dataclasses.dataclass(kw_only=True) class ApplicationSpecification: """ARC-0032 application specification diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 2f71cbf8..409523c9 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner from algosdk.constants import TX_GROUP_LIMIT from algosdk.transaction import AssetTransferTxn +from typing_extensions import deprecated if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -68,6 +69,10 @@ def _ensure_asset_balance_conditions( raise ValueError(error_message) +@deprecated( + "Use TransactionComposer.add_asset_opt_in() or AlgorandClient.asset.opt_in() instead. " + "Example: composer.add_asset_opt_in(AssetOptInParams(sender=account.address, asset_id=123))" +) def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, @@ -116,6 +121,10 @@ def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) return result +@deprecated( + "Use TransactionComposer.add_asset_opt_out() or AlgorandClient.asset.opt_out() instead. " + "Example: composer.add_asset_opt_out(AssetOptOutParams(sender=account.address, asset_id=123, creator=creator_address))" +) def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index ed0bd0e5..6a73ba45 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -7,11 +7,11 @@ from enum import Enum from typing import TYPE_CHECKING, TypeAlias, TypedDict +import algosdk from algosdk import transaction from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner -from algosdk.logic import get_application_address from algosdk.transaction import StateSchema -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._legacy_v2.application_specification import ( ApplicationSpecification, @@ -36,26 +36,26 @@ __all__ = [ - "UPDATABLE_TEMPLATE_NAME", "DELETABLE_TEMPLATE_NAME", "NOTE_PREFIX", + "UPDATABLE_TEMPLATE_NAME", "ABICallArgs", - "ABICreateCallArgs", "ABICallArgsDict", + "ABICreateCallArgs", "ABICreateCallArgsDict", - "DeploymentFailedError", - "AppReference", "AppDeployMetaData", - "AppMetaData", "AppLookup", + "AppMetaData", + "AppReference", "DeployCallArgs", - "DeployCreateCallArgs", "DeployCallArgsDict", + "DeployCreateCallArgs", "DeployCreateCallArgsDict", - "Deployer", "DeployResponse", - "OnUpdate", + "Deployer", + "DeploymentFailedError", "OnSchemaBreak", + "OnUpdate", "OperationPerformed", "TemplateValueDict", "TemplateValueMapping", @@ -175,6 +175,7 @@ def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: return None +@deprecated("Deprecated") def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified creator that have a transaction note containing {py:class}`AppDeployMetaData` @@ -222,7 +223,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - if create_metadata and create_metadata.name: apps[create_metadata.name] = AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=create_metadata, created_round=app_created_at_round, **(update_metadata or create_metadata).__dict__, @@ -255,7 +256,8 @@ class AppChanges: schema_change_description: str | None -def check_for_app_changes( # noqa: PLR0913 +@deprecated("Deprecated") +def check_for_app_changes( algod_client: "AlgodClient", *, new_approval: bytes, @@ -412,7 +414,7 @@ def check_template_variables(approval_program: str, template_values: TemplateVal logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") -@deprecated(reason="Use `AppManager.replace_template_variables` instead", version="3.0.0") +@deprecated("Use `AppManager.replace_template_variables` instead") def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: """Replaces `TMPL_*` variables in `program` with `template_values` @@ -809,7 +811,7 @@ def _create_metadata( ) -> AppMetaData: return AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=original_metadata or app_spec_note, created_round=created_round, updated_round=updated_round or created_round, diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py index a365a3c1..a556d90f 100644 --- a/src/algokit_utils/_legacy_v2/logic_error.py +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -2,6 +2,8 @@ from copy import copy from typing import TYPE_CHECKING, TypedDict +from typing_extensions import deprecated + from algokit_utils._legacy_v2.models import SimulationTrace if TYPE_CHECKING: @@ -37,8 +39,9 @@ def parse_logic_error( } +@deprecated("Use algokit_utils.models.error.LogicError instead") class LogicError(Exception): - def __init__( # noqa: PLR0913 + def __init__( self, *, logic_error_str: str, @@ -74,9 +77,9 @@ def trace(self, lines: int = 5) -> str: return """ Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map""" + 1. Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2. Set approval_source_map from a previously compiled approval program OR + 3. Import a previously exported source map using import_source_map""" program_lines = copy(self.lines) program_lines[self.line_no] += "\t\t<-- Error" diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index d20bed83..7887cb60 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -9,7 +9,7 @@ SimulateAtomicTransactionResponse, TransactionSigner, ) -from deprecated import deprecated +from typing_extensions import deprecated # Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) @@ -185,17 +185,17 @@ class CreateCallParametersDict(OnCompleteCallParametersDict, total=False): # Pre 1.3.1 backwards compatibility -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class RawTransactionParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class CommonCallParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParametersDict instead", version="1.3.1") +@deprecated("Use TransactionParametersDict instead") class CommonCallParametersDict(TransactionParametersDict): """Deprecated, use TransactionParametersDict instead""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index b1bcc2cb..4d1341b9 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -6,19 +6,20 @@ from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +from typing_extensions import deprecated __all__ = [ "AlgoClientConfig", + "AlgoClientConfigs", "get_algod_client", "get_algonode_config", "get_default_localnet_config", "get_indexer_client", + "get_kmd_client", "get_kmd_client_from_algod_client", "is_localnet", "is_mainnet", "is_testnet", - "AlgoClientConfigs", - "get_kmd_client", ] @@ -40,12 +41,14 @@ class AlgoClientConfigs: kmd_config: AlgoClientConfig | None +@deprecated("Use AlgorandClient.client.algod") def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: """Returns the client configuration to point to the default LocalNet""" port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) +@deprecated("Use AlgorandClient.client.test_net() or AlgorandClient.main_net() instead") def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str ) -> AlgoClientConfig: @@ -56,6 +59,7 @@ def get_algonode_config( ) +@deprecated("Use AlgorandClient.client.from_environment() instead. Example: client = AlgorandClient.from_environment()") def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment @@ -65,6 +69,7 @@ def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: return AlgodClient(config.token, config.server, headers) +@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment @@ -73,6 +78,7 @@ def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: return KMDClient(config.token, config.server) +@deprecated("Use AlgorandClient.client.from_environment().indexer instead") def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. @@ -82,24 +88,28 @@ def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: return IndexerClient(config.token, config.server, headers) +@deprecated("Use AlgorandClient.client.is_local_net() instead") def is_localnet(client: AlgodClient) -> bool: """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" params = client.suggested_params() return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] +@deprecated("Use AlgorandClient.client.is_main_net() instead") def is_mainnet(client: AlgodClient) -> bool: """Returns True if client genesis is `mainnet-v1`""" params = client.suggested_params() return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] +@deprecated("Use AlgorandClient.client.is_test_net() instead") def is_testnet(client: AlgodClient) -> bool: """Returns True if client genesis is `testnet-v1`""" params = client.suggested_params() return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] +@deprecated("Use AlgorandClient.client.default_local_net().kmd instead") def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index d4d95d19..d997a211 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -1,19 +1,44 @@ +import os from collections.abc import Callable from dataclasses import dataclass from typing import Any -from algosdk.account import generate_account +from algosdk import mnemonic from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.mnemonic import to_private_key +from algosdk.transaction import SuggestedParams from typing_extensions import Self -from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.clients.dispenser_api_client import DispenserAssetName, TestNetDispenserApiClient +from algokit_utils.config import config +from algokit_utils.models.account import DISPENSER_ACCOUNT_NAME, Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + PaymentParams, + SendAtomicTransactionComposerResults, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import SendSingleTransactionResult +logger = config.logger -@dataclass -class AddressAndSigner: - address: str - signer: TransactionSigner + +@dataclass(frozen=True, kw_only=True) +class _CommonEnsureFundedParams: + transaction_id: str + amount_funded: AlgoAmount + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedResponse(SendSingleTransactionResult, _CommonEnsureFundedParams): + pass + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedFromTestnetDispenserApiResponse(_CommonEnsureFundedParams): + pass class AccountManager: @@ -26,14 +51,15 @@ def __init__(self, client_manager: ClientManager): :param client_manager: The ClientManager client to use for algod and kmd clients """ self._client_manager = client_manager - self._accounts = dict[str, TransactionSigner]() + self._kmd_account_manager = KmdAccountManager(client_manager) + self._accounts = dict[str, Account]() self._default_signer: TransactionSigner | None = None def set_default_signer(self, signer: TransactionSigner) -> Self: """ Sets the default signer to use if no other signer is specified. - :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount` + :param signer: The signer to use :return: The `AccountManager` so method calls can be chained """ self._default_signer = signer @@ -47,10 +73,17 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: :param signer: The signer to sign transactions with for the given sender :return: The AccountCreator instance for method chaining """ - self._accounts[sender] = signer + if isinstance(signer, AccountTransactionSigner): + self._accounts[sender] = Account(private_key=signer.private_key) return self - def get_signer(self, sender: str) -> TransactionSigner: + def get_account(self, sender: str) -> Account: + account = self._accounts.get(sender) + if not account: + raise ValueError(f"No account found for address {sender}") + return account + + def get_signer(self, sender: str | Account) -> TransactionSigner: """ Returns the `TransactionSigner` for the given sender address. @@ -59,82 +92,400 @@ def get_signer(self, sender: str) -> TransactionSigner: :param sender: The sender address :return: The `TransactionSigner` or throws an error if not found """ - signer = self._accounts.get(sender, None) or self._default_signer + account = self._accounts.get(self._get_address(sender)) + signer = account.signer if account else self._default_signer if not signer: raise ValueError(f"No signer found for address {sender}") return signer - def get_information(self, sender: str) -> dict[str, Any]: + def get_information(self, sender: str | Account) -> dict[str, Any]: """ Returns the given sender account's current status, balance and spendable amounts. - Example: - address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" - account_info = account.get_information(address) - - `Response data schema details `_ - :param sender: The address of the sender/account to look up :return: The account information """ - info = self._client_manager.algod.account_info(sender) + info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) return info - def get_asset_information(self, sender: str, asset_id: int) -> dict[str, Any]: - info = self._client_manager.algod.account_asset_info(sender, asset_id) - assert isinstance(info, dict) - return info + def from_mnemonic(self, mnemonic: str) -> Account: + private_key = to_private_key(mnemonic) + account = Account(private_key=private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) + return account + + def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Account: + account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") + + if account_mnemonic: + private_key = mnemonic.to_private_key(account_mnemonic) + account = Account(private_key=private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=private_key)) + return account + + if self._client_manager.is_local_net(): + kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with) + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account + + raise ValueError(f"Missing environment variable {name.upper()}_MNEMONIC when looking for account {name}") def from_kmd( - self, - name: str, - predicate: Callable[[dict[str, Any]], bool] | None = None, - ) -> AddressAndSigner: - account = get_kmd_wallet_account( - name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd - ) - if not account: + self, name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None + ) -> Account: + kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender) + if not kmd_account: raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}") - self.set_signer(account.address, account.signer) - return AddressAndSigner(address=account.address, signer=account.signer) + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account - def random(self) -> AddressAndSigner: + def rekeyed(self, sender: Account | str, account: Account) -> Account: + sender_address = sender.address if isinstance(sender, Account) else sender + self._accounts[sender_address] = account + return Account(address=sender_address, private_key=account.private_key) + + def rekey_account( # noqa: PLR0913 + self, + account: str | Account, + rekey_to: str | Account, + *, + # Common transaction parameters + signer: TransactionSigner | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + suppress_log: bool | None = None, + ) -> SendAtomicTransactionComposerResults: + """Rekey an account to a new address. + + Args: + account: The account to rekey + rekey_to: The address or account to rekey to + signer: Optional transaction signer + note: Optional transaction note + lease: Optional transaction lease + static_fee: Optional static fee + extra_fee: Optional extra fee + max_fee: Optional max fee + validity_window: Optional validity window + first_valid_round: Optional first valid round + last_valid_round: Optional last valid round + suppress_log: Optional flag to suppress logging + + Returns: + The transaction result """ - Tracks and returns a new, random Algorand account with secret key loaded. + sender_address = self._get_address(account) + rekey_address = self._get_address(rekey_to) + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=sender_address, + receiver=sender_address, + amount=AlgoAmount.from_micro_algo(0), + rekey_to=rekey_address, + signer=signer, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + suppress_log=suppress_log, + ) + ) + .send() + ) + + # If rekey_to is a signing account, set it as the signer for this account + if isinstance(rekey_to, Account): + self.rekeyed(account, rekey_to) + + if not suppress_log: + logger.info(f"Rekeyed {account} to {rekey_to} via transaction {result.tx_ids[-1]}") + + return result - Example: - account = account.random() + def random(self) -> Account: + """ + Tracks and returns a new, random Algorand account. :return: The account """ - (sk, addr) = generate_account() - signer = AccountTransactionSigner(sk) + account = Account.new_account() + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=account.private_key)) + return account + + def localnet_dispenser(self) -> Account: + kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() + account = Account(private_key=kmd_account.private_key) + self._accounts[account.address] = account + self.set_signer(account.address, AccountTransactionSigner(private_key=kmd_account.private_key)) + return account + + def dispenser_from_environment(self) -> Account: + name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") + if name: + return self.from_environment(DISPENSER_ACCOUNT_NAME) + return self.localnet_dispenser() + + def ensure_funded( # noqa: PLR0913 + self, + account_to_fund: str | Account, + dispenser_account: str | Account, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + # Sender params + max_rounds_to_wait: int | None = None, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + # Common txn params + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + ) -> EnsureFundedResponse | None: + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self._get_address(dispenser_account) + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=dispenser_account, + receiver=account_to_fund, + amount=amount_funded, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + ) + .send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + ) + ) - self.set_signer(addr, signer) + return EnsureFundedResponse( + returns=result.returns, + transactions=result.transactions, + confirmations=result.confirmations, + tx_ids=result.tx_ids, + group_id=result.group_id, + transaction_id=result.tx_ids[0], + confirmation=result.confirmations[0], + transaction=result.transactions[0], + amount_funded=amount_funded, + ) + + def ensure_funded_from_environment( # noqa: PLR0913 + self, + account_to_fund: str | Account, + min_spending_balance: AlgoAmount, + *, # Force remaining params to be keyword-only + min_funding_increment: AlgoAmount | None = None, + # SendParams + max_rounds_to_wait: int | None = None, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + # Common transaction params (omitting sender) + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + ) -> EnsureFundedResponse | None: + """Ensure an account is funded from a dispenser account configured in environment. + + Args: + account_to_fund: Address of account to fund + min_spending_balance: Minimum spending balance to ensure + min_funding_increment: Optional minimum funding increment + max_rounds_to_wait: Optional maximum rounds to wait for transaction + suppress_log: Optional flag to suppress logging + populate_app_call_resources: Optional flag to populate app call resources + signer: Optional transaction signer + rekey_to: Optional rekey address + note: Optional transaction note + lease: Optional transaction lease + static_fee: Optional static fee + extra_fee: Optional extra fee + max_fee: Optional maximum fee + validity_window: Optional validity window + first_valid_round: Optional first valid round + last_valid_round: Optional last valid round + + Returns: + EnsureFundedResponse if funding was needed, None otherwise + """ + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self.dispenser_from_environment() + + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=dispenser_account.address, + receiver=account_to_fund, + amount=amount_funded, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + ) + .send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + ) + ) - return AddressAndSigner(address=addr, signer=signer) + return EnsureFundedResponse( + returns=result.returns, + transactions=result.transactions, + confirmations=result.confirmations, + tx_ids=result.tx_ids, + group_id=result.group_id, + transaction_id=result.tx_ids[0], + confirmation=result.confirmations[0], + transaction=result.transactions[0], + amount_funded=amount_funded, + ) - def dispenser(self) -> AddressAndSigner: + def ensure_funded_from_testnet_dispenser_api( + self, + account_to_fund: str | Account, + dispenser_client: TestNetDispenserApiClient, + min_spending_balance: AlgoAmount, + *, # Force remaining params to be keyword-only + min_funding_increment: AlgoAmount | None = None, + ) -> EnsureFundedFromTestnetDispenserApiResponse | None: + """Ensure an account is funded using the TestNet Dispenser API. + + Args: + account_to_fund: Address of account to fund + dispenser_client: Instance of TestNetDispenserApiClient to use for funding + min_spending_balance: Minimum spending balance to ensure + min_funding_increment: Optional minimum funding increment + + Returns: + EnsureFundedResponse if funding was needed, None otherwise + + Raises: + ValueError: If attempting to fund on non-TestNet network """ - Returns an account (with private key loaded) that can act as a dispenser. + account_to_fund = self._get_address(account_to_fund) - Example: - account = account.dispenser() + if not self._client_manager.is_test_net(): + raise ValueError("Attempt to fund using TestNet dispenser API on non TestNet network.") - If running on LocalNet then it will return the default dispenser account automatically, - otherwise it will load the account mnemonic stored in os.environ['DISPENSER_MNEMONIC']. + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) - :return: The account - """ - acct = get_dispenser_account(self._client_manager.algod) + if not amount_funded: + return None - self.set_signer(acct.address, acct.signer) + result = dispenser_client.fund( + address=account_to_fund, + amount=amount_funded.micro_algo, + asset_id=DispenserAssetName.ALGO, + ) + + return EnsureFundedFromTestnetDispenserApiResponse( + transaction_id=result.tx_id, + amount_funded=AlgoAmount.from_micro_algo(result.amount), + ) + + def _get_address(self, sender: str | Account) -> str: + return sender.address if isinstance(sender, Account) else sender + + def _get_composer(self, get_suggested_params: Callable[[], SuggestedParams] | None = None) -> TransactionComposer: + if get_suggested_params is None: + + def _get_suggested_params() -> SuggestedParams: + return self._client_manager.algod.suggested_params() - return AddressAndSigner(address=acct.address, signer=acct.signer) + get_suggested_params = _get_suggested_params + + return TransactionComposer( + algod=self._client_manager.algod, get_signer=self.get_signer, get_suggested_params=get_suggested_params + ) + + def _calculate_fund_amount( + self, + min_spending_balance: int, + current_spending_balance: int, + min_funding_increment: int, + ) -> int | None: + if min_spending_balance > current_spending_balance: + min_fund_amount = min_spending_balance - current_spending_balance + return max(min_fund_amount, min_funding_increment) + return None + + def _get_ensure_funded_amount( + self, + sender: str, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + ) -> AlgoAmount | None: + account_info = self.get_information(sender) + current_spending_balance = account_info["amount"] - account_info["min-balance"] + + min_increment = min_funding_increment.micro_algo if min_funding_increment else 0 + amount_funded = self._calculate_fund_amount( + min_spending_balance.micro_algo, current_spending_balance, min_increment + ) - def localnet_dispenser(self) -> AddressAndSigner: - acct = get_localnet_default_account(self._client_manager.algod) - self.set_signer(acct.address, acct.signer) - return AddressAndSigner(address=acct.address, signer=acct.signer) + return AlgoAmount.from_micro_algo(amount_funded) if amount_funded is not None else None diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py new file mode 100644 index 00000000..6ac08c2d --- /dev/null +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -0,0 +1,190 @@ +from collections.abc import Callable +from typing import Any, cast + +from algosdk.kmd import KMDClient + +from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.config import config +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer + +logger = config.logger + + +class KmdAccount(Account): + """Account retrieved from KMD with signing capabilities, extending base Account""" + + def __init__(self, private_key: str, address: str | None = None) -> None: + """Initialize KMD account with private key and optional address override + + Args: + private_key: Base64 encoded private key + address: Optional address override (for rekeyed accounts) + """ + super().__init__(private_key=private_key, address=address or "") + + +class KmdAccountManager: + """Provides abstractions over KMD that makes it easier to get and manage accounts.""" + + _kmd: KMDClient | None + + def __init__(self, client_manager: ClientManager) -> None: + """Create a new KMD manager. + + Args: + client_manager: ClientManager to use for account management + """ + self._client_manager = client_manager + try: + self._kmd = client_manager.kmd + except ValueError: + self._kmd = None + + def kmd(self) -> KMDClient: + """Get the KMD client, initializing it if needed. + + Returns: + KMDClient: The initialized KMD client + + Raises: + Exception: If KMD is not configured + """ + if self._kmd is None: + if self._client_manager.is_local_net(): + kmd_config = ClientManager.get_config_from_environment_or_localnet() + self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config) + return self._kmd + raise Exception("Attempt to use KMD client with no KMD configured") + return self._kmd + + def get_wallet_account( + self, + wallet_name: str, + predicate: Callable[[dict[str, Any]], bool] | None = None, + sender: str | None = None, + ) -> KmdAccount | None: + """Returns an Algorand signing account with private key loaded from the given KMD wallet. + + Args: + wallet_name: The name of the wallet to retrieve an account from + predicate: Optional filter to use to find the account (otherwise returns a random account from the wallet) + sender: Optional sender address to use this signer for (aka a rekeyed account) + + Returns: + Optional[KmdAccount]: The signing account or None if no matching wallet or account was found + + Example: + ```python + # Get default funded account in a LocalNet + default_dispenser = kmd_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 + ) + ``` + """ + kmd_client = self.kmd() + wallets = kmd_client.list_wallets() + wallet = next((w for w in wallets if w["name"] == wallet_name), None) + if not wallet: + return None + + wallet_id = wallet["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + addresses = kmd_client.list_keys(wallet_handle) + + matched_address = None + if predicate: + for address in addresses: + account_info = self._client_manager.algod.account_info(address) + if predicate(cast(dict[str, Any], account_info)): + matched_address = address + break + else: + matched_address = next(iter(addresses), None) + + if not matched_address: + return None + + private_key = kmd_client.export_key(wallet_handle, "", matched_address) + return KmdAccount(private_key=private_key, address=sender) + + def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount: + """Gets or creates a funded account in a KMD wallet of the given name. + + This is useful to get idempotent accounts from LocalNet without having to specify the private key + (which will change when resetting the LocalNet). + + Args: + name: The name of the wallet to retrieve / create + fund_with: The number of Algos to fund the account with when created (default: 1000) + + Returns: + KmdAccount: An Algorand account with private key loaded + + Example: + ```python + # Idempotently get (if exists) or create (if doesn't exist) an account by name using KMD + # if creating it then fund it with 2 ALGO from the default dispenser account + new_account = kmd_manager.get_or_create_wallet_account("account1", 2) + # This will return the same account as above since the name matches + existing_account = kmd_manager.get_or_create_wallet_account("account1") + ``` + """ + existing = self.get_wallet_account(name) + if existing: + return existing + + kmd_client = self.kmd() + wallet_id = kmd_client.create_wallet(name, "")["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + kmd_client.generate_key(wallet_handle) + + account = self.get_wallet_account(name) + assert account is not None + + logger.info( + f"LocalNet account '{name}' doesn't yet exist; created account {account.address} " + f"with keys stored in KMD and funding with {fund_with} ALGO" + ) + + dispenser = self.get_localnet_dispenser_account() + TransactionComposer( + algod=self._client_manager.algod, + get_signer=lambda _: dispenser.signer, + get_suggested_params=self._client_manager.algod.suggested_params, + ).add_payment( + PaymentParams( + sender=dispenser.address, + receiver=account.address, + amount=fund_with or AlgoAmount.from_algo(1000), + ) + ).send() + return account + + def get_localnet_dispenser_account(self) -> KmdAccount: + """Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + + Returns: + KmdAccount: The default LocalNet dispenser account + + Raises: + Exception: If not running against LocalNet or dispenser account not found + + Example: + ```python + dispenser = kmd_manager.get_localnet_dispenser_account() + ``` + """ + if not self._client_manager.is_local_net(): + raise Exception("Can't get LocalNet dispenser account from non LocalNet network") + + dispenser = self.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000, # noqa: PLR2004 + ) + if not dispenser: + raise Exception("Error retrieving LocalNet dispenser account; couldn't find the default account in KMD") + + return dispenser diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py new file mode 100644 index 00000000..4c71d171 --- /dev/null +++ b/src/algokit_utils/applications/app_client.py @@ -0,0 +1,1396 @@ +from __future__ import annotations + +import base64 +import copy +import json +import os +from dataclasses import dataclass, fields +from typing import TYPE_CHECKING, Any, Protocol, TypeVar + +import algosdk +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_manager import BoxABIValue, BoxName, BoxValue +from algokit_utils.applications.utils import ( + get_abi_decoded_value, + get_abi_encoded_value, + get_abi_tuple_from_abi_struct, + get_arc56_method, +) +from algokit_utils.errors.logic_error import LogicError, parse_logic_error +from algokit_utils.models.application import ( + AppState, + Arc56Contract, + CompiledTeal, + ProgramSourceInfo, + SourceInfoDetail, + StorageKey, + StorageMap, +) +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, + AppDeleteMethodCall, + AppMethodCallTransactionArgument, + AppUpdateMethodCall, + AppUpdateParams, + BuiltTransactions, + PaymentParams, +) +from algokit_utils.transactions.transaction_sender import SendAppTransactionResult, SendSingleTransactionResult + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.atomic_transaction_composer import TransactionSigner + + from algokit_utils.applications.app_manager import ( + AppManager, + BoxIdentifier, + BoxReference, + TealTemplateParams, + ) + from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue + from algokit_utils.models.amount import AlgoAmount + from algokit_utils.protocols.application import AlgorandClientProtocol + from algokit_utils.transactions.transaction_composer import TransactionComposer + +# TEAL opcodes for constant blocks +BYTE_CBLOCK = 38 # bytecblock opcode +INT_CBLOCK = 32 # intcblock opcode + +T = TypeVar("T") # For generic return type in _handle_call_errors + + +def get_constant_block_offset(program: bytes) -> int: # noqa: C901 + """Calculate the offset after constant blocks in TEAL program. + + Args: + program: The compiled TEAL program bytes + + Returns: + The maximum offset after bytecblock/intcblock operations + """ + bytes_list = list(program) + program_size = len(bytes_list) + + # Remove version byte + bytes_list.pop(0) + + # Track offsets + bytecblock_offset: int | None = None + intcblock_offset: int | None = None + + while bytes_list: + # Get current byte + byte = bytes_list.pop(0) + + # Check if byte is a constant block opcode + if byte in (BYTE_CBLOCK, INT_CBLOCK): + is_bytecblock = byte == BYTE_CBLOCK + + # Get number of values in constant block + if not bytes_list: + break + values_remaining = bytes_list.pop(0) + + # Process each value in the block + for _ in range(values_remaining): + if is_bytecblock: + # For bytecblock, next byte is length of element + if not bytes_list: + break + length = bytes_list.pop(0) + # Remove the bytes for this element + bytes_list = bytes_list[length:] + else: + # For intcblock, read until we find end of uvarint (MSB not set) + while bytes_list: + byte = bytes_list.pop(0) + if not (byte & 0x80): # Check if MSB is not set + break + + # Update appropriate offset + if is_bytecblock: + bytecblock_offset = program_size - len(bytes_list) - 1 + else: + intcblock_offset = program_size - len(bytes_list) - 1 + + # If next byte isn't a constant block opcode, we're done + if not bytes_list or bytes_list[0] not in (BYTE_CBLOCK, INT_CBLOCK): + break + + # Return maximum offset + return max(bytecblock_offset or 0, intcblock_offset or 0) + + +@dataclass(kw_only=True, frozen=True) +class AppClientCompilationParams: + deploy_time_params: TealTemplateParams | None = None + updatable: bool | None = None + deletable: bool | None = None + + +@dataclass(kw_only=True, frozen=True) +class ExposedLogicErrorDetails: + is_clear_state_program: bool = False + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + program: bytes | None = None + approval_source_info: ProgramSourceInfo | None = None + clear_source_info: ProgramSourceInfo | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientParams: + """Full parameters for creating an app client""" + + app_spec: ( + Arc56Contract | ApplicationSpecification | str + ) # Using string quotes since these types may be defined elsewhere + algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere + app_id: int + app_name: str | None = None + default_sender: str | bytes | None = None # Address can be string or bytes + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientCompilationResult: + approval_program: bytes + clear_state_program: bytes + compiled_approval: CompiledTeal | None = None + compiled_clear: CompiledTeal | None = None + + +@dataclass(kw_only=True, frozen=True) +class CommonTxnParams: + sender: str + signer: TransactionSigner | None = None + rekey_to: str | None = None + note: bytes | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + + +@dataclass(kw_only=True) +class FundAppAccountParams: + sender: str | None = None + signer: TransactionSigner | None = None + rekey_to: str | None = None + note: bytes | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + amount: AlgoAmount + close_remainder_to: str | None = None + max_rounds_to_wait: int | None = None + suppress_log: bool | None = None + populate_app_call_resources: bool | None = None + on_complete: algosdk.transaction.OnComplete | None = None + + +@dataclass(kw_only=True) +class AppClientCallParams: + method: str | None = None # If calling ABI method, name or signature + args: list | None = None # Arguments to pass to the method + boxes: list | None = None # Box references to load + accounts: list[str] | None = None # Account addresses to load + apps: list[int] | None = None # App IDs to load + assets: list[int] | None = None # Asset IDs to load + lease: (str | bytes) | None = None # Optional lease + sender: str | None = None # Optional sender account + note: (bytes | dict | str) | None = None # Transaction note + send_params: dict | None = None # Parameters to control transaction sending + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallParams: + method: str + args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + extra_fee: AlgoAmount | None = None + first_valid_round: int | None = None + lease: bytes | None = None + max_fee: AlgoAmount | None = None + note: bytes | None = None + rekey_to: str | None = None + sender: str | None = None + signer: TransactionSigner | None = None + static_fee: AlgoAmount | None = None + validity_window: int | None = None + last_valid_round: int | None = None + on_complete: algosdk.transaction.OnComplete | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithCompilationParams(AppClientMethodCallParams, AppClientCompilationParams): + """Combined parameters for method calls with compilation""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithSendParams(AppClientMethodCallParams, SendParams): + """Combined parameters for method calls with send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientMethodCallWithCompilationAndSendParams( + AppClientMethodCallParams, AppClientCompilationParams, SendParams +): + """Combined parameters for method calls with compilation and send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallParams: + signer: TransactionSigner | None = None + rekey_to: str | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + sender: str | None = None + note: bytes | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + + +@dataclass(kw_only=True, frozen=True) +class CallOnComplete: + on_complete: algosdk.transaction.OnComplete + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCompilationParams(AppClientBareCallParams, AppClientCompilationParams): + """Combined parameters for bare calls with compilation""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithSendParams(AppClientBareCallParams, SendParams): + """Combined parameters for bare calls with send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, AppClientCompilationParams, SendParams): + """Combined parameters for bare calls with compilation and send options""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, CallOnComplete): + """Combined parameters for bare calls with an OnComplete value""" + + +@dataclass(kw_only=True, frozen=True) +class ResolveAppClientByNetwork: + app_spec: Arc56Contract | ApplicationSpecification | str + algorand: AlgorandClientProtocol + app_name: str | None = None + default_sender: str | bytes | None = None + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppSourceMaps: + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +class _AppClientStateMethodsProtocol(Protocol): + def get_all(self) -> dict[str, Any]: ... + + def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: ... + + def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: ... # noqa: ANN401 + + def get_map(self, map_name: str) -> dict[str, ABIValue]: ... + + +class _AppClientStateMethods(_AppClientStateMethodsProtocol): + def __init__( + self, + *, + get_all: Callable[[], dict[str, Any]], + get_value: Callable[[str, dict[str, AppState] | None], ABIValue | None], + get_map_value: Callable[[str, bytes | Any, dict[str, AppState] | None], Any], + get_map: Callable[[str], dict[str, ABIValue]], + ) -> None: + self._get_all = get_all + self._get_value = get_value + self._get_map_value = get_map_value + self._get_map = get_map + + def get_all(self) -> dict[str, Any]: + return self._get_all() + + def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: + return self._get_value(name, app_state) + + def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 + return self._get_map_value(map_name, key, app_state) + + def get_map(self, map_name: str) -> dict[str, ABIValue]: + return self._get_map(map_name) + + +class _AppClientStateAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def local_state(self, address: str) -> _AppClientStateMethodsProtocol: + """Methods to access local state for the current app for a given address""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address), + key_getter=lambda: self._app_spec.state.keys.get("local", {}), + map_getter=lambda: self._app_spec.state.maps.get("local", {}), + ) + + @property + def global_state(self) -> _AppClientStateMethodsProtocol: + """Methods to access global state for the current app""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_global_state(self._app_id), + key_getter=lambda: self._app_spec.state.keys.get("global", {}), + map_getter=lambda: self._app_spec.state.maps.get("global", {}), + ) + + # @property + # def box(self) -> AppClientStateMethods: + # """Methods to access box storage for the current app""" + # return self._get_state_methods( + # state_getter=lambda: self._algorand.app.get_box_state(self._app_id), + # key_getter=lambda: self._app_spec.state.keys.get("box", {}), + # map_getter=lambda: self._app_spec.state.maps.get("box", {}), + # ) + + def _get_state_methods( # noqa: C901 + self, + state_getter: Callable[[], dict[str, AppState]], + key_getter: Callable[[], dict[str, StorageKey]], + map_getter: Callable[[], dict[str, StorageMap]], + ) -> _AppClientStateMethodsProtocol: + def get_all() -> dict[str, Any]: + state = state_getter() + keys = key_getter() + return {key: get_value(key, state) for key in keys} + + def get_value(name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None: + state = app_state or state_getter() + key_info = key_getter()[name] + value = next((s for s in state.values() if s.key_base64 == key_info.key), None) + + if value and value.value_raw: + return get_abi_decoded_value(value.value_raw, key_info.value_type, self._app_spec.structs) + + return None + + def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 + state = app_state or state_getter() + metadata = map_getter()[map_name] + + prefix = bytes(metadata.prefix or "", "base64") + encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) + full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") + value = next((s for s in state.values() if s.key_base64 == full_key), None) + if value and value.value_raw: + return get_abi_decoded_value(value.value_raw, metadata.value_type, self._app_spec.structs) + return None + + def get_map(map_name: str) -> dict[str, ABIValue]: + state = state_getter() + metadata = map_getter()[map_name] + + prefix = metadata.prefix or "" + + prefixed_state = {k: v for k, v in state.items() if k.startswith(prefix)} + + decoded_map = {} + + for key_encoded, value in prefixed_state.items(): + key_bytes = key_encoded[len(prefix) :] + try: + decoded_key = get_abi_decoded_value(key_bytes, metadata.key_type, self._app_spec.structs) + except Exception as e: + raise ValueError(f"Failed to decode key {key_encoded}") from e + + try: + if value and value.value_raw: + decoded_value = get_abi_decoded_value( + value.value_raw, metadata.value_type, self._app_spec.structs + ) + else: + decoded_value = get_abi_decoded_value(value.value, metadata.value_type, self._app_spec.structs) + except Exception as e: + raise ValueError(f"Failed to decode value {value}") from e + + decoded_map[str(decoded_key)] = decoded_value + + return decoded_map + + return _AppClientStateMethods( + get_all=get_all, + get_value=get_value, + get_map_value=get_map_value, + get_map=get_map, + ) + + def get_local_state(self, address: str) -> dict[str, AppState]: + return self._algorand.app.get_local_state(self._app_id, address) + + def get_global_state(self) -> dict[str, AppState]: + return self._algorand.app.get_global_state(self._app_id) + + +class _AppClientBareParamsAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def _get_bare_params( + self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete + ) -> dict[str, Any]: + """Get bare parameters for application calls. + + Args: + params: The parameters to process + on_complete: The OnComplete value for the transaction + + Returns: + The processed parameters with defaults filled in + """ + params = params or {} + sender = self._client._get_sender(params.get("sender")) + return { + **params, + "app_id": self._app_id, + "sender": sender, + "signer": self._client._get_signer(params.get("sender"), params.get("signer")), + "on_complete": on_complete, + } + + def update(self, params: AppClientBareCallWithCompilationAndSendParams | None = None) -> AppUpdateParams: + call_params: AppUpdateParams = AppUpdateParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.UpdateApplicationOC) + ) + return call_params + + def opt_in(self, params: AppClientBareCallWithSendParams | None = None) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.OptInOC)) + return call_params + + def delete(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__, OnComplete.DeleteApplicationOC) + ) + return call_params + + def clear_state(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.ClearStateOC)) + return call_params + + def close_out(self, params: AppClientBareCallWithSendParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.CloseOutOC)) + return call_params + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> AppCallParams: + call_params: AppCallParams = AppCallParams(**self._get_bare_params(params.__dict__, OnComplete.NoOpOC)) + return call_params + + +class _AppClientMethodCallParamsAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_params_accessor = _AppClientBareParamsAccessor(client) + + @property + def bare(self) -> _AppClientBareParamsAccessor: + return self._bare_params_accessor + + def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: + def random_note() -> bytes: + return base64.b64encode(os.urandom(16)) + + return PaymentParams( + sender=self._client._get_sender(params.sender), + signer=self._client._get_signer(params.sender, params.signer), + receiver=self._client.app_address, + amount=params.amount, + rekey_to=params.rekey_to, + note=params.note or random_note(), + lease=params.lease, + static_fee=params.static_fee, + extra_fee=params.extra_fee, + max_fee=params.max_fee, + validity_window=params.validity_window, + first_valid_round=params.first_valid_round, + last_valid_round=params.last_valid_round, + close_remainder_to=params.close_remainder_to, + ) + + def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) + return AppCallMethodCall(**input_params) + + def call(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) + return AppCallMethodCall(**input_params) + + def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + input_params = self._get_abi_params( + params.__dict__, on_complete=algosdk.transaction.OnComplete.DeleteApplicationOC + ) + return AppDeleteMethodCall(**input_params) + + def update( + self, params: AppClientMethodCallParams | AppClientMethodCallWithCompilationAndSendParams + ) -> AppUpdateMethodCall: + compile_params = ( + self._client.compile( + app_spec=self._client.app_spec, + app_manager=self._algorand.app, + deploy_time_params=params.deploy_time_params, + updatable=params.updatable, + deletable=params.deletable, + ).__dict__ + if isinstance(params, AppClientMethodCallWithCompilationAndSendParams) + else {} + ) + + input_params = { + **self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.UpdateApplicationOC), + **compile_params, + } + # Filter input_params to include only fields valid for AppUpdateMethodCall + app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCall)} + filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields} + return AppUpdateMethodCall(**filtered_input_params) + + def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.CloseOutOC) + return AppCallMethodCall(**input_params) + + def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + input_params = copy.deepcopy(params) + + input_params["app_id"] = self._app_id + input_params["on_complete"] = on_complete + + input_params["sender"] = self._client._get_sender(params["sender"]) + input_params["signer"] = self._client._get_signer(params["sender"], params["signer"]) + + if params.get("method"): + input_params["method"] = get_arc56_method(params["method"], self._app_spec) + if params.get("args"): + input_params["args"] = self._client._get_abi_args_with_default_values( + method_name_or_signature=params["method"], + args=params["args"], + sender=self._client._get_sender(input_params["sender"]), + ) + + return input_params + + +class _AppClientBareCreateTransactionMethods: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + + def update(self, params: AppClientBareCallWithCompilationAndSendParams) -> Transaction: + return self._algorand.create_transaction.app_update(self._client.params.bare.update(params)) + + def opt_in(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.opt_in(params)) + + def delete(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.delete(params)) + + def clear_state(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.clear_state(params)) + + def close_out(self, params: AppClientBareCallWithSendParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.close_out(params)) + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> Transaction: + return self._algorand.create_transaction.app_call(self._client.params.bare.call(params)) + + +class _AppClientMethodCallTransactionCreator: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_create_transaction_methods = _AppClientBareCreateTransactionMethods(client) + + @property + def bare(self) -> _AppClientBareCreateTransactionMethods: + return self._bare_create_transaction_methods + + def fund_app_account(self, params: FundAppAccountParams) -> Transaction: + return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params)) + + def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params)) + + def update(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params)) + + def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params)) + + def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params)) + + def call(self, params: AppClientMethodCallParams) -> BuiltTransactions: + return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params)) + + +class _AppClientBareSendAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + + def update( + self, + params: AppClientBareCallWithCompilationAndSendParams, + ) -> SendAppTransactionResult: + """Send an application update transaction. + + Args: + params: The parameters for the update call + compilation: Optional compilation parameters + max_rounds_to_wait: The maximum number of rounds to wait for confirmation + suppress_log: Whether to suppress log output + populate_app_call_resources: Whether to populate app call resources + + Returns: + The result of sending the transaction + """ + compiled = self._client.compile_and_persist_sourcemaps( + params.deploy_time_params, params.updatable, params.deletable + ) + bare_params = self._client.params.bare.update(params) + bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) + bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear) + call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params)) + return SendAppTransactionResult(**{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}) + + def opt_in(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.opt_in(params)) + ) + + def delete(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.delete(params)) + ) + + def clear_state(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.clear_state(params)) + ) + + def close_out(self, params: AppClientBareCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.close_out(params)) + ) + + def call(self, params: AppClientBareCallWithCallOnCompleteParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call(self._client.params.bare.call(params)) + ) + + +class _AppClientSendAccessor: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_send_accessor = _AppClientBareSendAccessor(client) + + @property + def bare(self) -> _AppClientBareSendAccessor: + return self._bare_send_accessor + + def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.payment(self._client.params.fund_app_account(params)) + ) + + def opt_in(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params)) + ) + + def delete(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params)) + ) + + def update(self, params: AppClientMethodCallWithCompilationAndSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_update_method_call(self._client.params.update(params)) + ) + + def close_out(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params)) + ) + + def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionResult: + is_read_only_call = params.on_complete == algosdk.transaction.OnComplete.NoOpOC or ( + not params.on_complete and get_arc56_method(params.method, self._app_spec).method.readonly + ) + + if is_read_only_call: + method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( + self._client.params.call(params) + ) + + simulate_response = self._client._handle_call_errors( + lambda: method_call_to_simulate.simulate( + allow_unnamed_resources=params.populate_app_call_resources or True, + skip_signatures=True, + allow_more_logs=True, + allow_empty_signatures=True, + extra_opcode_budget=None, + exec_trace_config=None, + round=None, + fix_signers=None, # TODO: double check on whether algosdk py even has this param + ) + ) + + return SendAppTransactionResult( + tx_ids=simulate_response.tx_ids, + transactions=simulate_response.transactions, + transaction=simulate_response.transactions[-1], + confirmation=simulate_response.confirmations[-1] if simulate_response.confirmations else b"", + confirmations=simulate_response.confirmations, + group_id=simulate_response.group_id or "", + returns=simulate_response.returns, + return_value=simulate_response.returns[-1], + ) + + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call_method_call(self._client.params.call(params)) + ) + + +class AppClient: + def __init__(self, params: AppClientParams) -> None: + self._app_id = params.app_id + self._app_spec = self.normalise_app_spec(params.app_spec) + self._algorand = params.algorand + self._app_address = algosdk.logic.get_application_address(self._app_id) + self._app_name = params.app_name or self._app_spec.name + self._default_sender = params.default_sender + self._default_signer = params.default_signer + self._approval_source_map = params.approval_source_map + self._clear_source_map = params.clear_source_map + self._state_accessor = _AppClientStateAccessor(self) + self._params_accessor = _AppClientMethodCallParamsAccessor(self) + self._send_accessor = _AppClientSendAccessor(self) + self._create_transaction_accessor = _AppClientMethodCallTransactionCreator(self) + + @property + def app_id(self) -> int: + return self._app_id + + @property + def app_address(self) -> str: + return self._app_address + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def state(self) -> _AppClientStateAccessor: + return self._state_accessor + + @property + def params(self) -> _AppClientMethodCallParamsAccessor: + return self._params_accessor + + @property + def send(self) -> _AppClientSendAccessor: + return self._send_accessor + + @property + def create_transaction(self) -> _AppClientMethodCallTransactionCreator: + return self._create_transaction_accessor + + @staticmethod + def normalise_app_spec(app_spec: Arc56Contract | ApplicationSpecification | str) -> Arc56Contract: + if isinstance(app_spec, str): + spec = json.loads(app_spec) + if "hints" in spec: + spec = ApplicationSpecification.from_json(app_spec) + else: + spec = app_spec + + if isinstance(spec, Arc56Contract): + return spec + + elif isinstance(spec, ApplicationSpecification): + # Convert ARC-32 to ARC-56 + from algokit_utils.applications.utils import arc32_to_arc56 + + return arc32_to_arc56(spec) + elif isinstance(spec, dict): + # normalize field names to lowercase to python camel + return Arc56Contract.from_json(spec) + else: + raise ValueError("Invalid app spec format") + + @staticmethod + def from_network( + app_spec: Arc56Contract | ApplicationSpecification | str, + algorand: AlgorandClientProtocol, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + network = algorand.client.network() + app_spec = AppClient.normalise_app_spec(app_spec) + network_names = [network.genesis_hash] + + if network.is_local_net: + network_names.append("localnet") + if network.is_main_net: + network_names.append("mainnet") + if network.is_test_net: + network_names.append("testnet") + + available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else [] + network_index = next((i for i, n in enumerate(available_app_spec_networks) if n in network_names), None) + + if network_index is None: + raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec") + + app_id = app_spec.networks[available_app_spec_networks[network_index]]["app_id"] # type: ignore[index] + + return AppClient( + AppClientParams( + app_id=app_id, + app_spec=app_spec, + algorand=algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + + @staticmethod + def compile( + app_spec: Arc56Contract, + app_manager: AppManager, + deploy_time_params: TealTemplateParams | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + ) -> AppClientCompilationResult: + def is_base64(s: str) -> bool: + try: + return base64.b64encode(base64.b64decode(s)).decode() == s + except Exception: + return False + + if not app_spec.source: + if not app_spec.byte_code or not app_spec.byte_code.get("approval") or not app_spec.byte_code.get("clear"): + raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code") + + return AppClientCompilationResult( + approval_program=base64.b64decode(app_spec.byte_code.get("approval", "")), + clear_state_program=base64.b64decode(app_spec.byte_code.get("clear", "")), + ) + + approval_source = app_spec.source.get("approval", "") + approval_template: str = ( + base64.b64decode(approval_source).decode("utf-8") if is_base64(approval_source) else approval_source + ) + compiled_approval = app_manager.compile_teal_template( + approval_template, + template_params=deploy_time_params, + deployment_metadata=( + {"updatable": updatable or False, "deletable": deletable or False} + if updatable is not None or deletable is not None + else None + ), + ) + + clear_source = app_spec.source.get("clear", "") + clear_template: str = ( + base64.b64decode(clear_source).decode("utf-8") if is_base64(clear_source) else clear_source + ) + compiled_clear = app_manager.compile_teal_template( + clear_template, + template_params=deploy_time_params, + ) + + # TODO: Add invocation of persisting sourcemaps + return AppClientCompilationResult( + approval_program=compiled_approval.compiled_base64_to_bytes, + compiled_approval=compiled_approval, + clear_state_program=compiled_clear.compiled_base64_to_bytes, + compiled_clear=compiled_clear, + ) + + @staticmethod + def expose_logic_error_static( # noqa: C901 + e: Exception, app_spec: Arc56Contract, details: ExposedLogicErrorDetails + ) -> Exception: + """Takes an error that may include a logic error and re-exposes it with source info.""" + source_map = details.clear_source_map if details.is_clear_state_program else details.approval_source_map + + error_details = parse_logic_error(str(e)) + if not error_details: + return e + + # The PC value to find in the ARC56 SourceInfo + arc56_pc = error_details["pc"] + + program_source_info = ( + details.clear_source_info if details.is_clear_state_program else details.approval_source_info + ) + + # The offset to apply to the PC if using the cblocks pc offset method + cblocks_offset = 0 + + # If the program uses cblocks offset, then we need to adjust the PC accordingly + if program_source_info and program_source_info.pc_offset_method == "cblocks": + if not details.program: + raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset") + + cblocks_offset = get_constant_block_offset(details.program) + arc56_pc = error_details["pc"] - cblocks_offset + + # Find the source info for this PC and get the error message + source_info = None + if program_source_info and program_source_info.source_info: + source_info = next( + (s for s in program_source_info.source_info if isinstance(s, SourceInfoDetail) and arc56_pc in s.pc), + None, + ) + error_message = source_info.error_message if source_info else None + + # If we have the source we can display the TEAL in the error message + if hasattr(app_spec, "source"): + program_source = ( + (app_spec.source.get("clear") if details.is_clear_state_program else app_spec.source.get("approval")) + if app_spec.source + else None + ) + custom_get_line_for_pc = None + + def get_line_for_pc(input_pc: int) -> int | None: + if not program_source_info: + return None + teal = [line.teal for line in program_source_info.source_info if input_pc - cblocks_offset in line.pc] + return teal[0] if teal else None + + if not source_map: + custom_get_line_for_pc = get_line_for_pc + + if program_source: + e = LogicError( + logic_error_str=str(e), + program=program_source, + source_map=source_map, + transaction_id=error_details["transaction_id"], + message=error_details["message"], + pc=error_details["pc"], + logic_error=e, + get_line_for_pc=custom_get_line_for_pc, + traces=None, + ) + + if error_message: + import re + + app_id = re.search(r"(?<=app=)\d+", str(e)) + tx_id = re.search(r"(?<=transaction )\S+(?=:)", str(e)) + error = Exception( + f"Runtime error when executing {app_spec.name} " + f"(appId: {app_id.group() if app_id else ''}) in transaction " + f"{tx_id.group() if tx_id else ''}: {error_message}" + ) + error.__cause__ = e + return error + + return e + + # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' + def compile_and_persist_sourcemaps( + self, + deploy_time_params: TealTemplateParams | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + ) -> AppClientCompilationResult: + result = AppClient.compile(self._app_spec, self._algorand.app, deploy_time_params, updatable, deletable) + + if result.compiled_approval: + self._approval_source_map = result.compiled_approval.source_map + if result.compiled_clear: + self._clear_source_map = result.compiled_clear.source_map + + return result + + def clone( + self, + app_name: str | None = None, + default_sender: str | bytes | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient( + AppClientParams( + app_id=self._app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ) + ) + + def export_source_maps(self) -> AppSourceMaps: + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "you need to call create, update, or deploy first" + ) + + return AppSourceMaps( + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + ) + + def import_source_maps(self, source_maps: AppSourceMaps) -> None: + if not source_maps.approval_source_map: + raise ValueError("Approval source map is required") + if not source_maps.clear_source_map: + raise ValueError("Clear source map is required") + + if not isinstance(source_maps.approval_source_map, dict | SourceMap): + raise ValueError( + "Approval source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`" + ) + if not isinstance(source_maps.clear_source_map, dict | SourceMap): + raise ValueError( + "Clear source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`" + ) + + self._approval_source_map = ( + SourceMap(source_map=source_maps.approval_source_map) + if isinstance(source_maps.approval_source_map, dict) + else source_maps.approval_source_map + ) + self._clear_source_map = ( + SourceMap(source_map=source_maps.clear_source_map) + if isinstance(source_maps.clear_source_map, dict) + else source_maps.clear_source_map + ) + + def get_local_state(self, address: str) -> dict[str, AppState]: + return self._state_accessor.get_local_state(address) + + def get_global_state(self) -> dict[str, AppState]: + return self._state_accessor.get_global_state() + + def get_box_names(self) -> list[BoxName]: + return self._algorand.app.get_box_names(self._app_id) + + def get_box_value(self, name: BoxIdentifier) -> bytes: + return self._algorand.app.get_box_value(self._app_id, name) + + def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type) + + def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]: + names = self.get_box_names() + if filter_func: + names = [name for name in names if filter_func(name)] + + # Get values for filtered names + values = self._algorand.app.get_box_values(self.app_id, [name.name_raw for name in names]) + + # Return list of BoxValue objects + return [BoxValue(name=name, value=values[i]) for i, name in enumerate(names)] + + def get_box_values_from_abi_type( + self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None + ) -> list[BoxABIValue]: + # Get box names and apply filter if provided + names = self.get_box_names() + if filter_func: + names = [name for name in names if filter_func(name)] + + # Get values for filtered names and decode them + values = self._algorand.app.get_box_values_from_abi_type( + self.app_id, [name.name_raw for name in names], abi_type + ) + + # Return list of BoxABIValue objects + return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] + + def new_group(self) -> TransactionComposer: + return self._algorand.new_group() + + def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: + return self.send.fund_app_account(params) + + def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT001, FBT002 + """Takes an error that may include a logic error from a call to the current app and re-exposes the + error to include source code information via the source map and ARC-56 spec. + + Args: + e: The error to parse + is_clear_state_program: Whether the code was running the clear state program (defaults to approval program) + + Returns: + The new error, or if there was no logic error or source map then the wrapped error with source details + """ + + # Get source info based on program type + source_info = None + if hasattr(self._app_spec, "source_info") and self._app_spec.source_info: + source_info = ( + self._app_spec.source_info.get("clear") + if is_clear_state_program + else self._app_spec.source_info.get("approval") + ) + + pc_offset_method = source_info.pc_offset_method if source_info else None + + program: bytes | None = None + if pc_offset_method == "cblocks": + # TODO: Cache this if we deploy the app and it's not updateable + app_info = self._algorand.app.get_by_id(self.app_id) + program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program + + return AppClient.expose_logic_error_static( + e, + self._app_spec, + ExposedLogicErrorDetails( + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=program, + approval_source_info=( + self._app_spec.source_info.get("approval") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + clear_source_info=( + self._app_spec.source_info.get("clear") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + ), + ) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + """Make the given call and catch any errors, augmenting with debugging information before re-throwing.""" + try: + return call() + except Exception as e: + raise self.expose_logic_error(e=e) from None + + def _get_sender(self, sender: str | None) -> str: + if not sender and not self._default_sender: + raise Exception( + f"No sender provided and no default sender present in app client for call to app {self.app_name}" + ) + return sender or self._default_sender # type: ignore[return-value] + + def _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: + return signer or self._default_signer if sender else None + + def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + """Get bare parameters for application calls. + + Args: + params: The parameters to process + on_complete: The OnComplete value for the transaction + + Returns: + The processed parameters with defaults filled in + """ + sender = self._get_sender(params.get("sender")) + return { + **params, + "app_id": self._app_id, + "sender": sender, + "signer": self._get_signer(params.get("sender"), params.get("signer")), + "on_complete": on_complete, + } + + def _get_abi_args_with_default_values( # noqa: C901, PLR0912 + self, + *, + method_name_or_signature: str, + args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, + sender: str, + ) -> list[Any]: + """Get ABI args with default values filled in. + + Args: + method_name_or_signature: Method name or ABI signature + args: Optional list of argument values + sender: Sender address + + Returns: + List of argument values with defaults filled in + + Raises: + ValueError: If required argument is missing or default value lookup fails + """ + method = get_arc56_method(method_name_or_signature, self._app_spec) + result = [] + + for i, method_arg in enumerate(method.arc56_args): + # Get provided arg value if any + arg_value = args[i] if args and i < len(args) else None + + if arg_value is not None: + # Convert struct to tuple if needed + if method_arg.struct and isinstance(arg_value, dict): + arg_value = get_abi_tuple_from_abi_struct( + arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs + ) + result.append(arg_value) + continue + + # Handle default value if arg not provided + default_value = method_arg.default_value + if default_value: + match default_value.source: + case "literal": + value_raw = base64.b64decode(default_value.data) + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) + + case "method": + # Get method return value + default_method = get_arc56_method(default_value.data, self._app_spec) + empty_args = [None] * len(default_method.args) + call_result = self._algorand.send.app_call_method_call( + AppCallMethodCall( + app_id=self._app_id, + method=algosdk.abi.Method.from_signature(default_value.data), + args=empty_args, + sender=sender, + ) + ) + + if not call_result.return_value: + raise ValueError("Default value method call did not return a value") + + if isinstance(call_result.return_value, dict): + # Convert struct return value to tuple + result.append( + get_abi_tuple_from_abi_struct( + call_result.return_value, + self._app_spec.structs[str(default_method.arc56_returns.type)], + self._app_spec.structs, + ) + ) + else: + result.append(call_result.return_value.return_value) + + case "local" | "global": + # Get state value + state = ( + self.get_global_state() + if default_value.source == "global" + else self.get_local_state(sender) + ) + value = next((s for s in state.values() if s.key_base64 == default_value.data), None) + if not value: + raise ValueError( + f"Key '{default_value.data}' not found in {default_value.source} " + f"storage for argument {method_arg.name or f'arg{i+1}'}" + ) + + if value.value_raw: + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(value.value_raw, value_type, self._app_spec.structs)) + else: + result.append(value.value) + + case "box": + # Get box value + box_name = base64.b64decode(default_value.data) + box_value = self._algorand.app.get_box_value(self._app_id, box_name) + value_type = default_value.type or method_arg.type + result.append(get_abi_decoded_value(box_value, value_type, self._app_spec.structs)) + + elif not algosdk.abi.is_abi_transaction_type(method_arg.type): + # Error if required non-txn arg missing + raise ValueError( + f"No value provided for required argument " + f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}" + ) + + return result + + def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + sender = self._get_sender(params.get("sender")) + method = get_arc56_method(params["method"], self._app_spec) + args = self._get_abi_args_with_default_values( + method_name_or_signature=params["method"], args=params.get("args"), sender=sender + ) + return { + **params, + "appId": self._app_id, + "sender": sender, + "signer": self._get_signer(params.get("sender"), params.get("signer")), + "method": method, + "onComplete": on_complete, + "args": args, + } diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py new file mode 100644 index 00000000..357d808c --- /dev/null +++ b/src/algokit_utils/applications/app_deployer.py @@ -0,0 +1,602 @@ +import base64 +import dataclasses +import json +from dataclasses import dataclass +from typing import Literal + +import algosdk +from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner +from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete +from algosdk.v2client.indexer import IndexerClient + +from algokit_utils._legacy_v2.deploy import ( + AppDeployMetaData, + AppLookup, + AppMetaData, + OnSchemaBreak, + OnUpdate, + OperationPerformed, +) +from algokit_utils.applications.app_manager import AppManager, BoxReference, TealTemplateParams +from algokit_utils.config import config +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppDeleteParams, + AppUpdateMethodCall, + AppUpdateParams, +) +from algokit_utils.transactions.transaction_sender import ( + AlgorandClientTransactionSender, +) + +APP_DEPLOY_NOTE_DAPP = "algokit_deployer" + +logger = config.logger + + +@dataclass(kw_only=True) +class DeployAppUpdateParams: + """Parameters for an update transaction in app deployment""" + + sender: str + on_complete: OnComplete = OnComplete.UpdateApplicationOC + signer: TransactionSigner | None = None + args: list[bytes] | None = None + note: bytes | None = None + lease: bytes | None = None + rekey_to: str | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(kw_only=True) +class DeployAppDeleteParams: + """Parameters for a delete transaction in app deployment""" + + sender: str + on_complete: OnComplete = OnComplete.DeleteApplicationOC + signer: TransactionSigner | None = None + note: bytes | None = None + lease: bytes | None = None + rekey_to: str | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None + + +@dataclass(kw_only=True) +class AppDeployParams: + """Parameters for deploying an app""" + + metadata: AppDeployMetaData + deploy_time_params: TealTemplateParams | None = None + on_schema_break: Literal["replace", "fail", "append"] | OnSchemaBreak = OnSchemaBreak.Fail + on_update: Literal["update", "replace", "fail", "append"] | OnUpdate = OnUpdate.Fail + create_params: AppCreateParams | AppCreateMethodCall + update_params: DeployAppUpdateParams | AppUpdateMethodCall + delete_params: DeployAppDeleteParams | AppDeleteMethodCall + existing_deployments: AppLookup | None = None + ignore_cache: bool = False + max_fee: int | None = None + max_rounds_to_wait: int | None = None + suppress_log: bool = False + + +@dataclass(kw_only=True, frozen=True) +class ConfirmedTransactionResult: + transaction: TransactionWrapper + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppDeployResult: + operation_performed: OperationPerformed + + # Common fields from AppMetadata + name: str + version: str + created_round: int + updated_round: int + deleted: bool + created_metadata: dict + deletable: bool | None = None + updatable: bool | None = None + + app_id: int | None = None + app_address: str | None = None + transaction: TransactionWrapper | None = None + tx_id: str | None = None + transactions: list[TransactionWrapper] | None = None + tx_ids: list[str] | None = None + confirmation: algosdk.v2client.algod.AlgodResponseType | None = None + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + compiled_approval: dict | None = None + compiled_clear: dict | None = None + return_value: ABIResult | None = None + delete_return_value: ABIResult | None = None + delete_result: ConfirmedTransactionResult | None = None + + +class AppDeployer: + """Manages deployment and deployment metadata of applications""" + + def __init__( + self, + app_manager: AppManager, + transaction_sender: AlgorandClientTransactionSender, + indexer: IndexerClient | None = None, + ): + self._app_manager = app_manager + self._transaction_sender = transaction_sender + self._indexer = indexer + self._app_lookups: dict[str, AppLookup] = {} + + def _create_deploy_note(self, metadata: AppDeployMetaData) -> bytes: + note = { + "dapp_name": APP_DEPLOY_NOTE_DAPP, + "format": "j", + "data": metadata.__dict__, + } + return json.dumps(note).encode() + + def deploy(self, deployment: AppDeployParams) -> AppDeployResult: + # Create new instances with updated notes + logger.info( + f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " + f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and " + f"{len(deployment.create_params.clear_state_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}", + suppress_log=deployment.suppress_log, + ) + note = self._create_deploy_note(deployment.metadata) + create_params = dataclasses.replace(deployment.create_params, note=note) + update_params = dataclasses.replace(deployment.update_params, note=note) + + deployment = dataclasses.replace( + deployment, + create_params=create_params, + update_params=update_params, + ) + + # Validate inputs + if ( + deployment.existing_deployments + and deployment.existing_deployments.creator != deployment.create_params.sender + ): + raise ValueError( + f"Received invalid existingDeployments value for creator " + f"{deployment.existing_deployments.creator} when attempting to deploy " + f"for creator {deployment.create_params.sender}" + ) + + if not deployment.existing_deployments and not self._indexer: + raise ValueError( + "Didn't receive an indexer client when this AppManager was created, " + "but also didn't receive an existingDeployments cache - one of them must be provided" + ) + + # Compile code if needed + approval_program = deployment.create_params.approval_program + clear_program = deployment.create_params.clear_state_program + + if isinstance(approval_program, str): + compiled_approval = self._app_manager.compile_teal_template( + approval_program, + deployment.deploy_time_params, + deployment.metadata.__dict__, + ) + approval_program = compiled_approval.compiled_base64_to_bytes + + if isinstance(clear_program, str): + compiled_clear = self._app_manager.compile_teal_template( + clear_program, + deployment.deploy_time_params, + ) + clear_program = compiled_clear.compiled_base64_to_bytes + + # Get existing app metadata + apps = deployment.existing_deployments or self.get_creator_apps_by_name( + creator_address=deployment.create_params.sender, + ignore_cache=deployment.ignore_cache, + ) + + existing_app = apps.apps.get(deployment.metadata.name) + if not existing_app or existing_app.deleted: + return self._create_app( + deployment=deployment, + approval_program=approval_program, + clear_program=clear_program, + ) + + # Check for changes + existing_app_record = self._app_manager.get_by_id(existing_app.app_id) + + existing_approval = base64.b64encode(existing_app_record.approval_program).decode() + existing_clear = base64.b64encode(existing_app_record.clear_state_program).decode() + + new_approval = base64.b64encode(approval_program).decode() + new_clear = base64.b64encode(clear_program).decode() + + is_update = new_approval != existing_approval or new_clear != existing_clear + is_schema_break = ( + existing_app_record.local_ints + < (deployment.create_params.schema.get("local_ints", 0) if deployment.create_params.schema else 0) + or existing_app_record.global_ints + < (deployment.create_params.schema.get("global_ints", 0) if deployment.create_params.schema else 0) + or existing_app_record.local_byte_slices + < (deployment.create_params.schema.get("local_byte_slices", 0) if deployment.create_params.schema else 0) + or existing_app_record.global_byte_slices + < (deployment.create_params.schema.get("global_byte_slices", 0) if deployment.create_params.schema else 0) + ) + + if is_schema_break: + logger.warning( + f"Detected a breaking app schema change in app {existing_app.app_id}:", + extra={ + "from": { + "global_ints": existing_app_record.global_ints, + "global_byte_slices": existing_app_record.global_byte_slices, + "local_ints": existing_app_record.local_ints, + "local_byte_slices": existing_app_record.local_byte_slices, + }, + "to": deployment.create_params.schema, + }, + suppress_log=deployment.suppress_log, + ) + + return self._handle_schema_break( + deployment=deployment, + existing_app=existing_app, + approval_program=approval_program, + clear_program=clear_program, + ) + + if is_update: + return self._handle_update( + deployment=deployment, + existing_app=existing_app, + approval_program=approval_program, + clear_program=clear_program, + ) + + return AppDeployResult( + **existing_app.__dict__, + operation_performed=OperationPerformed.Nothing, + app_id=existing_app.app_id, + app_address=existing_app.app_address, + ) + + def _create_app( + self, + deployment: AppDeployParams, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Create a new application""" + + if isinstance(deployment.create_params, AppCreateMethodCall): + result = self._transaction_sender.app_create_method_call( + AppCreateMethodCall( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + result = self._transaction_sender.app_create( + AppCreateParams( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + app_metadata = AppMetaData( + app_id=result.app_id, + app_address=get_application_address(result.app_id), + **deployment.metadata.__dict__, + created_metadata=deployment.metadata, + created_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + app_metadata_dict = app_metadata.__dict__ + app_metadata_dict["operation_performed"] = OperationPerformed.Create + app_metadata_dict["app_id"] = result.app_id + app_metadata_dict["app_address"] = get_application_address(result.app_id) + + return AppDeployResult( + **app_metadata_dict, + tx_id=result.tx_id, + tx_ids=result.tx_ids, + transaction=result.transaction, + transactions=result.transactions, + confirmation=result.confirmation, + confirmations=result.confirmations, + return_value=result.return_value, + ) + + def _handle_schema_break( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): + raise ValueError( + "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp" + ) + + if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app") + + def _handle_update( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_update in (OnUpdate.Fail, "fail"): + raise ValueError( + "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." + ) + + if deployment.on_update in (OnUpdate.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if deployment.on_update in (OnUpdate.UpdateApp, "update"): + if existing_app.updatable: + return self._update_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app") + + if deployment.on_update in (OnUpdate.ReplaceApp, "replace"): + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app") + + raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") + + def _replace_app( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + composer = self._transaction_sender.new_group() + + # Add create transaction + if isinstance(deployment.create_params, AppCreateMethodCall): + composer.add_app_create_method_call( + AppCreateMethodCall( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + composer.add_app_create( + AppCreateParams( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + # Add delete transaction + if isinstance(deployment.delete_params, AppDeleteMethodCall): + delete_call_params = AppDeleteMethodCall( + **{ + **deployment.delete_params.__dict__, + "app_id": existing_app.app_id, + } + ) + composer.add_app_delete_method_call(delete_call_params) + else: + delete_params = AppDeleteParams( + **{ + **deployment.delete_params.__dict__, + "app_id": existing_app.app_id, + } + ) + composer.add_app_delete(delete_params) + + result = composer.send() + + app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] + app_metadata = AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + **deployment.metadata.__dict__, + created_metadata=deployment.metadata, + created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + deleted=False, + ) + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + app_metadata_dict = app_metadata.__dict__ + app_metadata_dict["operation_performed"] = OperationPerformed.Replace + app_metadata_dict["app_id"] = app_id + app_metadata_dict["app_address"] = get_application_address(app_id) + + # Extract return_value and delete_return_value from ABIResult + return_value = result.returns[0] if result.returns and isinstance(result.returns[0], ABIResult) else None + delete_return_value = ( + result.returns[-1] if len(result.returns) > 1 and isinstance(result.returns[-1], ABIResult) else None + ) + + return AppDeployResult( + **app_metadata_dict, + tx_id=result.tx_ids[0], + tx_ids=result.tx_ids, + transaction=result.transactions[0], + transactions=result.transactions, + confirmation=result.confirmations[0], + confirmations=result.confirmations, + return_value=return_value, + delete_return_value=delete_return_value, + delete_result=ConfirmedTransactionResult( + transaction=result.transactions[-1], + confirmation=result.confirmations[-1], + ), + ) + + def _update_app( + self, + deployment: AppDeployParams, + existing_app: AppMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Update an existing application""" + + if isinstance(deployment.update_params, AppUpdateMethodCall): + result = self._transaction_sender.app_update_method_call( + AppUpdateMethodCall( + **{ + **deployment.update_params.__dict__, + "app_id": existing_app.app_id, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + else: + result = self._transaction_sender.app_update( + AppUpdateParams( + **{ + **deployment.update_params.__dict__, + "app_id": existing_app.app_id, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) + ) + + app_metadata = AppMetaData( + app_id=existing_app.app_id, + app_address=existing_app.app_address, + created_metadata=existing_app.created_metadata, + created_round=existing_app.created_round, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + **deployment.metadata.__dict__, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + return AppDeployResult( + **app_metadata.__dict__, + operation_performed=OperationPerformed.Update, + transaction=result.transaction, + transactions=result.transactions, + confirmation=result.confirmation, + confirmations=result.confirmations, + return_value=result.return_value, + ) + + def _update_app_lookup(self, sender: str, app_metadata: AppMetaData) -> None: + """Update the app lookup cache""" + + lookup = self._app_lookups.get(sender) + if not lookup: + self._app_lookups[sender] = AppLookup( + creator=sender, + apps={app_metadata.name: app_metadata}, + ) + else: + lookup.apps[app_metadata.name] = app_metadata + + def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> AppLookup: + """Get apps created by an account""" + + if not ignore_cache and creator_address in self._app_lookups: + return self._app_lookups[creator_address] + + if not self._indexer: + raise ValueError( + "Didn't receive an indexer client when this AppManager was created, " + "but received a call to get_creator_apps" + ) + + app_lookup: dict[str, AppMetaData] = {} + + # Get all apps created by account + created_apps = self._indexer.search_applications(creator=creator_address) + + for app in created_apps["applications"]: + app_id = app["id"] + + # Get creation transaction + creation_txns = self._indexer.search_transactions( + application_id=app_id, + min_round=app["created-at-round"], + address=creator_address, + address_role="sender", + note_prefix=base64.b64encode(APP_DEPLOY_NOTE_DAPP.encode()), + limit=1, + ) + + if not creation_txns["transactions"]: + continue + + creation_txn = creation_txns["transactions"][0] + + try: + note = base64.b64decode(creation_txn["note"]).decode() + if not note.startswith(f"{APP_DEPLOY_NOTE_DAPP}:j"): + continue + + metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :]) + + if metadata.get("name"): + app_lookup[metadata["name"]] = AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + created_metadata=metadata, + created_round=creation_txn["confirmed-round"], + **metadata, + updated_round=creation_txn["confirmed-round"], + deleted=app.get("deleted", False), + ) + except Exception as e: + logger.warning( + f"Error processing app {app_id} for creator {creator_address}: {e}", + ) + continue + + lookup = AppLookup(creator=creator_address, apps=app_lookup) + self._app_lookups[creator_address] = lookup + return lookup diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py new file mode 100644 index 00000000..2c8c3a94 --- /dev/null +++ b/src/algokit_utils/applications/app_factory.py @@ -0,0 +1,690 @@ +import base64 +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, TypeGuard, TypeVar + +import algosdk +from algosdk import transaction +from algosdk.abi import Method +from algosdk.atomic_transaction_composer import ABIResult, TransactionSigner +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.deploy import AppDeployMetaData, AppLookup, OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_client import ( + AppClient, + AppClientBareCallParams, + AppClientCompilationParams, + AppClientCompilationResult, + AppClientMethodCallParams, + AppClientParams, + AppSourceMaps, + ExposedLogicErrorDetails, +) +from algokit_utils.applications.app_deployer import ( + AppDeployParams, + ConfirmedTransactionResult, + DeployAppDeleteParams, + DeployAppUpdateParams, +) +from algokit_utils.applications.app_manager import TealTemplateParams +from algokit_utils.applications.utils import ( + get_abi_decoded_value, + get_abi_tuple_from_abi_struct, + get_arc56_method, + get_arc56_return_value, +) +from algokit_utils.models.abi import ABIStruct, ABIValue +from algokit_utils.models.application import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + Arc56Contract, + Arc56Method, + CompiledTeal, + MethodArg, +) +from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.application import AlgorandClientProtocol +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppUpdateMethodCall, + BuiltTransactions, +) +from algokit_utils.transactions.transaction_sender import SendAppCreateTransactionResult, SendAppTransactionResult + +T = TypeVar("T") + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryParams: + algorand: AlgorandClientProtocol + app_spec: Arc56Contract | ApplicationSpecification | str + app_name: str | None = None + default_sender: str | bytes | None = None + default_signer: TransactionSigner | None = None + version: str | None = None + updatable: bool | None = None + deletable: bool | None = None + deploy_time_params: TealTemplateParams | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateParams(AppClientBareCallParams, AppClientCompilationParams): + on_complete: transaction.OnComplete | None = None + schema: dict[str, int] | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateWithSendParams(AppFactoryCreateParams, SendParams): + pass + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompilationParams): + on_complete: transaction.OnComplete | None = None + schema: dict[str, int] | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, SendParams): + pass + + +@dataclass(frozen=True, kw_only=True) +class AppFactoryCreateResult(SendAppTransactionResult): + """Result from creating an application via AppFactory""" + + app_id: int + """The ID of the created application""" + app_address: str + """The address of the created application""" + compiled_approval: CompiledTeal | None = None + """The compiled approval program if source was provided""" + compiled_clear: CompiledTeal | None = None + """The compiled clear program if source was provided""" + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryDeployResult: + """Represents the result object from app deployment""" + + app_address: str + app_id: int + approval_program: bytes # Uint8Array + clear_state_program: bytes # Uint8Array + compiled_approval: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + compiled_clear: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + created_metadata: dict # {name: str, version: str, updatable: bool, deletable: bool} + created_round: int + deletable: bool + deleted: bool + delete_return_value: ABIValue | ABIStruct | None = None + delete_result: ConfirmedTransactionResult | None = None + group_id: str | None = None + name: str + operation_performed: OperationPerformed + return_value: ABIValue | ABIStruct | None = None + returns: list[Any] | None = None + transaction: TransactionWrapper + transactions: list[TransactionWrapper] + tx_id: str + tx_ids: list[str] + updatable: bool + updated_round: int + version: str + + +class _AppFactoryBareParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParams: + create_args = {} + if params: + create_args = {**params.__dict__.copy()} + del create_args["schema"] + del create_args["sender"] + del create_args["on_complete"] + del create_args["deploy_time_params"] + del create_args["updatable"] + del create_args["deletable"] + compiled = self._factory.compile(params) + create_args["approval_program"] = compiled.approval_program + create_args["clear_state_program"] = compiled.clear_state_program + + return AppCreateParams( + **create_args, + schema=(params.schema if params else None) + or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + }, + sender=self._factory._get_sender(params.sender if params else None), + on_complete=(params.on_complete if params else None) or OnComplete.NoOpOC, + ) + + def deploy_update(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.UpdateApplicationOC, + } + + def deploy_delete(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.DeleteApplicationOC, + } + + +class _AppFactoryParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareParamsAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareParamsAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCall: + compiled = self._factory.compile(params) + params_dict = params.__dict__ + params_dict["schema"] = params.schema or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + } + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = params.on_complete or OnComplete.NoOpOC + del params_dict["deploy_time_params"] + del params_dict["updatable"] + del params_dict["deletable"] + return AppCreateMethodCall( + **params_dict, + app_id=0, + approval_program=compiled.approval_program, + clear_state_program=compiled.clear_state_program, + ) + + def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCall: + params_dict = params.__dict__.copy() + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = OnComplete.UpdateApplicationOC + return AppUpdateMethodCall(**params_dict, app_id=0, approval_program="", clear_state_program="") + + def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + params_dict = params.__dict__.copy() + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = OnComplete.DeleteApplicationOC + return AppDeleteMethodCall(**params_dict, app_id=0) + + +class _AppFactoryBareCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + + def create(self, params: AppFactoryCreateParams | None = None) -> Transaction: + return self._factory._algorand.create_transaction.app_create(self._factory.params.bare.create(params)) + + +class _AppFactoryCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareCreateTransactionAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareCreateTransactionAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> BuiltTransactions: + return self._factory._algorand.create_transaction.app_create_method_call(self._factory.params.create(params)) + + +class _AppFactoryBareSendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateWithSendParams | None = None) -> tuple[AppClient, AppFactoryCreateResult]: + updatable = params.updatable if params and params.updatable is not None else self._factory._updatable + deletable = params.deletable if params and params.deletable is not None else self._factory._deletable + deploy_time_params = ( + params.deploy_time_params + if params and params.deploy_time_params is not None + else self._factory._deploy_time_params + ) + + compiled = self._factory.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + create_args = {} + if params: + create_args = {**params.__dict__} + del create_args["max_rounds_to_wait"] + del create_args["suppress_log"] + del create_args["populate_app_call_resources"] + + create_args["updatable"] = updatable + create_args["deletable"] = deletable + create_args["deploy_time_params"] = deploy_time_params + + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create( + self._factory.params.bare.create(AppFactoryCreateParams(**create_args)) + ) + ).__dict__ + + result["compiled_approval"] = compiled.compiled_approval + result["compiled_clear"] = compiled.compiled_clear + + return ( + self._factory.get_app_client_by_id( + app_id=result["app_id"], + ), + AppFactoryCreateResult(**result), + ) + + +class _AppFactorySendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + self._bare = _AppFactoryBareSendAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareSendAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, SendAppCreateTransactionResult]: + updatable = params.updatable if params.updatable is not None else self._factory._updatable + deletable = params.deletable if params.deletable is not None else self._factory._deletable + deploy_time_params = ( + params.deploy_time_params if params.deploy_time_params is not None else self._factory._deploy_time_params + ) + + compiled = self._factory.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + create_params_dict = params.__dict__.copy() + create_params_dict["updatable"] = updatable + create_params_dict["deletable"] = deletable + create_params_dict["deploy_time_params"] = deploy_time_params + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create_method_call( + self._factory.params.create(AppFactoryCreateMethodCallParams(**create_params_dict)) + ) + ) + + return ( + self._factory.get_app_client_by_id( + app_id=result.app_id, + ), + SendAppCreateTransactionResult( + **{ + **result.__dict__, + **( + {"compiled_approval": compiled.compiled_approval, "compiled_clear": compiled.compiled_clear} + if compiled + else {} + ), + } + ), + ) + + +class AppFactory: + def __init__(self, params: AppFactoryParams) -> None: + self._app_spec = AppClient.normalise_app_spec(params.app_spec) + self._app_name = params.app_name or self._app_spec.name + self._algorand = params.algorand + self._version = params.version or "1.0" + self._default_sender = params.default_sender + self._default_signer = params.default_signer + self._deploy_time_params = params.deploy_time_params + self._updatable = params.updatable + self._deletable = params.deletable + self._approval_source_map: SourceMap | None = None + self._clear_source_map: SourceMap | None = None + self._params_accessor = _AppFactoryParamsAccessor(self) + self._send_accessor = _AppFactorySendAccessor(self) + self._create_transaction_accessor = _AppFactoryCreateTransactionAccessor(self) + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def algorand(self) -> AlgorandClientProtocol: + return self._algorand + + @property + def params(self) -> _AppFactoryParamsAccessor: + return self._params_accessor + + @property + def send(self) -> _AppFactorySendAccessor: + return self._send_accessor + + @property + def create_transaction(self) -> _AppFactoryCreateTransactionAccessor: + return self._create_transaction_accessor + + def deploy( # noqa: PLR0913 + self, + *, + deploy_time_params: TealTemplateParams | None = None, + on_update: OnUpdate = OnUpdate.Fail, + on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + create_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + existing_deployments: AppLookup | None = None, + ignore_cache: bool = False, + updatable: bool | None = None, + deletable: bool | None = None, + app_name: str | None = None, + max_rounds_to_wait: int | None = None, # noqa: ARG002 TODO: revisit + suppress_log: bool = False, # noqa: ARG002 TODO: revisit + populate_app_call_resources: bool = False, # noqa: ARG002 TODO: revisit + ) -> tuple[AppClient, AppFactoryDeployResult]: + updatable = ( + updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") + ) + deletable = ( + deletable if deletable is not None else self._deletable or self._get_deploy_time_control("deletable") + ) + deploy_time_params = deploy_time_params if deploy_time_params is not None else self._deploy_time_params + + compiled = self.compile( + AppClientCompilationParams( + deploy_time_params=deploy_time_params, + updatable=updatable, + deletable=deletable, + ) + ) + + def _is_method_call_params( + params: AppClientMethodCallParams | AppClientBareCallParams | None, + ) -> TypeGuard[AppClientMethodCallParams]: + return params is not None and hasattr(params, "method") + + update_args: DeployAppUpdateParams | AppUpdateMethodCall + if _is_method_call_params(update_params): + update_args = self.params.deploy_update(update_params) + else: + update_args = DeployAppUpdateParams( + **self.params.bare.deploy_update( + update_params if isinstance(update_params, AppClientBareCallParams) else None + ) + ) + + delete_args: DeployAppDeleteParams | AppDeleteMethodCall + if _is_method_call_params(delete_params): + delete_args = self.params.deploy_delete(delete_params) + else: + delete_args = DeployAppDeleteParams( + **self.params.bare.deploy_delete( + delete_params if isinstance(delete_params, AppClientBareCallParams) else None + ) + ) + + app_deploy_params = AppDeployParams( + deploy_time_params=deploy_time_params, + on_schema_break=on_schema_break, + on_update=on_update, + existing_deployments=existing_deployments, + ignore_cache=ignore_cache, + create_params=( + self.params.create( + AppFactoryCreateMethodCallParams( + **create_params.__dict__, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + if create_params and hasattr(create_params, "method") + else self.params.bare.create( + AppFactoryCreateParams( + **create_params.__dict__ if create_params else {}, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + ), + update_params=update_args, + delete_params=delete_args, + metadata=AppDeployMetaData( + name=app_name or self._app_name, + version=self._version, + updatable=updatable, + deletable=deletable, + ), + ) + deploy_result = self._algorand.app_deployer.deploy(app_deploy_params) + + app_client = self.get_app_client_by_id( + app_id=deploy_result.app_id or 0, + app_name=app_name, + default_sender=self._default_sender, + default_signer=self._default_signer, + ) + + result = {**deploy_result.__dict__, **(compiled.__dict__ if compiled else {})} + + if "return_value" in result: + if result["operation_performed"] == OperationPerformed.Update: + if update_params and isinstance(update_params, AppClientMethodCallParams): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(update_params.method, self._app_spec), + self._app_spec.structs, + ) + elif create_params and isinstance(create_params, AppClientMethodCallParams): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(create_params.method, self._app_spec), + self._app_spec.structs, + ) + + if "delete_return_value" in result and delete_params and isinstance(delete_params, AppClientMethodCallParams): + result["delete_return_value"] = get_arc56_return_value( + result["delete_return_value"], + get_arc56_method(delete_params.method, self._app_spec), + self._app_spec.structs, + ) + + return app_client, AppFactoryDeployResult(**result) + + def get_app_client_by_id( + self, + app_id: int, + app_name: str | None = None, + default_sender: str | bytes | None = None, # Address can be string or bytes + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient( + AppClientParams( + app_id=app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ) + ) + + def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT002 FBT001 TODO: revisit + return AppClient.expose_logic_error_static( + e, + self._app_spec, + ExposedLogicErrorDetails( + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=None, + approval_source_info=( + self._app_spec.source_info.get("approval") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + clear_source_info=( + self._app_spec.source_info.get("clear") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + ), + ) + + def export_source_maps(self) -> AppSourceMaps: + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "you need to call create, update, or deploy first" + ) + return AppSourceMaps( + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + ) + + def import_source_maps(self, source_maps: AppSourceMaps) -> None: + self._approval_source_map = source_maps.approval_source_map + self._clear_source_map = source_maps.clear_source_map + + def compile(self, compilation: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + result = AppClient.compile( + self._app_spec, + self._algorand.app, + deploy_time_params=compilation.deploy_time_params if compilation else None, + updatable=compilation.updatable if compilation else None, + deletable=compilation.deletable if compilation else None, + ) + + if result.compiled_approval: + self._approval_source_map = result.compiled_approval.source_map + if result.compiled_clear: + self._clear_source_map = result.compiled_clear.source_map + + return result + + def _get_deploy_time_control(self, control: str) -> bool | None: + approval = ( + self._app_spec.source["approval"] if self._app_spec.source and "approval" in self._app_spec.source else None + ) + + template_name = UPDATABLE_TEMPLATE_NAME if control == "updatable" else DELETABLE_TEMPLATE_NAME + if not approval or template_name not in approval: + return None + + on_complete = "UpdateApplication" if control == "updatable" else "DeleteApplication" + return on_complete in self._app_spec.bare_actions.get("call", []) or any( + on_complete in m.actions.call for m in self._app_spec.methods if m.actions and m.actions.call + ) + + def _get_sender(self, sender: str | bytes | None) -> str: + if not sender and not self._default_sender: + raise Exception( + f"No sender provided and no default sender present in app client for call to app {self._app_name}" + ) + return str(sender or self._default_sender) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + try: + return call() + except Exception as e: + raise self.expose_logic_error(e) from None + + def _parse_method_call_return(self, result: SendAppTransactionResult, method: Method) -> SendAppTransactionResult: + return SendAppTransactionResult( + **{ + **result.__dict__, + "return_value": get_arc56_return_value(result.return_value, method, self._app_spec.structs) + if isinstance(result.return_value, ABIResult) + else None, + } + ) + + def _get_create_abi_args_with_default_values( + self, + method_name_or_signature: str | Arc56Method, + args: list[Any] | None, + ) -> list[Any]: + method = ( + get_arc56_method(method_name_or_signature, self._app_spec) + if isinstance(method_name_or_signature, str) + else method_name_or_signature + ) + result = [] + + def _has_struct(arg: Any) -> TypeGuard[MethodArg]: # noqa: ANN401 + return hasattr(arg, "struct") + + for i, method_arg in enumerate(method.args): + arg = method_arg + arg_value = args[i] if args and i < len(args) else None + + if arg_value is not None: + if _has_struct(arg) and arg.struct and isinstance(arg_value, dict): + arg_value = get_abi_tuple_from_abi_struct( + arg_value, + self._app_spec.structs[arg.struct], + self._app_spec.structs, + ) + result.append(arg_value) + continue + + default_value = getattr(arg, "default_value", None) + if default_value: + if default_value.source == "literal": + value_raw = base64.b64decode(default_value.data) + value_type = default_value.type or str(arg.type) + result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) + else: + raise ValueError( + f"Can't provide default value for {default_value.source} for a contract creation call" + ) + else: + raise ValueError( + f"No value provided for required argument " + f"{arg.name or f'arg{i+1}'} in call to method {method.name}" + ) + + return result diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index 307d5e0f..e282ede7 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -2,33 +2,44 @@ from collections.abc import Mapping from dataclasses import dataclass from enum import IntEnum -from typing import Any, TypeAlias +from typing import Any, TypeAlias, cast import algosdk import algosdk.atomic_transaction_composer import algosdk.box_reference -from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.atomic_transaction_composer import ABIResult, AccountTransactionSigner +from algosdk.box_reference import BoxReference as AlgosdkBoxReference from algosdk.logic import get_application_address +from algosdk.source_map import SourceMap from algosdk.v2client import algod -from algokit_utils.models.abi import ABIValue -from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.models.abi import ABIType, ABIValue +from algokit_utils.models.application import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + AppInformation, + AppState, + CompiledTeal, +) -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class BoxName: name: str name_raw: bytes name_base64: str -@dataclass(frozen=True) -class AppState: - key_raw: bytes - key_base64: str - value_raw: bytes | None - value_base64: str | None - value: str | int +@dataclass(kw_only=True, frozen=True) +class BoxValue: + name: BoxName + value: bytes + + +@dataclass(kw_only=True, frozen=True) +class BoxABIValue: + name: BoxName + value: ABIValue class DataTypeFlag(IntEnum): @@ -39,31 +50,17 @@ class DataTypeFlag(IntEnum): TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] -@dataclass(frozen=True) -class AppInformation: - app_id: int - app_address: str - approval_program: bytes - clear_state_program: bytes - creator: str - global_state: dict[str, AppState] - local_ints: int - local_byte_slices: int - global_ints: int - global_byte_slices: int - extra_program_pages: int | None +BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner -@dataclass(frozen=True) -class CompiledTeal: - teal: str - compiled: bytes - compiled_hash: str - compiled_base64_to_bytes: bytes - source_map: dict | None +class BoxReference(AlgosdkBoxReference): + def __init__(self, app_id: int, name: bytes): + super().__init__(app_index=app_id, name=name) - -BoxIdentifier = str | bytes | AccountTransactionSigner + def __eq__(self, other: object) -> bool: + if isinstance(other, (BoxReference | AlgosdkBoxReference)): + return self.app_index == other.app_index and self.name == other.name + return False def _is_valid_token_character(char: str) -> bool: @@ -134,7 +131,7 @@ def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: result: list[str] = [] match_count = 0 - token = f"TMPL_{template_variable}" + token = f"TMPL_{template_variable}" if not template_variable.startswith("TMPL_") else template_variable token_idx_offset = len(value) - len(token) for line in program_lines: comment_idx = _find_unquoted_string(line, "//") @@ -173,7 +170,7 @@ def compile_teal(self, teal_code: str) -> CompiledTeal: compiled=compiled["result"], compiled_hash=compiled["hash"], compiled_base64_to_bytes=base64.b64decode(compiled["result"]), - source_map=compiled.get("sourcemap"), + source_map=SourceMap(compiled.get("sourcemap", {})), ) self._compilation_results[teal_code] = result return result @@ -182,7 +179,7 @@ def compile_teal_template( self, teal_template_code: str, template_params: TealTemplateParams | None = None, - deployment_metadata: dict[str, bool] | None = None, + deployment_metadata: Mapping[str, bool] | None = None, ) -> CompiledTeal: teal_code = AppManager.strip_teal_comments(teal_template_code) teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) @@ -237,41 +234,51 @@ def get_box_names(self, app_id: int) -> list[BoxName]: ] def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: - name = b"" - if isinstance(box_name, str): - name = box_name.encode("utf-8") - elif isinstance(box_name, bytes): - name = box_name - elif isinstance(box_name, AccountTransactionSigner): - name = algosdk.encoding.decode_address(algosdk.account.address_from_private_key(box_name.private_key)) - else: - raise ValueError(f"Invalid box identifier type: {type(box_name)}") - + name = AppManager.get_box_reference(box_name)[1] box_result = self._algod.application_box_by_name(app_id, name) assert isinstance(box_result, dict) - return base64.b64decode(box_result["value"]) + return bytes(box_result["value"], "utf-8") def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: return [self.get_box_value(app_id, box_name) for box_name in box_names] - def get_box_value_from_abi_type( - self, app_id: int, box_name: BoxIdentifier, abi_type: algosdk.abi.ABIType - ) -> ABIValue: + def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_type: ABIType) -> ABIValue: value = self.get_box_value(app_id, box_name) try: - return abi_type.decode(value) # type: ignore[no-any-return] + parse_to_tuple = isinstance(abi_type, algosdk.abi.TupleType) + decoded_value = abi_type.decode(base64.b64decode(value)) + return tuple(decoded_value) if parse_to_tuple else decoded_value except Exception as e: raise ValueError(f"Failed to decode box value {value.decode('utf-8')} with ABI type {abi_type}") from e def get_box_values_from_abi_type( - self, app_id: int, box_names: list[BoxIdentifier], abi_type: algosdk.abi.ABIType + self, app_id: int, box_names: list[BoxIdentifier], abi_type: ABIType ) -> list[ABIValue]: return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] + @staticmethod + def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes]: + if isinstance(box_id, (BoxReference | AlgosdkBoxReference)): + return box_id.app_index, box_id.name + + name = b"" + if isinstance(box_id, str): + name = box_id.encode("utf-8") + elif isinstance(box_id, bytes): + name = box_id + elif isinstance(box_id, AccountTransactionSigner): + name = cast( + bytes, algosdk.encoding.decode_address(algosdk.account.address_from_private_key(box_id.private_key)) + ) + else: + raise ValueError(f"Invalid box identifier type: {type(box_id)}") + + return 0, name + @staticmethod def get_abi_return( confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None - ) -> ABIValue | None: + ) -> ABIResult | None: """Get the ABI return value from a transaction confirmation.""" if not method: return None @@ -287,7 +294,7 @@ def get_abi_return( if not abi_result: return None - return abi_result.return_value # type: ignore[no-any-return] + return abi_result @staticmethod def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: @@ -346,7 +353,7 @@ def replace_template_variables(program: str, template_values: TealTemplateParams return "\n".join(program_lines) @staticmethod - def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: dict[str, bool]) -> str: + def replace_teal_template_deploy_time_control_params(teal_template_code: str, params: Mapping[str, bool]) -> str: if params.get("updatable") is not None: if UPDATABLE_TEMPLATE_NAME not in teal_template_code: raise ValueError( diff --git a/src/algokit_utils/applications/utils.py b/src/algokit_utils/applications/utils.py new file mode 100644 index 00000000..05bc4650 --- /dev/null +++ b/src/algokit_utils/applications/utils.py @@ -0,0 +1,428 @@ +import base64 +from typing import Any, Literal, TypeVar + +from algosdk.abi import Method as AlgorandABIMethod +from algosdk.abi import TupleType +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + AppSpecStateDict, + DefaultArgumentDict, + MethodConfigDict, + MethodHints, +) +from algokit_utils.models.abi import ABIStruct, ABIType, ABIValue +from algokit_utils.models.application import ( + ABIArgumentType, + ABITypeAlias, + Arc56Contract, + Arc56ContractState, + Arc56Method, + CallConfig, + DefaultValue, + Method, + MethodActions, + MethodArg, + MethodReturns, + OnCompleteAction, + StorageKey, + StructField, + StructName, +) + +T = TypeVar("T", bound=ABIValue | bytes | ABIStruct | None) + + +def get_arc56_method(method_name_or_signature: str, app_spec: Arc56Contract) -> Arc56Method: + if "(" not in method_name_or_signature: + # Filter by method name + methods = [m for m in app_spec.methods if m.name == method_name_or_signature] + if not methods: + raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") + if len(methods) > 1: + signatures = [AlgorandABIMethod.undictify(m.__dict__).get_signature() for m in app_spec.methods] + raise ValueError( + f"Received a call to method {method_name_or_signature} in contract {app_spec.name}, " + f"but this resolved to multiple methods; please pass in an ABI signature instead: " + f"{', '.join(signatures)}" + ) + method = methods[0] + else: + # Find by signature + method = None + for m in app_spec.methods: + abi_method = AlgorandABIMethod.undictify(m.to_dict()) + if abi_method.get_signature() == method_name_or_signature: + method = m + break + + if method is None: + raise ValueError(f"Unable to find method {method_name_or_signature} in {app_spec.name} app.") + + return Arc56Method(method) + + +def get_arc56_return_value( + return_value: ABIResult | None, + method: Method | AlgorandABIMethod, + structs: dict[str, list[StructField]], +) -> ABIValue | ABIStruct | None: + """Checks for decode errors on the return value and maps it to the specified type. + + Args: + return_value: The smart contract response + method: The method that was called + structs: The struct fields from the app spec + + Returns: + The smart contract response with an updated return value + + Raises: + ValueError: If there is a decode error + """ + + # Get method returns info + if isinstance(method, AlgorandABIMethod): + type_str = method.returns.type + struct = None # AlgorandABIMethod doesn't have struct info + else: + type_str = method.returns.type + struct = method.returns.struct + + # Handle void/undefined returns + if type_str == "void" or return_value is None: + return None + + # Handle decode errors + if return_value.decode_error: + raise ValueError(return_value.decode_error) + + # Get raw return value + raw_value = return_value.raw_value + + # Handle AVM types + if type_str == "AVMBytes": + return raw_value + if type_str == "AVMString" and raw_value: + return raw_value.decode("utf-8") + if type_str == "AVMUint64" and raw_value: + return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return] + + # Handle structs + if struct and struct in structs: + return_tuple = return_value.return_value + return get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs) + + # Return as-is + return return_value.return_value # type: ignore[no-any-return] + + +def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: ANN401, PLR0911 + if isinstance(value, (bytes | bytearray)): + return value + if type_str == "AVMUint64": + return ABIType.from_string("uint64").encode(value) + if type_str in ("AVMBytes", "AVMString"): + if isinstance(value, str): + return value.encode("utf-8") + if not isinstance(value, (bytes | bytearray)): + raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}") + return value + if type_str in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs) + if isinstance(value, (list | tuple)): + return tuple_type.encode(value) # type: ignore[arg-type] + else: + tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs) + return tuple_type.encode(tuple_values) + else: + abi_type = ABIType.from_string(type_str) + return abi_type.encode(value) + + +def get_abi_decoded_value( + value: bytes | int | str, type_str: str | ABITypeAlias | ABIArgumentType, structs: dict[str, list[StructField]] +) -> ABIValue: + type_value = str(type_str) + + if type_value == "AVMBytes" or not isinstance(value, bytes): + return value + if type_value == "AVMString": + return value.decode("utf-8") + if type_value == "AVMUint64": + return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return] + if type_value in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs) + decoded_tuple = tuple_type.decode(value) + return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs) + return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return] + + +def get_abi_tuple_from_abi_struct( + struct_value: dict[str, Any], + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> list[Any]: + result = [] + for field in struct_fields: + key = field.name + if key not in struct_value: + raise ValueError(f"Missing value for field '{key}'") + value = struct_value[key] + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_tuple_from_abi_struct(value, field_type, structs) + result.append(value) + return result + + +def get_abi_tuple_type_from_abi_struct_definition( + struct_def: list[StructField], structs: dict[str, list[StructField]] +) -> TupleType: + types = [] + for field in struct_def: + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs)) + else: + types.append(ABIType.from_string(field_type)) # type: ignore[arg-type] + elif isinstance(field_type, list): + types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs)) + else: + raise ValueError(f"Invalid field type: {field_type}") + return TupleType(types) + + +def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> dict[str, Any]: + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + +def arc32_to_arc56(app_spec: ApplicationSpecification) -> Arc56Contract: # noqa: C901 + """ + Convert ARC-32 application specification to ARC-56 contract format. + + Args: + app_spec: ARC-32 application specification + + Returns: + ARC-56 contract specification + """ + + def convert_structs() -> dict[StructName, list[StructField]]: + structs: dict[StructName, list[StructField]] = {} + for hint in app_spec.hints.values(): + if not hint.structs: + continue + for struct in hint.structs.values(): + fields = [ + StructField( + name=name, + type=type_, + ) + for name, type_ in struct["elements"] + ] + structs[struct["name"]] = fields + return structs + + def get_hint(method: AlgorandABIMethod) -> MethodHints | None: + sig = method.get_signature() + return app_spec.hints.get(sig) + + def get_default_value( + type: str | ABIType, # noqa: A002 TODO: revisit + default_arg: DefaultArgumentDict, + ) -> DefaultValue | None: + if not default_arg or default_arg["source"] == "abi-method": + return None + + source_map = { + "constant": "literal", + "global-state": "global", + "local-state": "local", + } + + data = default_arg["data"] + if isinstance(data, str): + data = base64.b64encode(data.encode()).decode() + elif isinstance(data, bytes): + data = base64.b64encode(data).decode() + else: + data = str(data) + + return DefaultValue( + data=data, + type="AVMString" if type == "string" else str(type), + source=source_map.get(default_arg["source"], "literal"), # type: ignore[arg-type] + ) + + def convert_method(method: AlgorandABIMethod) -> Method: + hint = get_hint(method) + + args: list[MethodArg] = [] + for arg in method.args: + if not arg.name: + continue + struct_name = None + if hint and hint.structs and arg.name in hint.structs: + struct_name = hint.structs[arg.name].get("name") + + default_value = None + if hint and hint.default_arguments and arg.name in hint.default_arguments: + default_value = get_default_value(str(arg.type), hint.default_arguments[arg.name]) + + method_arg = MethodArg( + type=arg.type, # type: ignore[arg-type] + struct=struct_name, + name=arg.name, + desc=arg.desc, + default_value=default_value, + ) + args.append(method_arg) + + method_returns = MethodReturns( + type=str(method.returns.type), + struct=hint.structs.get("output", {}).get("name") if hint and hint.structs else None, # type: ignore[call-overload] + desc=method.returns.desc, + ) + + method_actions = MethodActions( + create=convert_actions(hint.call_config, "CREATE") if hint and hint.call_config else [], # type: ignore # noqa: PGH003 + call=convert_actions(hint.call_config, "CALL") if hint and hint.call_config else [], + ) + + return Method( + name=method.name, + desc=method.desc, + args=args, + returns=method_returns, + actions=method_actions, + readonly=hint.read_only if hint else False, + events=[], + recommendations=None, + ) + + def convert_storage_keys(schema_dict: AppSpecStateDict) -> dict[str, StorageKey]: + return { + name: StorageKey( + desc=spec.get("descr"), + key_type=spec["type"], + value_type="AVMUint64" if spec["type"] == "uint64" else "AVMBytes", + key=base64.b64encode(spec["key"].encode()).decode(), + ) + for name, spec in schema_dict.get("declared", {}).items() + } + + def convert_actions( + call_config: CallConfig | MethodConfigDict, action_type: Literal["CREATE", "CALL"] + ) -> list[OnCompleteAction | Literal["NoOp", "OptIn", "DeleteApplication"]]: + """ + Converts method configuration into a list of on-complete action literals. + + Args: + call_config (CallConfig | MethodConfigDict): Configuration dictionary or CallConfig object for method + actions. + action_type (Literal["CREATE", "CALL"]): The type of action to convert. + + Returns: + List[OnCompleteAction]: A list of on-complete action literals. + """ + config_action_map = { + "no_op": "NoOp", + "opt_in": "OptIn", + "close_out": "CloseOut", + "clear_state": "ClearState", + "update_application": "UpdateApplication", + "delete_application": "DeleteApplication", + } + + def get_action_value(key: str) -> str | None: + if isinstance(call_config, dict): + config_value = call_config.get(key) # type: ignore[call-overload] + # Handle legacy CallConfig enum + return config_value.name if hasattr(config_value, "name") else config_value # type: ignore[no-any-return] + # Handle new CallConfig dataclass + return getattr(call_config, key, None) + + return [action for key, action in config_action_map.items() if get_action_value(key) in ("ALL", action_type)] # type: ignore # noqa: PGH003 + + # Convert structs + structs = convert_structs() + + # Get schema information from app_spec + global_schema = app_spec.schema.get("global", {}) + local_schema = app_spec.schema.get("local", {}) + + state = Arc56ContractState( + schemas={ + "global": { + "ints": int(app_spec.global_state_schema.num_uints) if app_spec.global_state_schema.num_uints else 0, + "bytes": int(app_spec.global_state_schema.num_byte_slices) + if app_spec.global_state_schema.num_byte_slices + else 0, + }, + "local": { + "ints": int(app_spec.local_state_schema.num_uints) if app_spec.local_state_schema.num_uints else 0, + "bytes": int(app_spec.local_state_schema.num_byte_slices) + if app_spec.local_state_schema.num_byte_slices + else 0, + }, + }, + keys={ + "global": convert_storage_keys(global_schema), + "local": convert_storage_keys(local_schema), + "box": {}, + }, + maps={ + "global": {}, + "local": {}, + "box": {}, + }, + ) + + contract_source = { + "approval": app_spec.approval_program, + "clear": app_spec.clear_program, + } + + bare_actions = { + "create": convert_actions(app_spec.bare_call_config, "CREATE"), + "call": convert_actions(app_spec.bare_call_config, "CALL"), + } + + return Arc56Contract( + arcs=[], + name=app_spec.contract.name, + desc=app_spec.contract.desc, + structs=structs, + methods=[convert_method(m) for m in app_spec.contract.methods], + state=state, + source=contract_source, + bare_actions=bare_actions, + byte_code=None, + compiler_info=None, + events=None, + networks=None, + scratch_variables=None, + source_info=None, + template_variables=None, + ) diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index ee642dac..18184715 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -14,7 +14,7 @@ ) -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AccountAssetInformation: """Information about an account's holding of a particular asset.""" @@ -28,7 +28,7 @@ class AccountAssetInformation: """The round this information was retrieved at.""" -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetInformation: """Information about an asset.""" @@ -66,7 +66,7 @@ class AssetInformation: """32-byte hash of some metadata that is relevant to the asset and/or asset holders.""" -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class BulkAssetOptInOutResult: """Individual result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.""" diff --git a/src/algokit_utils/clients/algorand_client.py b/src/algokit_utils/clients/algorand_client.py index f679c95e..eb4ef73e 100644 --- a/src/algokit_utils/clients/algorand_client.py +++ b/src/algokit_utils/clients/algorand_client.py @@ -7,17 +7,11 @@ from typing_extensions import Self from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.applications.app_deployer import AppDeployer from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client, -) +from algokit_utils.models.network import AlgoClientConfigs from algokit_utils.transactions.transaction_composer import ( AppCallParams, AppMethodCallParams, @@ -36,16 +30,16 @@ __all__ = [ "AlgorandClient", - "AssetCreateParams", - "AssetOptInParams", + "AppCallParams", "AppMethodCallParams", - "PaymentParams", - "AssetFreezeParams", "AssetConfigParams", + "AssetCreateParams", "AssetDestroyParams", - "AppCallParams", - "OnlineKeyRegistrationParams", + "AssetFreezeParams", + "AssetOptInParams", "AssetTransferParams", + "OnlineKeyRegistrationParams", + "PaymentParams", ] @@ -53,7 +47,7 @@ class AlgorandClient: """A client that brokers easy access to Algorand functionality.""" def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): - self._client_manager: ClientManager = ClientManager(config) + self._client_manager: ClientManager = ClientManager(clients_or_configs=config, algorand_client=self) self._account_manager: AccountManager = AccountManager(self._client_manager) self._asset_manager: AssetManager = AssetManager(self._client_manager.algod, lambda: self.new_group()) self._app_manager: AppManager = AppManager(self._client_manager.algod) @@ -63,6 +57,9 @@ def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): app_manager=self._app_manager, algod_client=self._client_manager.algod, ) + self._app_deployer: AppDeployer = AppDeployer( + self._app_manager, self._transaction_sender, self._client_manager.indexer_if_present + ) self._transaction_creator = AlgorandClientTransactionCreator( new_group=lambda: self.new_group(), ) @@ -163,10 +160,14 @@ def asset(self) -> AssetManager: return self._asset_manager @property - def app_deployer(self) -> AppManager: - """Get or create applications.""" + def app(self) -> AppManager: return self._app_manager + @property + def app_deployer(self) -> AppDeployer: + """Get or create applications.""" + return self._app_deployer + @property def send(self) -> AlgorandClientTransactionSender: """Methods for sending a transaction and waiting for confirmation""" @@ -192,9 +193,9 @@ def default_local_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_default_localnet_config("algod"), - indexer_config=get_default_localnet_config("indexer"), - kmd_config=get_default_localnet_config("kmd"), + algod_config=ClientManager.get_default_local_net_config("algod"), + indexer_config=ClientManager.get_default_local_net_config("indexer"), + kmd_config=ClientManager.get_default_local_net_config("kmd"), ) ) @@ -207,8 +208,8 @@ def test_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_algonode_config("testnet", "algod", ""), - indexer_config=get_algonode_config("testnet", "indexer", ""), + algod_config=ClientManager.get_algonode_config("testnet", "algod"), + indexer_config=ClientManager.get_algonode_config("testnet", "indexer"), kmd_config=None, ) ) @@ -222,8 +223,8 @@ def main_net() -> "AlgorandClient": """ return AlgorandClient( AlgoClientConfigs( - algod_config=get_algonode_config("mainnet", "algod", ""), - indexer_config=get_algonode_config("mainnet", "indexer", ""), + algod_config=ClientManager.get_algonode_config("mainnet", "algod"), + indexer_config=ClientManager.get_algonode_config("mainnet", "indexer"), kmd_config=None, ) ) @@ -249,13 +250,7 @@ def from_environment() -> "AlgorandClient": :return: The `AlgorandClient` """ - return AlgorandClient( - AlgoSdkClients( - algod=get_algod_client(), - kmd=get_kmd_client(), - indexer=get_indexer_client(), - ) - ) + return AlgorandClient(ClientManager.get_config_from_environment_or_localnet()) @staticmethod def from_config(config: AlgoClientConfigs) -> "AlgorandClient": diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 16108520..ece39c6e 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,15 +1,22 @@ +import os +from dataclasses import dataclass +from typing import Literal +from urllib import parse + import algosdk +from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +# from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils.applications.app_manager import TealTemplateParams from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_indexer_client, - get_kmd_client, -) +from algokit_utils.models.application import Arc56Contract +from algokit_utils.models.network import AlgoClientConfig, AlgoClientConfigs +from algokit_utils.protocols.application import AlgorandClientProtocol class AlgoSdkClients: @@ -24,21 +31,48 @@ def __init__( self.kmd = kmd +@dataclass(kw_only=True, frozen=True) +class NetworkDetail: + is_test_net: bool + is_main_net: bool + is_local_net: bool + genesis_id: str + genesis_hash: str + + +def genesis_id_is_localnet(genesis_id: str) -> bool: + return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + + +def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: + server = os.getenv(f"{environment_prefix}_SERVER") + if server is None: + raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") + port = os.getenv(f"{environment_prefix}_PORT") + if port: + parsed = parse.urlparse(server) + server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() + return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) + + class ClientManager: - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients): + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClientProtocol): if isinstance(clients_or_configs, AlgoSdkClients): _clients = clients_or_configs elif isinstance(clients_or_configs, AlgoClientConfigs): _clients = AlgoSdkClients( - algod=get_algod_client(clients_or_configs.algod_config), - indexer=get_indexer_client(clients_or_configs.indexer_config) + algod=ClientManager.get_algod_client(clients_or_configs.algod_config), + indexer=ClientManager.get_indexer_client(clients_or_configs.indexer_config) if clients_or_configs.indexer_config else None, - kmd=get_kmd_client(clients_or_configs.kmd_config) if clients_or_configs.kmd_config else None, + kmd=ClientManager.get_kmd_client(clients_or_configs.kmd_config) + if clients_or_configs.kmd_config + else None, ) self._algod = _clients.algod self._indexer = _clients.indexer self._kmd = _clients.kmd + self._algorand = algorand_client @property def algod(self) -> AlgodClient: @@ -52,6 +86,10 @@ def indexer(self) -> IndexerClient: raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured") return self._indexer + @property + def indexer_if_present(self) -> IndexerClient | None: + return self._indexer + @property def kmd(self) -> KMDClient: """Returns an algosdk KMD API client or raises an error if it's not been provided.""" @@ -59,6 +97,25 @@ def kmd(self) -> KMDClient: raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured") return self._kmd + def network(self) -> NetworkDetail: + sp = self._algod.suggested_params() # TODO: cache it + return NetworkDetail( + is_test_net=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], + is_main_net=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], + is_local_net=ClientManager.genesis_id_is_local_net(str(sp.gen)), + genesis_id=str(sp.gen), + genesis_hash=sp.gh, + ) + + def is_local_net(self) -> bool: + return self.network().is_local_net + + def is_test_net(self) -> bool: + return self.network().is_test_net + + def is_main_net(self) -> bool: + return self.network().is_main_net + def get_testnet_dispenser( self, auth_token: str | None = None, request_timeout: int | None = None ) -> TestNetDispenserApiClient: @@ -66,3 +123,175 @@ def get_testnet_dispenser( return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout) return TestNetDispenserApiClient(auth_token=auth_token) + + def get_app_factory( + self, + app_spec: Arc56Contract | ApplicationSpecification | str, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + version: str | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + deploy_time_params: TealTemplateParams | None = None, + ) -> AppFactory: + if not self._algorand: + raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") + + return AppFactory( + AppFactoryParams( + algorand=self._algorand, + app_spec=app_spec, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + version=version, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) + + @staticmethod + def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: + """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment + + If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" + config = config or _get_config_from_environment("ALGOD") + headers = {"X-Algo-API-Token": config.token or ""} + return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers) + + @staticmethod + def get_algod_client_from_environment() -> AlgodClient: + return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) + + @staticmethod + def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment + + If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" + config = config or _get_config_from_environment("KMD") + return KMDClient(config.token, config.server) + + @staticmethod + def get_kmd_client_from_environment() -> KMDClient: + return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) + + @staticmethod + def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: + """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. + + If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and + `INDEXER_TOKEN`""" + config = config or _get_config_from_environment("INDEXER") + headers = {"X-Indexer-API-Token": config.token} + return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers) + + @staticmethod + def get_indexer_client_from_environment() -> IndexerClient: + return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) + + @staticmethod + def genesis_id_is_local_net(genesis_id: str) -> bool: + return genesis_id_is_localnet(genesis_id) + + @staticmethod + def get_config_from_environment_or_localnet() -> AlgoClientConfigs: + """Retrieve client configuration from environment variables or fallback to localnet defaults. + + If ALGOD_SERVER is set in environment variables, it will use environment configuration, + otherwise it will use default localnet configuration. + + Returns: + AlgoClientConfigs: Configuration for algod, indexer, and optionally kmd + """ + algod_server = os.getenv("ALGOD_SERVER") + + if algod_server: + # Use environment configuration + algod_config = ClientManager.get_algod_config_from_environment() + + # Only include indexer if INDEXER_SERVER is set + indexer_config = ( + ClientManager.get_indexer_config_from_environment() if os.getenv("INDEXER_SERVER") else None + ) + + # Include KMD config only for local networks (not mainnet/testnet) + kmd_config = ( + ClientManager.get_kmd_config_from_environment() + if not any(net in algod_server.lower() for net in ["mainnet", "testnet"]) + else None + ) + else: + # Use localnet defaults + algod_config = ClientManager.get_default_local_net_config("algod") + indexer_config = ClientManager.get_default_local_net_config("indexer") + kmd_config = ClientManager.get_default_local_net_config("kmd") + + return AlgoClientConfigs( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config, + ) + + @staticmethod + def get_default_local_net_config(config_or_port: Literal["algod", "indexer", "kmd"] | int) -> AlgoClientConfig: + port = ( + config_or_port + if isinstance(config_or_port, int) + else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port] + ) + + return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) + + @staticmethod + def get_algod_config_from_environment() -> AlgoClientConfig: + """Retrieve the algod configuration from environment variables. + + Expects ALGOD_SERVER to be defined in environment variables. + ALGOD_PORT and ALGOD_TOKEN are optional. + + Raises: + ValueError: If ALGOD_SERVER environment variable is not set + """ + return _get_config_from_environment("ALGOD") + + @staticmethod + def get_indexer_config_from_environment() -> AlgoClientConfig: + """Retrieve the indexer configuration from environment variables. + + Expects INDEXER_SERVER to be defined in environment variables. + INDEXER_PORT and INDEXER_TOKEN are optional. + + Raises: + ValueError: If INDEXER_SERVER environment variable is not set + """ + return _get_config_from_environment("INDEXER") + + @staticmethod + def get_kmd_config_from_environment() -> AlgoClientConfig: + """Retrieve the kmd configuration from environment variables. + + Expects KMD_SERVER to be defined in environment variables. + KMD_PORT and KMD_TOKEN are optional. + """ + return _get_config_from_environment("KMD") + + @staticmethod + def get_algonode_config( + network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"] + ) -> AlgoClientConfig: + """Returns the Algorand configuration to point to the free tier of the AlgoNode service. + + Args: + network: Which network to connect to - TestNet or MainNet + config: Which algod config to return - Algod or Indexer + + Returns: + AlgoClientConfig: Configuration for the specified network and service + """ + service_type = "api" if config == "algod" else "idx" + return AlgoClientConfig( + server=f"https://{network}-{service_type}.algonode.cloud", + port=443, + ) diff --git a/src/algokit_utils/clients/dispenser_api_client.py b/src/algokit_utils/clients/dispenser_api_client.py index 66593e80..b8a3ef78 100644 --- a/src/algokit_utils/clients/dispenser_api_client.py +++ b/src/algokit_utils/clients/dispenser_api_client.py @@ -1,12 +1,13 @@ import contextlib import enum -import logging import os from dataclasses import dataclass import httpx -logger = logging.getLogger(__name__) +from algokit_utils.config import config + +logger = config.logger class DispenserApiConfig: diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index 55850fd0..f76704ce 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -2,6 +2,7 @@ import os from collections.abc import Callable from pathlib import Path +from typing import Any logger = logging.getLogger(__name__) @@ -10,6 +11,50 @@ ALGOKIT_CONFIG_FILENAME = ".algokit.toml" +class AlgoKitLogger: + def __init__(self) -> None: + self._logger = logging.getLogger("algokit") + self._setup_logger() + + def _setup_logger(self) -> None: + formatter = logging.Formatter("%(levelname)s: %(message)s") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + self._logger.addHandler(handler) + self._logger.setLevel(logging.INFO) + + def _get_logger(self, *, suppress_log: bool = False) -> logging.Logger: + if suppress_log: + null_logger = logging.getLogger("null") + null_logger.addHandler(logging.NullHandler()) + return null_logger + return self._logger + + def error(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an error message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).error(message, *args, **kwargs) + + def exception(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an exception message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).exception(message, *args, **kwargs) + + def warning(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a warning message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).warning(message, *args, **kwargs) + + def info(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log an info message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).info(message, *args, **kwargs) + + def debug(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a debug message, optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs) + + def verbose(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None: + """Log a verbose message (maps to debug), optionally suppressing output""" + self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs) + + class UpdatableConfig: """Class to manage and update configuration settings for the AlgoKit project. @@ -19,26 +64,33 @@ class UpdatableConfig: trace_all (bool): Indicates whether to trace all operations. trace_buffer_size_mb (int): The size of the trace buffer in megabytes. max_search_depth (int): The maximum depth to search for a specific file. + populate_app_call_resources (bool): Indicates whether to populate app call resources. """ def __init__(self) -> None: + self._logger = AlgoKitLogger() self._debug: bool = False self._project_root: Path | None = None self._trace_all: bool = False self._trace_buffer_size_mb: int | float = 256 # megabytes self._max_search_depth: int = 10 + self._populate_app_call_resources: bool = False self._configure_project_root() def _configure_project_root(self) -> None: """Configures the project root by searching for a specific file within a depth limit.""" current_path = Path(__file__).resolve() for _ in range(self._max_search_depth): - logger.debug(f"Searching in: {current_path}") + self.logger.debug(f"Searching in: {current_path}") if (current_path / ALGOKIT_CONFIG_FILENAME).exists(): self._project_root = current_path break current_path = current_path.parent + @property + def logger(self) -> AlgoKitLogger: + return self._logger + @property def debug(self) -> bool: """Returns the debug status.""" @@ -59,6 +111,10 @@ def trace_buffer_size_mb(self) -> int | float: """Returns the size of the trace buffer in megabytes.""" return self._trace_buffer_size_mb + @property + def populate_app_call_resource(self) -> bool: + return self._populate_app_call_resources + def with_debug(self, func: Callable[[], str | None]) -> None: """Executes a function with debug mode temporarily enabled.""" original_debug = self._debug @@ -68,7 +124,7 @@ def with_debug(self, func: Callable[[], str | None]) -> None: finally: self._debug = original_debug - def configure( # noqa: PLR0913 + def configure( self, *, debug: bool, diff --git a/src/algokit_utils/errors/logic_error.py b/src/algokit_utils/errors/logic_error.py new file mode 100644 index 00000000..24fb40a0 --- /dev/null +++ b/src/algokit_utils/errors/logic_error.py @@ -0,0 +1,129 @@ +import base64 +import dataclasses +import re +from collections.abc import Callable +from copy import copy +from typing import TYPE_CHECKING, TypedDict + +from algosdk.atomic_transaction_composer import ( + SimulateAtomicTransactionResponse, +) + +if TYPE_CHECKING: + from algosdk.source_map import SourceMap as AlgoSourceMap + +__all__ = [ + "LogicError", + "parse_logic_error", +] + +LOGIC_ERROR = ( + ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" +) + +DEFAULT_BLAST_RADIUS = 5 + + +class LogicErrorData(TypedDict): + transaction_id: str + message: str + pc: int + + +@dataclasses.dataclass +class SimulationTrace: + app_budget_added: int | None + app_budget_consumed: int | None + failure_message: str | None + exec_trace: dict[str, object] + + +def parse_logic_error( + error_str: str, +) -> LogicErrorData | None: + match = re.match(LOGIC_ERROR, error_str) + if match is None: + return None + + return { + "transaction_id": match.group("transaction_id"), + "message": match.group("message"), + "pc": int(match.group("pc")), + } + + +class LogicError(Exception): + def __init__( + self, + *, + logic_error_str: str, + program: str, + source_map: "AlgoSourceMap | None", + transaction_id: str, + message: str, + pc: int, + logic_error: Exception | None = None, + traces: list[SimulationTrace] | None = None, + get_line_for_pc: Callable[[int], int | None] | None = None, + ): + self.logic_error = logic_error + self.logic_error_str = logic_error_str + try: + self.program = base64.b64decode(program).decode("utf-8") + except Exception: + self.program = program + self.source_map = source_map + self.lines = self.program.split("\n") + self.transaction_id = transaction_id + self.message = message + self.pc = pc + self.traces = traces + self.line_no = ( + self.source_map.get_line_for_pc(self.pc) + if self.source_map + else get_line_for_pc(self.pc) + if get_line_for_pc + else None + ) + + def __str__(self) -> str: + return ( + f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" + + (":" if self.line_no is None else f" and Source Line {self.line_no}:") + + f"\n{self.trace()}" + ) + + def trace(self, lines: int = 5) -> str: + if self.line_no is None: + return """ +Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the +error please provide an approval SourceMap. Either by: + 1.Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR + 2.Set approval_source_map from a previously compiled approval program OR + 3.Import a previously exported source map using import_source_map""" + + program_lines = copy(self.lines) + program_lines[self.line_no] += "\t\t<-- Error" + lines_before = max(0, self.line_no - lines) + lines_after = min(len(program_lines), self.line_no + lines) + return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) + + +def create_simulate_traces_for_logic_error(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: + traces = [] + if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget_added = txn_group.get("app-budget-added", None) + app_budget_consumed = txn_group.get("app-budget-consumed", None) + failure_message = txn_group.get("failure-message", None) + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + traces.append( + SimulationTrace( + app_budget_added=app_budget_added, + app_budget_consumed=app_budget_consumed, + failure_message=failure_message, + exec_trace=exec_trace, + ) + ) + return traces diff --git a/src/algokit_utils/models/abi.py b/src/algokit_utils/models/abi.py index 767eed09..4e837274 100644 --- a/src/algokit_utils/models/abi.py +++ b/src/algokit_utils/models/abi.py @@ -1,4 +1,14 @@ +from typing import TypeAlias + +import algosdk + +from algokit_utils.models.application import StructField + ABIPrimitiveValue = bool | int | str | bytes | bytearray # NOTE: This is present in js-algorand-sdk, but sadly not in untyped py-algorand-sdk -ABIValue = ABIPrimitiveValue | list["ABIValue"] | dict[str, "ABIValue"] +ABIValue: TypeAlias = ABIPrimitiveValue | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] +ABIStruct: TypeAlias = dict[str, list[StructField]] + + +ABIType: TypeAlias = algosdk.abi.ABIType diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py index 3014b7af..f83cc1e2 100644 --- a/src/algokit_utils/models/account.py +++ b/src/algokit_utils/models/account.py @@ -3,6 +3,8 @@ import algosdk from algosdk.atomic_transaction_composer import AccountTransactionSigner +DISPENSER_ACCOUNT_NAME = "DISPENSER" + @dataclasses.dataclass(kw_only=True) class Account: @@ -15,7 +17,7 @@ class Account: def __post_init__(self) -> None: if not self.address: - self.address = algosdk.account.address_from_private_key(self.private_key) + self.address = str(algosdk.account.address_from_private_key(self.private_key)) @property def public_key(self) -> bytes: diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index ac86cd3b..adb7ffae 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -121,3 +121,27 @@ def __ge__(self, other: object) -> bool: elif isinstance(other, int | Decimal): return self.amount_in_micro_algo >= int(other) raise TypeError(f"Unsupported operand type(s) for >=: 'AlgoAmount' and '{type(other).__name__}'") + + def __sub__(self, other: int | Decimal | AlgoAmount) -> AlgoAmount: + if isinstance(other, AlgoAmount): + total_micro_algos = self.micro_algos - other.micro_algos + elif isinstance(other, (int | Decimal)): + total_micro_algos = self.micro_algos - int(other) + else: + raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") + return AlgoAmount.from_micro_algos(total_micro_algos) + + def __rsub__(self, other: int | Decimal) -> AlgoAmount: + if isinstance(other, (int | Decimal)): + total_micro_algos = int(other) - self.micro_algos + return AlgoAmount.from_micro_algos(total_micro_algos) + raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'") + + def __isub__(self, other: int | Decimal | AlgoAmount) -> Self: + if isinstance(other, AlgoAmount): + self.amount_in_micro_algo -= other.micro_algos + elif isinstance(other, (int | Decimal)): + self.amount_in_micro_algo -= int(other) + else: + raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") + return self diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py index c68e78af..6ab5d0ff 100644 --- a/src/algokit_utils/models/application.py +++ b/src/algokit_utils/models/application.py @@ -1,5 +1,469 @@ +import json +from dataclasses import asdict, dataclass, field, is_dataclass +from typing import Any, Literal, TypeAlias + +import algosdk + UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" """The name of the TEAL template variable for deploy-time immutability control.""" DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" """The name of the TEAL template variable for deploy-time permanence control.""" + + +# ===== ARCs ===== + +# Define type aliases +ABITypeAlias: TypeAlias = str +ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType +StructName: TypeAlias = str +AVMBytes = Literal["AVMBytes"] +AVMString = Literal["AVMString"] +AVMUint64 = Literal["AVMUint64"] +AVMType = AVMBytes | AVMString | AVMUint64 +OnCompleteAction = Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"] +DefaultValueSource = Literal["box", "global", "local", "literal", "method"] + + +def convert_key_to_snake_case(name: str) -> str: + import re + + return re.sub(r"(? Any: # noqa: ANN401 + if isinstance(obj, dict): + return {convert_key_to_snake_case(k): convert_keys_to_snake_case(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_keys_to_snake_case(item) for item in obj] + return obj + + +class SerializableBaseClass: + """ + A base class that provides a generic `dictify` method to convert dataclass instances + into dictionaries recursively. + """ + + def to_dict(self) -> dict[str, Any]: + def serialize(obj: Any) -> dict[str, Any] | list[Any] | Any: # noqa: ANN401 + if is_dataclass(obj) and not isinstance(obj, type): + return {k: serialize(v) for k, v in asdict(obj).items()} + elif isinstance(obj, algosdk.abi.ABIType): + return str(obj) + elif isinstance(obj, list): + return [serialize(item) for item in obj] + elif isinstance(obj, dict): + return {k: serialize(v) for k, v in obj.items()} + else: + return obj + + result = serialize(self) + if not isinstance(result, dict): + raise TypeError("Serialized object is not a dictionary.") + return result + + +@dataclass +class CallConfig: + no_op: str | None = None + opt_in: str | None = None + close_out: str | None = None + clear_state: str | None = None + update_application: str | None = None + delete_application: str | None = None + + +@dataclass(kw_only=True) +class StructField: + name: str + type: ABITypeAlias | StructName | list["StructField"] + + +@dataclass(kw_only=True) +class StorageKey: + desc: str | None + key_type: ABITypeAlias | AVMType | StructName + value_type: ABITypeAlias | AVMType | StructName + key: str # base64 encoded bytes + + +@dataclass(kw_only=True) +class StorageMap: + desc: str | None + key_type: ABITypeAlias | AVMType | StructName + value_type: ABITypeAlias | AVMType | StructName + prefix: str | None # base64-encoded prefix + + +@dataclass(kw_only=True) +class DefaultValue: + data: str + type: ABITypeAlias | AVMType | None = None + source: DefaultValueSource + + +@dataclass(kw_only=True) +class MethodArg: + type: ABITypeAlias + struct: StructName | None = None + name: str | None = None + desc: str | None = None + default_value: DefaultValue | None = None + + +@dataclass +class MethodReturns: + type: ABITypeAlias + struct: StructName | None = None + desc: str | None = None + + +@dataclass(kw_only=True) +class MethodActions: + create: list[Literal["NoOp", "OptIn", "DeleteApplication"]] + call: list[Literal["NoOp", "OptIn", "CloseOut", "ClearState", "UpdateApplication", "DeleteApplication"]] + + +@dataclass(kw_only=True) +class BoxRecommendation: + app: int | None = None + key: str = "" + read_bytes: int = 0 + write_bytes: int = 0 + + +@dataclass(kw_only=True) +class Recommendations: + inner_transaction_count: int | None = None + boxes: list[BoxRecommendation] | None = None + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + + +@dataclass(kw_only=True) +class Method(SerializableBaseClass): + name: str + desc: str | None = None + args: list[MethodArg] = field(default_factory=list) + returns: MethodReturns = field(default_factory=lambda: MethodReturns(type="void")) + actions: MethodActions = field(default_factory=lambda: MethodActions(create=[], call=[])) + readonly: bool | None = False + events: list["Event"] | None = None + recommendations: Recommendations | None = None + + +@dataclass(kw_only=True) +class EventArg: + type: ABITypeAlias + name: str | None = None + desc: str | None = None + struct: StructName | None = None + + +@dataclass(kw_only=True) +class Event: + name: str + desc: str | None = None + args: list[EventArg] = field(default_factory=list) + + +@dataclass(kw_only=True) +class CompilerVersion: + major: int + minor: int + patch: int + commit_hash: str | None = None + + +@dataclass(kw_only=True) +class CompilerInfo: + compiler: Literal["algod", "puya"] + compiler_version: CompilerVersion + + +@dataclass +class SourceInfoDetail: + pc: list[int] + error_message: str | None = None + teal: int | None = None + source: str | None = None + + +@dataclass(kw_only=True) +class ProgramSourceInfo: + source_info: list[SourceInfoDetail] + pc_offset_method: Literal["none", "cblocks"] + + @staticmethod + def from_json(source_info: str | dict) -> "ProgramSourceInfo": + if "source_info" not in source_info: + raise ValueError("source_info is required") + source_dict: dict = json.loads(source_info) if isinstance(source_info, str) else source_info + parsed_source_dict = [SourceInfoDetail(**detail) for detail in source_dict["source_info"]] + return ProgramSourceInfo(source_info=parsed_source_dict, pc_offset_method=source_dict["pc_offset_method"]) + + +@dataclass(kw_only=True) +class Arc56ContractState: + keys: dict[str, dict[str, StorageKey]] + maps: dict[str, dict[str, StorageMap]] + schemas: dict[str, dict[str, int]] + + +@dataclass(kw_only=True) +class Arc56MethodArg: + """Represents an ARC-56 method argument with ABI type conversion.""" + + name: str | None = None + desc: str | None = None + struct: StructName | None = None + default_value: DefaultValue | None = None + type: ABIArgumentType + + @classmethod + def from_method_arg(cls, arg: MethodArg, converted_type: ABIArgumentType) -> "Arc56MethodArg": + """Create an Arc56MethodArg from a MethodArg with converted type.""" + return cls( + name=arg.name, + desc=arg.desc, + struct=arg.struct, + default_value=arg.default_value, + type=converted_type, + ) + + +@dataclass(kw_only=True) +class Arc56MethodReturnType: + """Represents an ARC-56 method return type with ABI type conversion.""" + + type: algosdk.abi.ABIType | Literal["void"] # Can be 'void' or ABIType + struct: StructName | None = None + desc: str | None = None + + +class Arc56Method(SerializableBaseClass, algosdk.abi.Method): + def __init__(self, method: Method) -> None: + # First, create the parent class with original arguments + super().__init__( + name=method.name, + args=method.args, # type: ignore[arg-type] + returns=algosdk.abi.Returns(arg_type=method.returns.type, desc=method.returns.desc), + desc=method.desc, + ) + self.method = method + + # Store our custom Arc56MethodArg list separately + + self._arc56_args = [ + Arc56MethodArg.from_method_arg( + arg, + algosdk.abi.ABIType.from_string(arg.type) + if not self._is_transaction_or_reference_type(arg.type) and isinstance(arg.type, str) + else arg.type, # type: ignore[arg-type] + ) + for arg in method.args + ] + + # Convert returns similar to TypeScript implementation, including struct support + converted_return_type: Literal["void"] | algosdk.abi.ABIType + if method.returns.type == "void": + converted_return_type = "void" + else: + converted_return_type = algosdk.abi.ABIType.from_string(str(method.returns.type)) + + self._arc56_returns = Arc56MethodReturnType( + type=converted_return_type, + struct=method.returns.struct, + desc=method.returns.desc, + ) + + def _is_transaction_or_reference_type(self, type_str: str) -> bool: + return type_str in [ + algosdk.constants.ASSETCONFIG_TXN, + algosdk.constants.PAYMENT_TXN, + algosdk.constants.KEYREG_TXN, + algosdk.constants.ASSETFREEZE_TXN, + algosdk.constants.ASSETTRANSFER_TXN, + algosdk.constants.APPCALL_TXN, + algosdk.constants.STATEPROOF_TXN, + algosdk.abi.ABIReferenceType.APPLICATION, + algosdk.abi.ABIReferenceType.ASSET, + algosdk.abi.ABIReferenceType.ACCOUNT, + ] + + @property + def arc56_args(self) -> list[Arc56MethodArg]: + """Get the ARC-56 specific argument representations.""" + return self._arc56_args + + @property + def arc56_returns(self) -> Arc56MethodReturnType: + """Get the ARC-56 specific returns type, including struct information.""" + return self._arc56_returns + + +@dataclass(kw_only=True) +class Arc56Contract(SerializableBaseClass): + arcs: list[int] + name: str + desc: str | None = None + networks: dict[str, dict[str, int]] | None = None + structs: dict[StructName, list[StructField]] = field(default_factory=dict) + methods: list[Method] = field(default_factory=list) + state: Arc56ContractState + bare_actions: dict[str, list[OnCompleteAction]] = field(default_factory=dict) + source_info: dict[str, ProgramSourceInfo] | None = None + source: dict[str, str] | None = None + byte_code: dict[str, str] | None = None + compiler_info: CompilerInfo | None = None + events: list[Event] | None = None + template_variables: dict[str, dict[str, ABITypeAlias | AVMType | StructName | str]] | None = None + scratch_variables: dict[str, dict[str, int | ABITypeAlias | AVMType | StructName]] | None = None + + @staticmethod + def from_json(application_spec: str | dict) -> "Arc56Contract": + """Convert a JSON dictionary into an Arc56Contract instance. + + Args: + json_data (dict): The JSON data representing an Arc56Contract + + Returns: + Arc56Contract: The constructed Arc56Contract instance + """ + # Convert networks if present + json_data = json.loads(application_spec) if isinstance(application_spec, str) else application_spec + json_data = convert_keys_to_snake_case(json_data) + networks = json_data.get("networks") + + # Convert structs + structs = { + name: [StructField(**field) if isinstance(field, dict) else field for field in struct_fields] + for name, struct_fields in json_data.get("structs", {}).items() + } + + # Convert methods + methods = [] + for method_data in json_data.get("methods", []): + # Convert method args + args = [MethodArg(**arg) for arg in method_data.get("args", [])] + + # Convert method returns + returns_data = method_data.get("returns", {"type": "void"}) + returns = MethodReturns(**returns_data) + + # Convert method actions + actions_data = method_data.get("actions", {"create": [], "call": []}) + actions = MethodActions(**actions_data) + + # Convert events if present + events = None + if "events" in method_data: + events = [Event(**event) for event in method_data["events"]] + + # Convert recommendations if present + recommendations = None + if "recommendations" in method_data: + recommendations = Recommendations(**method_data["recommendations"]) + + methods.append( + Method( + name=method_data["name"], + desc=method_data.get("desc"), + args=args, + returns=returns, + actions=actions, + readonly=method_data.get("readonly", False), + events=events, + recommendations=recommendations, + ) + ) + + # Convert state + state_data = json_data["state"] + state = Arc56ContractState( + keys={ + category: {name: StorageKey(**key_data) for name, key_data in keys.items()} + for category, keys in state_data.get("keys", {}).items() + }, + maps={ + category: {name: StorageMap(**map_data) for name, map_data in maps.items()} + for category, maps in state_data.get("maps", {}).items() + }, + schemas=state_data.get("schema", {}), + ) + + # Convert compiler info if present + compiler_info = None + if "compiler_info" in json_data: + compiler_version = CompilerVersion(**json_data["compiler_info"]["compiler_version"]) + compiler_info = CompilerInfo( + compiler=json_data["compiler_info"]["compiler"], compiler_version=compiler_version + ) + + # Convert events if present + events = None + if "events" in json_data: + events = [Event(**event) for event in json_data["events"]] + + source_info = {} + if "source_info" in json_data: + source_info = {key: ProgramSourceInfo.from_json(val) for key, val in json_data["source_info"].items()} + + return Arc56Contract( + arcs=json_data.get("arcs", []), + name=json_data["name"], + desc=json_data.get("desc"), + networks=networks, + structs=structs, + methods=methods, + state=state, + bare_actions=json_data.get("bare_actions", {}), + source_info=source_info, + source=json_data.get("source"), + byte_code=json_data.get("byte_code"), + compiler_info=compiler_info, + events=events, + template_variables=json_data.get("template_variables"), + scratch_variables=json_data.get("scratch_variables"), + ) + + +@dataclass(kw_only=True, frozen=True) +class AppState: + key_raw: bytes + key_base64: str + value_raw: bytes | None + value_base64: str | None + value: str | int + + +@dataclass(kw_only=True, frozen=True) +class AppInformation: + app_id: int + app_address: str + approval_program: bytes + clear_state_program: bytes + creator: str + global_state: dict[str, AppState] + local_ints: int + local_byte_slices: int + global_ints: int + global_byte_slices: int + extra_program_pages: int | None + + +@dataclass(kw_only=True, frozen=True) +class CompiledTeal: + teal: str + compiled: bytes + compiled_hash: str + compiled_base64_to_bytes: bytes + source_map: algosdk.source_map.SourceMap | None + + +@dataclass(kw_only=True, frozen=True) +class AppCompilationResult: + compiled_approval: CompiledTeal + compiled_clear: CompiledTeal diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py new file mode 100644 index 00000000..8ee897e2 --- /dev/null +++ b/src/algokit_utils/models/network.py @@ -0,0 +1,20 @@ +import dataclasses + + +@dataclasses.dataclass +class AlgoClientConfig: + """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or + {py:class}`algosdk.v2client.indexer.IndexerClient`""" + + server: str + """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + token: str | None = None + """API Token to authenticate with the service""" + port: str | int | None = None + + +@dataclasses.dataclass +class AlgoClientConfigs: + algod_config: AlgoClientConfig + indexer_config: AlgoClientConfig | None + kmd_config: AlgoClientConfig | None diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py new file mode 100644 index 00000000..ca8c0844 --- /dev/null +++ b/src/algokit_utils/models/transaction.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass(kw_only=True, frozen=True) +class SendParams: + max_rounds_to_wait: int | None = None + suppress_log: bool | None = None + populate_app_call_resources: bool | None = None diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/algokit_utils/protocols/application.py b/src/algokit_utils/protocols/application.py new file mode 100644 index 00000000..c4782162 --- /dev/null +++ b/src/algokit_utils/protocols/application.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Protocol + +from typing_extensions import runtime_checkable + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + from algokit_utils.applications.app_deployer import AppDeployer + from algokit_utils.applications.app_manager import AppManager + from algokit_utils.clients.client_manager import ClientManager + from algokit_utils.transactions.transaction_composer import TransactionComposer + from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator + from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender + + +@dataclass +class NetworkDetails: + genesis_id: str + genesis_hash: str + network_name: str + + +@runtime_checkable +class AlgorandClientProtocol(Protocol): + @property + def app(self) -> AppManager: ... + + @property + def app_deployer(self) -> AppDeployer: ... + + @property + def send(self) -> AlgorandClientTransactionSender: ... + + @property + def create_transaction(self) -> AlgorandClientTransactionCreator: ... + + def new_group(self) -> TransactionComposer: ... + + @property + def client(self) -> ClientManager: ... + + +@runtime_checkable +class ClientManagerProtocol(Protocol): + @property + def algod(self) -> AlgodClient: ... + + @property + def indexer(self) -> IndexerClient | None: ... + + async def network(self) -> NetworkDetails: ... + + async def is_local_net(self) -> bool: ... + + async def is_test_net(self) -> bool: ... + + async def is_main_net(self) -> bool: ... diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py index 251bbf96..33edd94c 100644 --- a/src/algokit_utils/transactions/models.py +++ b/src/algokit_utils/transactions/models.py @@ -1,4 +1,6 @@ -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, TypeVar, cast + +import algosdk # Define specific types for different formats @@ -28,3 +30,51 @@ class JsonFormatArc2Note(BaseArc2Note): TransactionNoteData = str | None | int | list[Any] | dict[str, Any] TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote + +T = TypeVar("T") + + +class TransactionWrapper(algosdk.transaction.Transaction): + """Wrapper around algosdk.transaction.Transaction with optional property validators""" + + def __init__(self, transaction: algosdk.transaction.Transaction) -> None: + self._raw = transaction + + @property + def raw(self) -> algosdk.transaction.Transaction: + return self._raw + + @property + def payment(self) -> algosdk.transaction.PaymentTxn | None: + return self._return_if_type( + algosdk.transaction.PaymentTxn, + ) + + @property + def keyreg(self) -> algosdk.transaction.KeyregTxn | None: + return self._return_if_type(algosdk.transaction.KeyregTxn) + + @property + def asset_config(self) -> algosdk.transaction.AssetConfigTxn | None: + return self._return_if_type(algosdk.transaction.AssetConfigTxn) + + @property + def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn | None: + return self._return_if_type(algosdk.transaction.AssetTransferTxn) + + @property + def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn | None: + return self._return_if_type(algosdk.transaction.AssetFreezeTxn) + + @property + def application_call(self) -> algosdk.transaction.ApplicationCallTxn | None: + return self._return_if_type(algosdk.transaction.ApplicationCallTxn) + + @property + def state_proof(self) -> algosdk.transaction.StateProofTxn | None: + return self._return_if_type(algosdk.transaction.StateProofTxn) + + def _return_if_type(self, txn_type: type[T]) -> T | None: + if isinstance(self._raw, txn_type): + return cast(T, self._raw) + return None diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 77dea2e9..7d66c939 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,47 +1,52 @@ from __future__ import annotations -import logging import math from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Union import algosdk import algosdk.atomic_transaction_composer +import algosdk.v2client.models from algosdk.atomic_transaction_composer import ( AtomicTransactionComposer, TransactionSigner, TransactionWithSigner, ) from algosdk.error import AlgodHTTPError -from algosdk.transaction import OnComplete, Transaction +from algosdk.transaction import OnComplete from algosdk.v2client.algod import AlgodClient -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response from algokit_utils.applications.app_manager import AppManager from algokit_utils.config import config +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.models import TransactionWrapper +from algokit_utils.transactions.utils import encode_lease, populate_app_call_resources if TYPE_CHECKING: from collections.abc import Callable from algosdk.abi import Method - from algosdk.box_reference import BoxReference from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.models import SimulateTraceConfig + from algokit_utils.applications.app_manager import BoxReference from algokit_utils.models.abi import ABIValue from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.models import Arc2TransactionNote -logger = logging.getLogger(__name__) +logger = config.logger -@dataclass(frozen=True) + +@dataclass(kw_only=True, frozen=True) class SenderParam: sender: str -@dataclass(frozen=True) -class CommonTxnParams: +@dataclass(kw_only=True, frozen=True) +class CommonTxnParams(SendParams): """ Common transaction parameters. @@ -72,14 +77,10 @@ class CommonTxnParams: last_valid_round: int | None = None -@dataclass(frozen=True) -class _RequiredPaymentParams: - receiver: str - amount: AlgoAmount - - -@dataclass(frozen=True) -class PaymentParams(CommonTxnParams, _RequiredPaymentParams): +@dataclass(kw_only=True, frozen=True) +class PaymentParams( + CommonTxnParams, +): """ Payment transaction parameters. @@ -88,21 +89,14 @@ class PaymentParams(CommonTxnParams, _RequiredPaymentParams): :param close_remainder_to: If given, close the sender account and send the remaining balance to this address. """ + receiver: str + amount: AlgoAmount close_remainder_to: str | None = None -@dataclass(frozen=True) -class _RequiredAssetCreateParams: - total: int - asset_name: str - unit_name: str - url: str - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetCreateParams( CommonTxnParams, - _RequiredAssetCreateParams, ): """ Asset creation parameters. @@ -123,6 +117,10 @@ class AssetCreateParams( :param metadata_hash: Hash of the metadata contained in the metadata URL. """ + total: int + asset_name: str | None = None + unit_name: str | None = None + url: str | None = None decimals: int | None = None default_frozen: bool | None = None manager: str | None = None @@ -132,15 +130,9 @@ class AssetCreateParams( metadata_hash: bytes | None = None -@dataclass(frozen=True) -class _RequiredAssetConfigParams: - asset_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetConfigParams( CommonTxnParams, - _RequiredAssetConfigParams, ): """ Asset configuration parameters. @@ -155,23 +147,16 @@ class AssetConfigParams( Clawback will be permanently disabled if undefined or an empty string. """ + asset_id: int manager: str | None = None reserve: str | None = None freeze: str | None = None clawback: str | None = None -@dataclass(frozen=True) -class _RequiredAssetFreezeParams: - asset_id: int - account: str - frozen: bool - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetFreezeParams( CommonTxnParams, - _RequiredAssetFreezeParams, ): """ Asset freeze parameters. @@ -181,16 +166,14 @@ class AssetFreezeParams( :param frozen: Whether the assets in the account should be frozen. """ - -@dataclass(frozen=True) -class _RequiredAssetDestroyParams: asset_id: int + account: str + frozen: bool -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetDestroyParams( CommonTxnParams, - _RequiredAssetDestroyParams, ): """ Asset destruction parameters. @@ -198,20 +181,12 @@ class AssetDestroyParams( :param asset_id: ID of the asset. """ - -@dataclass(frozen=True) -class _RequiredOnlineKeyRegistrationParams: - vote_key: str - selection_key: str - vote_first: int - vote_last: int - vote_key_dilution: int + asset_id: int -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class OnlineKeyRegistrationParams( CommonTxnParams, - _RequiredOnlineKeyRegistrationParams, ): """ Online key registration parameters. @@ -227,20 +202,17 @@ class OnlineKeyRegistrationParams( :param state_proof_key: The 64 byte state proof public key commitment. """ + vote_key: str + selection_key: str + vote_first: int + vote_last: int + vote_key_dilution: int state_proof_key: bytes | None = None -@dataclass(frozen=True) -class _RequiredAssetTransferParams: - asset_id: int - amount: int - receiver: str - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetTransferParams( CommonTxnParams, - _RequiredAssetTransferParams, ): """ Asset transfer parameters. @@ -252,19 +224,16 @@ class AssetTransferParams( :param close_asset_to: The account to close the asset to. """ + asset_id: int + amount: int + receiver: str clawback_target: str | None = None close_asset_to: str | None = None -@dataclass(frozen=True) -class _RequiredAssetOptInParams: - asset_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetOptInParams( CommonTxnParams, - _RequiredAssetOptInParams, ): """ Asset opt-in parameters. @@ -272,24 +241,22 @@ class AssetOptInParams( :param asset_id: ID of the asset. """ - -@dataclass(frozen=True) -class _RequiredAssetOptOutParams: asset_id: int - creator: str -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AssetOptOutParams( CommonTxnParams, - _RequiredAssetOptOutParams, ): """ Asset opt-out parameters. """ + asset_id: int + creator: str -@dataclass(frozen=True) + +@dataclass(kw_only=True, frozen=True) class AppCallParams(CommonTxnParams, SenderParam): """ Application call parameters. @@ -320,14 +287,8 @@ class AppCallParams(CommonTxnParams, SenderParam): box_references: list[BoxReference] | None = None -@dataclass(frozen=True) -class _RequiredAppCreateParams: - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): +@dataclass(kw_only=True, frozen=True) +class AppCreateParams(CommonTxnParams, SenderParam): """ Application create parameters. @@ -345,6 +306,8 @@ class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): :param extra_program_pages: Number of extra pages required for the programs """ + approval_program: str | bytes + clear_state_program: str | bytes schema: dict[str, int] | None = None on_complete: OnComplete | None = None args: list[bytes] | None = None @@ -355,15 +318,8 @@ class AppCreateParams(CommonTxnParams, SenderParam, _RequiredAppCreateParams): extra_program_pages: int | None = None -@dataclass(frozen=True) -class _RequiredAppUpdateParams: - app_id: int - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): +@dataclass(kw_only=True, frozen=True) +class AppUpdateParams(CommonTxnParams, SenderParam): """ Application update parameters. @@ -374,6 +330,9 @@ class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): teal (bytes) """ + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes args: list[bytes] | None = None account_references: list[str] | None = None app_references: list[int] | None = None @@ -382,16 +341,10 @@ class AppUpdateParams(CommonTxnParams, SenderParam, _RequiredAppUpdateParams): on_complete: OnComplete | None = None -@dataclass(frozen=True) -class _RequiredAppDeleteParams: - app_id: int - - -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteParams( CommonTxnParams, SenderParam, - _RequiredAppDeleteParams, ): """ Application delete parameters. @@ -400,19 +353,20 @@ class AppDeleteParams( """ app_id: int + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference] | None = None on_complete: OnComplete = OnComplete.DeleteApplicationOC -@dataclass(frozen=True) -class _RequiredMethodCallParams: - app_id: int - method: Method - - -@dataclass(frozen=True) -class AppMethodCall(CommonTxnParams, SenderParam, _RequiredMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppMethodCall(CommonTxnParams, SenderParam): """Base class for ABI method calls.""" + app_id: int + method: Method args: list | None = None account_references: list[str] | None = None app_references: list[int] | None = None @@ -420,14 +374,8 @@ class AppMethodCall(CommonTxnParams, SenderParam, _RequiredMethodCallParams): box_references: list[BoxReference] | None = None -@dataclass(frozen=True) -class _RequiredAppMethodCallParams: - app_id: int - method: Method - - -@dataclass(frozen=True) -class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppMethodCallParams(CommonTxnParams, SenderParam): """ Method call parameters. @@ -437,6 +385,8 @@ class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallPa :param on_complete: The OnComplete action (cannot be UpdateApplication or ClearState) """ + app_id: int + method: Method args: list[bytes] | None = None on_complete: OnComplete | None = None account_references: list[str] | None = None @@ -445,7 +395,7 @@ class AppMethodCallParams(CommonTxnParams, SenderParam, _RequiredAppMethodCallPa box_references: list[BoxReference] | None = None -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppCallMethodCall(AppMethodCall): """Parameters for a regular ABI method call. @@ -464,14 +414,8 @@ class AppCallMethodCall(AppMethodCall): on_complete: OnComplete | None = None -@dataclass(frozen=True) -class _RequiredAppCreateMethodCallParams: - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppCreateMethodCall(AppMethodCall, _RequiredAppCreateMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppCreateMethodCall(AppMethodCall): """Parameters for an ABI method call that creates an application. :param approval_program: The program to execute for all OnCompletes other than ClearState @@ -481,20 +425,15 @@ class AppCreateMethodCall(AppMethodCall, _RequiredAppCreateMethodCallParams): :param extra_program_pages: Number of extra pages required for the programs """ + approval_program: str | bytes + clear_state_program: str | bytes schema: dict[str, int] | None = None on_complete: OnComplete | None = None extra_program_pages: int | None = None -@dataclass(frozen=True) -class _RequiredAppUpdateMethodCallParams: - app_id: int - approval_program: str | bytes - clear_state_program: str | bytes - - -@dataclass(frozen=True) -class AppUpdateMethodCall(AppMethodCall, _RequiredAppUpdateMethodCallParams): +@dataclass(kw_only=True, frozen=True) +class AppUpdateMethodCall(AppMethodCall): """Parameters for an ABI method call that updates an application. :param app_id: ID of the application @@ -502,10 +441,13 @@ class AppUpdateMethodCall(AppMethodCall, _RequiredAppUpdateMethodCallParams): :param clear_state_program: The program to execute for ClearState OnComplete """ + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes on_complete: OnComplete = OnComplete.UpdateApplicationOC -@dataclass(frozen=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteMethodCall(AppMethodCall): """Parameters for an ABI method call that deletes an application. @@ -548,7 +490,7 @@ class AppDeleteMethodCall(AppMethodCall): ] -@dataclass +@dataclass(frozen=True) class BuiltTransactions: """ Set of transactions built by TransactionComposer. @@ -574,26 +516,27 @@ class TransactionComposerBuildResult: class SendAtomicTransactionComposerResults: """Results from sending an AtomicTransactionComposer transaction group""" - group_id: str | None + group_id: str """The group ID if this was a transaction group""" confirmations: list[algosdk.v2client.algod.AlgodResponseType] """The confirmation info for each transaction""" tx_ids: list[str] """The transaction IDs that were sent""" - transactions: list[Transaction] + transactions: list[TransactionWrapper] """The transactions that were sent""" - returns: list[Any] + returns: list[Any] | list[algosdk.atomic_transaction_composer.ABIResult] """The ABI return values from any ABI method calls""" + simulate_response: dict[str, Any] | None = None -def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 +def send_atomic_transaction_composer( # noqa: C901, PLR0912 atc: AtomicTransactionComposer, algod: AlgodClient, *, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, - suppress_log: bool = False, - populate_resources: bool | None = None, # TODO: implement/clarify # noqa: ARG001 + suppress_log: bool | None = None, + populate_resources: bool | None = None, # TODO: implement/clarify ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group @@ -615,6 +558,13 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 try: # Build transactions transactions_with_signer = atc.build_group() + + if populate_resources or ( + config.populate_app_call_resource + and any(isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer) + ): + atc = populate_app_call_resources(atc, algod) + transactions_to_send = [t.txn for t in transactions_with_signer] # Get group ID if multiple transactions @@ -652,11 +602,11 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 # Return results return SendAtomicTransactionComposerResults( - group_id=group_id, + group_id=group_id or "", confirmations=confirmations or [], tx_ids=[t.get_txid() for t in transactions_to_send], - transactions=transactions_to_send, - returns=[r.return_value for r in result.abi_results], + transactions=[TransactionWrapper(t) for t in transactions_to_send], + returns=result.abi_results, ) except AlgodHTTPError as e: @@ -720,7 +670,7 @@ class TransactionComposer: NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() - def __init__( # noqa: PLR0913 + def __init__( self, algod: AlgodClient, get_signer: Callable[[str], TransactionSigner], @@ -884,7 +834,7 @@ def build_transactions(self) -> BuiltTransactions: return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) - @deprecated(reason="Use send() instead", version="3.0.0") + @deprecated("Use send() instead") def execute( self, *, @@ -898,8 +848,8 @@ def send( self, *, max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool = False, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: group = self.build().transactions @@ -920,18 +870,78 @@ def send( except algosdk.error.AlgodHTTPError as e: raise Exception(f"Transaction failed: {e}") from e - def simulate(self) -> algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse: + def simulate( + self, + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + round: int | None = None, # noqa: A002 TODO: revisit + skip_signatures: int | None = None, + fix_signers: bool | None = None, + ) -> SendAtomicTransactionComposerResults: + atc = AtomicTransactionComposer() if skip_signatures else self.atc + + if skip_signatures: + allow_empty_signatures = True + fix_signers = True + transactions = self.build_transactions() + for txn in transactions.transactions: + atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer.NULL_SIGNER)) + atc.method_dict = transactions.method_calls + else: + self.build() + if config.debug and config.project_root and config.trace_all: - return simulate_and_persist_response( - self.atc, + response = simulate_and_persist_response( + atc, config.project_root, self.algod, config.trace_buffer_size_mb, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) + + return SendAtomicTransactionComposerResults( + confirmations=[], # TODO: extract confirmations, + transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) - return simulate_response( - self.atc, + response = simulate_response( + atc, self.algod, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + round, + skip_signatures, + fix_signers, + ) + + confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ + "txn-results" + ] + + return SendAtomicTransactionComposerResults( + confirmations=[txn["txn-result"] for txn in confirmation_results], + transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) @staticmethod @@ -966,7 +976,7 @@ def _common_txn_build_step( suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: if params.lease: - txn.lease = params.lease + txn.lease = encode_lease(params.lease) if params.rekey_to: txn.rekey_to = params.rekey_to if params.note: @@ -1002,53 +1012,55 @@ def _build_method_call( # noqa: C901, PLR0912 arg_offset = 0 if params.args: - for i, arg in enumerate(params.args): + for _, arg in enumerate(params.args): if self._is_abi_value(arg): method_args.append(arg) continue - if algosdk.abi.is_abi_transaction_type(params.method.args[i + arg_offset].type): - match arg: - case ( - AppCreateMethodCall() - | AppCallMethodCall() - | AppUpdateMethodCall() - | AppDeleteMethodCall() - ): - temp_txn_with_signers = self._build_method_call(arg, suggested_params) - method_args.extend(temp_txn_with_signers) - arg_offset += len(temp_txn_with_signers) - 1 - continue - case AppCallParams(): - txn = self._build_app_call(arg, suggested_params) - case PaymentParams(): - txn = self._build_payment(arg, suggested_params) - case AssetOptInParams(): - txn = self._build_asset_transfer( - AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params - ) - case AssetCreateParams(): - txn = self._build_asset_create(arg, suggested_params) - case AssetConfigParams(): - txn = self._build_asset_config(arg, suggested_params) - case AssetDestroyParams(): - txn = self._build_asset_destroy(arg, suggested_params) - case AssetFreezeParams(): - txn = self._build_asset_freeze(arg, suggested_params) - case AssetTransferParams(): - txn = self._build_asset_transfer(arg, suggested_params) - case OnlineKeyRegistrationParams(): - txn = self._build_key_reg(arg, suggested_params) - case _: - raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + if isinstance(arg, TransactionWithSigner): + method_args.append(arg) + continue + if isinstance(arg, algosdk.transaction.Transaction): + # Wrap in TransactionWithSigner method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + TransactionWithSigner(txn=arg, signer=params.signer or self.get_signer(params.sender)) ) - continue + match arg: + case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): + temp_txn_with_signers = self._build_method_call(arg, suggested_params) + method_args.extend(temp_txn_with_signers) + arg_offset += len(temp_txn_with_signers) - 1 + continue + case AppCallParams(): + txn = self._build_app_call(arg, suggested_params) + case PaymentParams(): + txn = self._build_payment(arg, suggested_params) + case AssetOptInParams(): + txn = self._build_asset_transfer( + AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params + ) + case AssetCreateParams(): + txn = self._build_asset_create(arg, suggested_params) + case AssetConfigParams(): + txn = self._build_asset_config(arg, suggested_params) + case AssetDestroyParams(): + txn = self._build_asset_destroy(arg, suggested_params) + case AssetFreezeParams(): + txn = self._build_asset_freeze(arg, suggested_params) + case AssetTransferParams(): + txn = self._build_asset_transfer(arg, suggested_params) + case OnlineKeyRegistrationParams(): + txn = self._build_key_reg(arg, suggested_params) + case _: + raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + + method_args.append( + TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) + ) - raise ValueError(f"Unsupported method arg: {arg!s}") + continue method_atc = AtomicTransactionComposer() @@ -1059,10 +1071,18 @@ def _build_method_call( # noqa: C901, PLR0912 sp=suggested_params, signer=params.signer or self.get_signer(params.sender), method_args=method_args, - on_complete=algosdk.transaction.OnComplete.NoOpOC, + on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC, note=params.note, lease=params.lease, - boxes=[(ref.app_index, ref.name) for ref in params.box_references] if params.box_references else None, + boxes=[AppManager.get_box_reference(ref) for ref in params.box_references] + if params.box_references + else None, + foreign_apps=params.app_references, + foreign_assets=params.asset_references, + accounts=params.account_references, + approval_program=params.approval_program if hasattr(params, "approval_program") else None, # type: ignore[arg-type] + clear_program=params.clear_state_program if hasattr(params, "clear_state_program") else None, # type: ignore[arg-type] + rekey_to=params.rekey_to, ) return self._build_atc(method_atc) @@ -1088,13 +1108,13 @@ def _build_asset_create( sp=suggested_params, total=params.total, default_frozen=params.default_frozen or False, - unit_name=params.unit_name, - asset_name=params.asset_name, + unit_name=params.unit_name or "", + asset_name=params.asset_name or "", manager=params.manager, reserve=params.reserve, freeze=params.freeze, clawback=params.clawback, - url=params.url, + url=params.url or "", metadata_hash=params.metadata_hash, decimals=params.decimals or 0, ) @@ -1103,10 +1123,10 @@ def _build_asset_create( def _build_app_call( self, - params: AppCallParams | AppUpdateParams | AppCreateParams, + params: AppCallParams | AppUpdateParams | AppCreateParams | AppDeleteParams, suggested_params: algosdk.transaction.SuggestedParams, ) -> algosdk.transaction.Transaction: - app_id = params.app_id if isinstance(params, AppCallParams | AppUpdateMethodCall) else None + app_id = getattr(params, "app_id", 0) approval_program = None clear_program = None @@ -1148,12 +1168,12 @@ def _build_app_call( txn = algosdk.transaction.ApplicationCreateTxn( **sdk_params, global_schema=algosdk.transaction.StateSchema( - num_uints=params.schema.get("global_uints", 0), - num_byte_slices=params.schema.get("global_byte_slices", 0), + num_uints=params.schema.get("global_ints", 0), + num_byte_slices=params.schema.get("global_bytes", 0), ), local_schema=algosdk.transaction.StateSchema( - num_uints=params.schema.get("local_uints", 0), - num_byte_slices=params.schema.get("local_byte_slices", 0), + num_uints=params.schema.get("local_ints", 0), + num_byte_slices=params.schema.get("local_bytes", 0), ), extra_pages=params.extra_program_pages or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) @@ -1239,7 +1259,7 @@ def _build_key_reg( return self._common_txn_build_step(params, txn, suggested_params) def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool: - if isinstance(x, list): + if isinstance(x, list | tuple): return len(x) == 0 or all(self._is_abi_value(item) for item in x) return isinstance(x, bool | int | float | str | bytes) @@ -1254,6 +1274,9 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 return [txn] case AtomicTransactionComposer(): return self._build_atc(txn) + case algosdk.transaction.Transaction(): + signer = self.get_signer(txn.sender) + return [TransactionWithSigner(txn=txn, signer=signer)] case AppCreateMethodCall() | AppCallMethodCall() | AppUpdateMethodCall() | AppDeleteMethodCall(): return self._build_method_call(txn, suggested_params) @@ -1266,7 +1289,7 @@ def _build_txn( # noqa: C901, PLR0912, PLR0911 case AssetCreateParams(): asset_create = self._build_asset_create(txn, suggested_params) return [TransactionWithSigner(txn=asset_create, signer=signer)] - case AppCallParams() | AppUpdateParams() | AppCreateParams(): + case AppCallParams() | AppUpdateParams() | AppCreateParams() | AppDeleteParams(): app_call = self._build_app_call(txn, suggested_params) return [TransactionWithSigner(txn=app_call, signer=signer)] case AssetConfigParams(): diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 3050dcb7..831100d2 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,16 +1,16 @@ from collections.abc import Callable from dataclasses import dataclass -from logging import getLogger from typing import Any, TypedDict, TypeVar import algosdk import algosdk.atomic_transaction_composer -from algosdk.atomic_transaction_composer import AtomicTransactionResponse +from algosdk.atomic_transaction_composer import ABIResult, AtomicTransactionResponse from algosdk.transaction import Transaction from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager -from algokit_utils.models.abi import ABIValue +from algokit_utils.config import config +from algokit_utils.transactions.models import TransactionWrapper from algokit_utils.transactions.transaction_composer import ( AppCallMethodCall, AppCallParams, @@ -33,48 +33,45 @@ TxnParams, ) -logger = getLogger(__name__) +logger = config.logger -@dataclass +@dataclass(frozen=True, kw_only=True) class SendSingleTransactionResult: - tx_id: str # Single transaction ID (last from txIds array) - transaction: Transaction # Last transaction + transaction: TransactionWrapper # Last transaction confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation # Fields from SendAtomicTransactionComposerResults group_id: str + tx_id: str | None = None tx_ids: list[str] # Full array of transaction IDs - transactions: list[Transaction] + transactions: list[TransactionWrapper] confirmations: list[algosdk.v2client.algod.AlgodResponseType] returns: list[algosdk.atomic_transaction_composer.ABIResult] | None = None - # Fields from AssetCreateParams - asset_id: int | None = None +@dataclass(frozen=True, kw_only=True) +class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): + asset_id: int -@dataclass + +@dataclass(frozen=True) class SendAppTransactionResult(SendSingleTransactionResult): - return_value: ABIValue | None = None + return_value: ABIResult | None = None -@dataclass +@dataclass(frozen=True) class SendAppUpdateTransactionResult(SendAppTransactionResult): compiled_approval: Any | None = None compiled_clear: Any | None = None -@dataclass -class _RequiredSendAppTransactionResult: +@dataclass(frozen=True, kw_only=True) +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult): app_id: int app_address: str -@dataclass -class SendAppCreateTransactionResult(SendAppUpdateTransactionResult, _RequiredSendAppTransactionResult): - pass - - class LogConfig(TypedDict, total=False): pre_log: Callable[[TxnParams, Transaction], str] post_log: Callable[[TxnParams, AtomicTransactionResponse], str] @@ -116,11 +113,14 @@ def send_transaction(params: T) -> SendSingleTransactionResult: logger.debug(pre_log(params, transaction)) raw_result = composer.send() + raw_result_dict = raw_result.__dict__.copy() + raw_result_dict["transactions"] = raw_result.transactions + del raw_result_dict["simulate_response"] result = SendSingleTransactionResult( - **raw_result.__dict__, + **raw_result_dict, confirmation=raw_result.confirmations[-1], - transaction=raw_result.transactions[-1], + transaction=raw_result_dict["transactions"][-1], tx_id=raw_result.tx_ids[-1], ) @@ -210,7 +210,7 @@ def payment(self, params: PaymentParams) -> SendSingleTransactionResult: ), )(params) - def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult: + def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransactionResult: """Create a new Algorand Standard Asset.""" result = self._send( lambda c: c.add_asset_create, @@ -223,11 +223,10 @@ def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult ), )(params) - result = SendSingleTransactionResult( + return SendSingleAssetCreateTransactionResult( **result.__dict__, + asset_id=int(result.confirmation["asset-index"]), # type: ignore[call-overload] ) - result.asset_id = int(result.confirmation["asset-index"]) # type: ignore[call-overload] - return result def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: """Configure an existing Algorand Standard Asset.""" diff --git a/src/algokit_utils/transactions/utils.py b/src/algokit_utils/transactions/utils.py new file mode 100644 index 00000000..216db262 --- /dev/null +++ b/src/algokit_utils/transactions/utils.py @@ -0,0 +1,302 @@ +from typing import Any, cast + +from algosdk import logic, transaction +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, EmptySigner, TransactionWithSigner +from algosdk.box_reference import BoxReference +from algosdk.error import AtomicTransactionComposerError +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.models import SimulateRequest + +# Constants +MAX_APP_CALL_ACCOUNT_REFERENCES = 4 +MAX_APP_CALL_FOREIGN_REFERENCES = 8 + + +def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: # noqa: C901, PLR0915, PLR0912 + """ + Populate application call resources based on simulation results. + """ + # Get unnamed resources from simulation + unnamed_resources = get_unnamed_app_call_resources_accessed(atc, algod) + group = atc.build_group() + + # Process transaction-level resources + for i, txn_resources in enumerate(unnamed_resources["txns"]): + if not txn_resources or not isinstance(group[i].txn, transaction.ApplicationCallTxn): + continue + + # Validate no unexpected resources + if txn_resources.get("boxes") or txn_resources.get("extraBoxRefs"): + raise ValueError("Unexpected boxes at the transaction level") + if txn_resources.get("appLocals"): + raise ValueError("Unexpected app local at the transaction level") + if txn_resources.get("assetHoldings"): + raise ValueError("Unexpected asset holding at the transaction level") + + # Update application call fields + app_txn = cast(transaction.ApplicationCallTxn, group[i].txn) + accounts = list(getattr(app_txn, "accounts", []) or []) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + boxes = list(getattr(app_txn, "boxes", []) or []) + + # Add new resources + accounts.extend(txn_resources.get("accounts", [])) + foreign_apps.extend(txn_resources.get("apps", [])) + foreign_assets.extend(txn_resources.get("assets", [])) + boxes.extend(txn_resources.get("boxes", [])) + + # Validate limits + if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES: + raise ValueError( + f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}" + ) + + total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes) + if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES: + raise ValueError( + f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}" + ) + + # Update transaction + app_txn.accounts = accounts + app_txn.foreign_apps = foreign_apps + app_txn.foreign_assets = foreign_assets + app_txn.boxes = boxes + + def populate_group_resource( # noqa: C901, PLR0915 + txns: list[TransactionWithSigner], reference: str | BoxReference | dict[str, Any] | int, ref_type: str + ) -> None: + """Helper function to populate group-level resources""" + + def is_appl_below_limit(t: TransactionWithSigner) -> bool: + if not isinstance(t.txn, transaction.ApplicationCallTxn): + return False + + app_txn = t.txn + accounts = len(app_txn.accounts or []) + assets = len(app_txn.foreign_assets or []) + apps = len(app_txn.foreign_apps or []) + boxes = len(app_txn.boxes or []) + + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Handle asset holding and app local references + if ref_type in ("assetHolding", "appLocal"): + ref_dict = cast(dict[str, Any], reference) + account = ref_dict["account"] + + # Try to find transaction with account already available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + account in (getattr(t.txn, "accounts", []) or []) + or account + in ( + logic.get_application_address(app_id) + for app_id in (getattr(t.txn, "foreign_apps", []) or []) + ) + or any(account in str(v) for v in t.txn.__dict__.values()) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + if ref_type == "assetHolding": + asset_id = ref_dict["asset"] + app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id] + else: + app_id = ref_dict["app"] + app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] + return + + # Find available transaction for the resource + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES + if ref_type == "account" + else True + ) + ), + -1, + ) + + if txn_idx == -1: + raise ValueError("No more transactions below reference limit. Add another app call to the group.") + + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + + # Add resource based on type + if ref_type == "account": + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(cast(str, reference)) + app_txn.accounts = accounts + elif ref_type == "app": + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(int(cast(str | int, reference))) + app_txn.foreign_apps = foreign_apps + elif ref_type == "box": + box_ref = cast(BoxReference, reference) + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(box_ref) + app_txn.boxes = boxes + if box_ref.app_index != 0: + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(box_ref.app_index) + app_txn.foreign_apps = foreign_apps + elif ref_type == "asset": + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(int(cast(str | int, reference))) + app_txn.foreign_assets = foreign_assets + elif ref_type == "assetHolding": + ref_dict = cast(dict[str, Any], reference) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(ref_dict["asset"]) + app_txn.foreign_assets = foreign_assets + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + elif ref_type == "appLocal": + ref_dict = cast(dict[str, Any], reference) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(ref_dict["app"]) + app_txn.foreign_apps = foreign_apps + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + + # Process group-level resources + group_resources = unnamed_resources["group"] + if group_resources: + # Handle cross-reference resources first + for app_local in group_resources.get("appLocals", []): + populate_group_resource(group, app_local, "appLocal") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != app_local["account"] + ] + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])] + + for asset_holding in group_resources.get("assetHoldings", []): + populate_group_resource(group, asset_holding, "assetHolding") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != asset_holding["account"] + ] + if "assets" in group_resources: + group_resources["assets"] = [ + asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"]) + ] + + # Handle remaining resources + for account in group_resources.get("accounts", []): + populate_group_resource(group, account, "account") + + for box in group_resources.get("boxes", []): + populate_group_resource(group, box, "box") + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box.app_index)] + + for asset in group_resources.get("assets", []): + populate_group_resource(group, asset, "asset") + + for app in group_resources.get("apps", []): + populate_group_resource(group, app, "app") + + # Handle extra box references + extra_box_refs = group_resources.get("extraBoxRefs", 0) + for _ in range(extra_box_refs): + empty_box = BoxReference(0, b"") + populate_group_resource(group, empty_box, "box") + + # Create new ATC with updated transactions + new_atc = AtomicTransactionComposer() + for txn_with_signer in group: + txn_with_signer.txn.group = None + new_atc.add_transaction(txn_with_signer) + + # Copy method calls + new_atc.method_dict = atc.method_dict.copy() + + return new_atc + + +def get_unnamed_app_call_resources_accessed(atc: AtomicTransactionComposer, algod: AlgodClient) -> dict[str, Any]: + """Get unnamed resources accessed by application calls in an atomic transaction group.""" + # Create simulation request with required flags + simulate_request = SimulateRequest( + txn_groups=[], allow_unnamed_resources=True, allow_empty_signatures=True, extra_opcode_budget=0 + ) + + # Create empty signer + null_signer = EmptySigner() + + # Clone the ATC and replace signers + empty_signer_atc = atc.clone() + for txn in empty_signer_atc.txn_list: + txn.signer = null_signer + + # Run simulation + result = empty_signer_atc.simulate(algod, simulate_request) + + # Get first group response + group_response = result.simulate_response["txn-groups"][0] + + # Check for simulation failure + if group_response.get("failure-message"): + failed_at = group_response.get("failed-at", [0])[0] + raise AtomicTransactionComposerError( + f"Error during resource population simulation in transaction {failed_at}: " + f"{group_response['failure-message']}" + ) + + # Return resources accessed at group and transaction level + return { + "group": group_response.get("unnamed-resources-accessed", {}), + "txns": [txn.get("unnamed-resources-accessed", {}) for txn in group_response.get("txn-results", [])], + } + + +MAX_LEASE_LENGTH = 32 + + +def encode_lease(lease: str | bytes | None) -> bytes | None: + if lease is None: + return None + elif isinstance(lease, bytes): + if not (1 <= len(lease) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received bytes with length {len(lease)}" + ) + if len(lease) == MAX_LEASE_LENGTH: + return lease + lease32 = bytearray(32) + lease32[: len(lease)] = lease + return bytes(lease32) + elif isinstance(lease, str): + encoded = lease.encode("utf-8") + if not (1 <= len(encoded) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received '{lease}' with length {len(lease)}" + ) + lease32 = bytearray(MAX_LEASE_LENGTH) + lease32[: len(encoded)] = encoded + return bytes(lease32) + else: + raise TypeError(f"Unknown lease type received of {type(lease)}") diff --git a/tests/accounts/__init__.py b/tests/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py new file mode 100644 index 00000000..ec56a007 --- /dev/null +++ b/tests/accounts/test_account_manager.py @@ -0,0 +1,108 @@ +import algosdk +import pytest + +from algokit_utils import Account +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.amount import AlgoAmount +from tests.conftest import get_unique_name + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +def test_new_account_is_retrieved_and_funded(algorand: AlgorandClient) -> None: + # Act + account_name = get_unique_name() + account = algorand.account.from_environment(account_name) + + # Assert + account_info = algorand.account.get_information(account.address) + assert account_info["amount"] > 0 + + +def test_same_account_is_subsequently_retrieved(algorand: AlgorandClient) -> None: + # Arrange + account_name = get_unique_name() + + # Act + account1 = algorand.account.from_environment(account_name) + account2 = algorand.account.from_environment(account_name) + + # Assert - accounts should be different objects but with same underlying keys + assert account1 is not account2 + assert account1.address == account2.address + assert account1.private_key == account2.private_key + + +def test_environment_is_used_in_preference_to_kmd(algorand: AlgorandClient, monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange + account_name = get_unique_name() + account1 = algorand.account.from_environment(account_name) + + # Set up environment variable for second account + env_account_name = "TEST_ACCOUNT" + monkeypatch.setenv(f"{env_account_name}_MNEMONIC", algosdk.mnemonic.from_private_key(account1.private_key)) + + # Act + account2 = algorand.account.from_environment(env_account_name) + + # Assert - accounts should be different objects but with same underlying keys + assert account1 is not account2 + assert account1.address == account2.address + assert account1.private_key == account2.private_key + + +def test_random_account_creation(algorand: AlgorandClient) -> None: + # Act + account = algorand.account.random() + + # Assert + assert account.address + assert account.private_key + assert len(account.public_key) == 32 + + +def test_ensure_funded_from_environment(algorand: AlgorandClient) -> None: + # Arrange + account = algorand.account.random() + min_balance = AlgoAmount.from_algos(1) + + # Act + result = algorand.account.ensure_funded_from_environment( + account_to_fund=account.address, + min_spending_balance=min_balance, + ) + + # Assert + assert result is not None + assert result.amount_funded is not None + account_info = algorand.account.get_information(account.address) + assert account_info["amount"] >= min_balance.micro_algos + + +def test_get_account_information(algorand: AlgorandClient) -> None: + # Arrange + account = algorand.account.random() + + # Act + info = algorand.account.get_information(account.address) + + # Assert + assert isinstance(info, dict) + assert "amount" in info + assert "min-balance" in info + assert "address" in info + assert info["address"] == account.address diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py new file mode 100644 index 00000000..c246924a --- /dev/null +++ b/tests/applications/test_app_client.py @@ -0,0 +1,733 @@ +import base64 +import json +import random +from pathlib import Path +from typing import Any + +import algosdk +import pytest +from algosdk.atomic_transaction_composer import TransactionSigner, TransactionWithSigner + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallWithSendParams, + AppClientParams, + FundAppAccountParams, +) +from algokit_utils.applications.app_manager import AppManager, BoxReference +from algokit_utils.applications.utils import arc32_to_arc56, get_arc56_method +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.abi import ABIType +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.application import Arc56Contract +from algokit_utils.transactions.transaction_composer import AppCreateParams, PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def raw_hello_world_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = hello_world_arc32_app_spec.global_state_schema + local_schema = hello_world_arc32_app_spec.local_state_schema + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=hello_world_arc32_app_spec.approval_program, + clear_state_program=hello_world_arc32_app_spec.clear_program, + schema={ + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def raw_testing_app_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def testing_app_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, testing_app_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_arc32_app_spec.global_state_schema + local_schema = testing_app_arc32_app_spec.local_state_schema + approval = AppManager.replace_template_variables( + testing_app_arc32_app_spec.approval_program, + { + "VALUE": 1, + "UPDATABLE": 0, + "DELETABLE": 0, + }, + ) + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval, + clear_state_program=testing_app_arc32_app_spec.clear_program, + schema={ + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_spec: ApplicationSpecification, + testing_app_arc32_app_id: int, +) -> AppClient: + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_arc32_app_id, + algorand=algorand, + app_spec=testing_app_arc32_app_spec, + ) + ) + + +@pytest.fixture +def test_app_client_with_sourcemaps( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_spec: ApplicationSpecification, + testing_app_arc32_app_id: int, +) -> AppClient: + sourcemaps = json.loads( + (Path(__file__).parent.parent / "artifacts" / "testing_app" / "sources.teal.map.json").read_text() + ) + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_arc32_app_id, + algorand=algorand, + approval_source_map=algosdk.source_map.SourceMap(sourcemaps["approvalSourceMap"]), + clear_source_map=algosdk.source_map.SourceMap(sourcemaps["clearSourceMap"]), + app_spec=testing_app_arc32_app_spec, + ) + ) + + +@pytest.fixture +def testing_app_puya_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app_puya" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_puya_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, testing_app_puya_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_puya_arc32_app_spec.global_state_schema + local_schema = testing_app_puya_arc32_app_spec.local_state_schema + + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=testing_app_puya_arc32_app_spec.approval_program, + clear_state_program=testing_app_puya_arc32_app_spec.clear_program, + schema={ + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client_puya( + algorand: AlgorandClient, + funded_account: Account, + testing_app_puya_arc32_app_spec: ApplicationSpecification, + testing_app_puya_arc32_app_id: int, +) -> AppClient: + return AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=testing_app_puya_arc32_app_id, + algorand=algorand, + app_spec=testing_app_puya_arc32_app_spec, + ) + ) + + +# TODO: add variations around arc 56 contracts too + + +def test_clone_overriding_default_sender_and_inheriting_app_name( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_default_sender = "ABC" * 55 + cloned_app_client = app_client.clone(default_sender=cloned_default_sender) + + assert app_client.app_name == "HelloWorld" + assert cloned_app_client.app_id == app_client.app_id + assert cloned_app_client.app_name == app_client.app_name + assert cloned_app_client._default_sender == cloned_default_sender # noqa: SLF001 + assert app_client._default_sender == funded_account.address # noqa: SLF001 + + +def test_clone_overriding_app_name( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_app_name = "George CLONEy" + cloned_app_client = app_client.clone(app_name=cloned_app_name) + assert app_client.app_name == hello_world_arc32_app_spec.contract.name == "HelloWorld" + assert cloned_app_client.app_name == cloned_app_name + + +def test_clone_inheriting_app_name_based_on_default_handling( + algorand: AlgorandClient, + funded_account: Account, + hello_world_arc32_app_spec: ApplicationSpecification, + hello_world_arc32_app_id: int, +) -> None: + app_client = AppClient( + AppClientParams( + default_sender=funded_account.address, + default_signer=funded_account.signer, + app_id=hello_world_arc32_app_id, + algorand=algorand, + app_spec=hello_world_arc32_app_spec, + ) + ) + + cloned_app_name = None + cloned_app_client = app_client.clone(app_name=cloned_app_name) + assert cloned_app_client.app_name == hello_world_arc32_app_spec.contract.name == app_client.app_name + + +def test_normalise_app_spec( + raw_hello_world_arc32_app_spec: str, + hello_world_arc32_app_spec: ApplicationSpecification, +) -> None: + normalized_app_spec_from_arc32 = AppClient.normalise_app_spec(hello_world_arc32_app_spec) + assert isinstance(normalized_app_spec_from_arc32, Arc56Contract) + + normalize_app_spec_from_raw_arc32 = AppClient.normalise_app_spec(raw_hello_world_arc32_app_spec) + assert isinstance(normalize_app_spec_from_raw_arc32, Arc56Contract) + + +def test_resolve_from_network( + algorand: AlgorandClient, + hello_world_arc32_app_id: int, + hello_world_arc32_app_spec: ApplicationSpecification, +) -> None: + arc56_app_spec = arc32_to_arc56(hello_world_arc32_app_spec) + arc56_app_spec.networks = {"localnet": {"app_id": hello_world_arc32_app_id}} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + ) + + assert app_client + + +def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: + call = test_app_client.create_transaction.call( + AppClientMethodCallWithSendParams( + method="call_abi", + args=["test"], + box_references=[BoxReference(app_id=0, name=b"1")], + ) + ) + + assert isinstance(call.transactions[0], algosdk.transaction.ApplicationCallTxn) + assert call.transactions[0].boxes == [BoxReference(app_id=0, name=b"1")] + + # Test with string box reference + call2 = test_app_client.create_transaction.call( + AppClientMethodCallWithSendParams( + method="call_abi", + args=["test"], + box_references=["1"], + ) + ) + + assert isinstance(call2.transactions[0], algosdk.transaction.ApplicationCallTxn) + assert call2.transactions[0].boxes == [BoxReference(app_id=0, name=b"1")] + + +def test_construct_transaction_with_abi_encoding_including_transaction( + algorand: AlgorandClient, funded_account: Account, test_app_client: AppClient +) -> None: + # Create a payment transaction with random amount + amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + payment_txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=amount, + ) + ) + + # Call the ABI method with the payment transaction + result = test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[payment_txn, "test"], + ) + ) + + assert result.confirmation + assert len(result.transactions) == 2 + return_value = AppManager.get_abi_return( + result.confirmation, get_arc56_method("call_abi_txn", test_app_client.app_spec) + ) + expected_return = f"Sent {amount.micro_algos}. test" + assert result.return_value + assert result.return_value.return_value == expected_return + assert return_value + assert return_value.return_value == result.return_value.return_value + + +def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + # Create a payment transaction with a random amount + amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=amount, + ) + ) + + called_indexes = [] + original_signer = algorand.account.get_signer(funded_account.address) + + class IndexCapturingSigner(TransactionSigner): + def sign_transactions( + self, txn_group: list[algosdk.transaction.Transaction], indexes: list[int] + ) -> list[algosdk.transaction.GenericSignedTransaction]: + called_indexes.extend(indexes) + return original_signer.sign_transactions(txn_group, indexes) + + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[txn, "test"], + sender=funded_account.address, + signer=IndexCapturingSigner(), + ) + ) + + assert called_indexes == [0, 1] + + +def test_sign_transaction_in_group_with_different_signer_if_provided( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + # Generate a new account + test_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(10), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + # Fund the account with 1 Algo + txn = algorand.create_transaction.payment( + PaymentParams( + sender=test_account.address, + receiver=test_account.address, + amount=AlgoAmount.from_algos(random.randint(1, 5)), + ) + ) + + # Call method with transaction and signer + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_txn", + args=[TransactionWithSigner(txn=txn, signer=test_account.signer), "test"], + ) + ) + + +def test_construct_transaction_with_abi_encoding_including_foreign_references_not_in_signature( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: Account +) -> None: + test_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(10), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + result = test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="call_abi_foreign_refs", + app_references=[345], + account_references=[test_account.address], + asset_references=[567], + ) + ) + + # Assuming the method returns a string matching the format below + expected_return = AppManager.get_abi_return( + result.confirmations[0], + get_arc56_method("call_abi_foreign_refs", test_app_client.app_spec), + ) + assert result.return_value + assert "App: 345, Asset: 567, Account: " in result.return_value.return_value + assert expected_return + assert expected_return.return_value == result.return_value.return_value + + +def test_retrieve_state(test_app_client: AppClient, funded_account: Account) -> None: + # Test global state + test_app_client.send.call( + AppClientMethodCallWithSendParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) + ) + global_state = test_app_client.get_global_state() + + assert "int1" in global_state + assert "int2" in global_state + assert "bytes1" in global_state + assert "bytes2" in global_state + assert hasattr(global_state["bytes2"], "value_raw") + assert sorted(global_state.keys()) == ["bytes1", "bytes2", "int1", "int2", "value"] + assert global_state["int1"].value == 1 + assert global_state["int2"].value == 2 + assert global_state["bytes1"].value == "asdf" + assert global_state["bytes2"].value_raw == bytes([1, 2, 3, 4]) + + # Test local state + test_app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) + test_app_client.send.call( + AppClientMethodCallWithSendParams(method="set_local", args=[1, 2, "asdf", bytes([1, 2, 3, 4])]) + ) + local_state = test_app_client.get_local_state(funded_account.address) + + assert "local_int1" in local_state + assert "local_int2" in local_state + assert "local_bytes1" in local_state + assert "local_bytes2" in local_state + assert sorted(local_state.keys()) == ["local_bytes1", "local_bytes2", "local_int1", "local_int2"] + assert local_state["local_int1"].value == 1 + assert local_state["local_int2"].value == 2 + assert local_state["local_bytes1"].value == "asdf" + assert local_state["local_bytes2"].value_raw == bytes([1, 2, 3, 4]) + + # Test box storage + box_name1 = bytes([0, 0, 0, 1]) + box_name1_base64 = base64.b64encode(box_name1).decode() + box_name2 = bytes([0, 0, 0, 2]) + box_name2_base64 = base64.b64encode(box_name2).decode() + + test_app_client.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name1, "value1"], + box_references=[box_name1], + ) + ) + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name2, "value2"], + box_references=[box_name2], + ) + ) + + box_values = test_app_client.get_box_values() + box1_value = test_app_client.get_box_value(box_name1) + + assert sorted(b.name.name_base64 for b in box_values) == sorted([box_name1_base64, box_name2_base64]) + box1 = next(b for b in box_values if b.name.name_base64 == box_name1_base64) + assert box1.value == base64.b64encode(bytes("value1", "utf-8")) + assert box1_value == box1.value + + box2 = next(b for b in box_values if b.name.name_base64 == box_name2_base64) + assert box2.value == base64.b64encode(bytes("value2", "utf-8")) + + # Legacy contract strips ABI prefix; manually encoded ABI string after + # passing algosdk's atc results in \x00\n\x00\n1234524352. + expected_value_decoded = "1234524352" + expected_value = "\x00\n" + expected_value_decoded + test_app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_box", + args=[box_name1, expected_value], + box_references=[box_name1], + ) + ) + + boxes = test_app_client.get_box_values_from_abi_type( + ABIType.from_string("string"), + lambda n: n.name_base64 == box_name1_base64, + ) + box1_abi_value = test_app_client.get_box_value_from_abi_type(box_name1, ABIType.from_string("string")) + + assert len(boxes) == 1 + assert boxes[0].value == expected_value_decoded + assert box1_abi_value == expected_value_decoded + + +@pytest.mark.parametrize( + ("box_name", "box_value", "value_type", "expected_value"), + [ + ( + "name1", + b"test_bytes", # Updated to match Bytes type + "byte[]", + [116, 101, 115, 116, 95, 98, 121, 116, 101, 115], + ), + ( + "name2", + "test_string", + "string", + "test_string", + ), + ( + "name3", # Updated to use string key + 123, + "uint32", + 123, + ), + ( + "name4", # Updated to use string key + 2**256, # Large number within uint512 range + "uint512", + 2**256, + ), + ( + "name5", # Updated to use string key + [1, 2, 3, 4], + "byte[4]", + [1, 2, 3, 4], + ), + ], +) +def test_box_methods_with_manually_encoded_abi_args( + test_app_client_puya: AppClient, + box_name: Any, # noqa: ANN401 + box_value: Any, # noqa: ANN401 + value_type: str, + expected_value: Any, # noqa: ANN401 +) -> None: + # Fund the app account + box_prefix = b"box_bytes" + + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + # Encode the box reference + box_identifier = box_prefix + ABIType.from_string("string").encode(box_name) + + # Call the method to set the box value + test_app_client_puya.send.call( + AppClientMethodCallWithSendParams( + method="set_box_bytes", + args=[box_name, ABIType.from_string(value_type).encode(box_value)], + box_references=[box_identifier], + ) + ) + + # Get and verify the box value + box_abi_value = test_app_client_puya.get_box_value_from_abi_type(box_identifier, ABIType.from_string(value_type)) + + # Convert the retrieved value to match expected type if needed + assert box_abi_value == expected_value + + +@pytest.mark.parametrize( + ("box_prefix_str", "method", "arg_value", "value_type"), + [ + ("box_str", "set_box_str", "string", "string"), + ("box_int", "set_box_int", 123, "uint32"), + ("box_int512", "set_box_int512", 2**256, "uint512"), + ("box_static", "set_box_static", [1, 2, 3, 4], "byte[4]"), + ("", "set_struct", ("box1", 123), "(string,uint64)"), + ], +) +def test_box_methods_with_arc4_returns_parametrized( + test_app_client_puya: AppClient, + box_prefix_str: str, + method: str, + arg_value: Any, # noqa: ANN401 + value_type: str, +) -> None: + """ + Test setting and retrieving box values with different data types and box prefixes. + + Args: + test_app_client_puya (AppClient): The AppClient instance for testing. + box_prefix_str (str): The string prefix for the box. + method (str): The method name to call for setting the box. + arg_value (Any): The value to set in the box. + value_type (str): The ABI type of the value. + """ + # Encode the box prefix + box_prefix = box_prefix_str.encode() + + # Fund the app account with 1 Algo + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + + # Encode the box name "box1" using ABIType "string" + box_name_encoded = ABIType.from_string("string").encode("box1") + box_reference = box_prefix + box_name_encoded + + # Send the transaction to set the box value + test_app_client_puya.send.call( + AppClientMethodCallWithSendParams( + method=method, + args=["box1", arg_value], + box_references=[box_reference], + ) + ) + + # Encode the expected value using the specified ABI type + value_encoded = ABIType.from_string(value_type).encode(arg_value) + expected_value = base64.b64encode(value_encoded) + + # Retrieve the actual box value + actual_box_value = test_app_client_puya.get_box_value(box_reference) + + # Assert that the actual box value matches the expected value + assert actual_box_value == expected_value + + if method == "set_struct": + abi_decoded_boxes = test_app_client_puya.get_box_values_from_abi_type( + ABIType.from_string("(string,uint64)"), + lambda n: n.name_base64 == base64.b64encode(box_prefix + box_name_encoded).decode(), + ) + assert len(abi_decoded_boxes) == 1 + assert abi_decoded_boxes[0].value == arg_value + + +# TODO: see if needs moving into app factory tests file +def test_abi_with_default_arg_method( + algorand: AlgorandClient, + funded_account: Account, + testing_app_arc32_app_id: int, + testing_app_arc32_app_spec: ApplicationSpecification, +) -> None: + arc56_app_spec = arc32_to_arc56(testing_app_arc32_app_spec) + arc56_app_spec.networks = {"localnet": {"app_id": testing_app_arc32_app_id}} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + default_sender=funded_account.address, + default_signer=funded_account.signer, + ) + # app_client.send. + app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in")) + app_client.send.call( + AppClientMethodCallWithSendParams( + method="set_local", + args=[1, 2, "banana", [1, 2, 3, 4]], + ) + ) + + method_signature = "default_value_from_local_state(string)string" + defined_value = "defined value" + + # Test with defined value + defined_value_result = app_client.send.call( + AppClientMethodCallWithSendParams(method=method_signature, args=[defined_value]) + ) + + assert defined_value_result.return_value + assert defined_value_result.return_value.return_value == "Local state, defined value" + + # Test with default value + default_value_result = app_client.send.call(AppClientMethodCallWithSendParams(method=method_signature, args=[None])) + assert default_value_result.return_value + assert default_value_result.return_value.return_value == "Local state, banana" + + +def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: + with pytest.raises(LogicError) as exc_info: + test_app_client_with_sourcemaps.send.call(AppClientMethodCallWithSendParams(method="error")) + + error = exc_info.value + assert error.pc == 885 + assert "assert failed pc=885" in str(error) + assert len(error.transaction_id) == 52 + assert error.line_no == 469 diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py new file mode 100644 index 00000000..8cf9e75a --- /dev/null +++ b/tests/applications/test_app_factory.py @@ -0,0 +1,488 @@ +from pathlib import Path + +import algosdk +import pytest +from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete + +from algokit_utils import OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallParams, + AppClientMethodCallWithCompilationAndSendParams, + AppClientMethodCallWithSendParams, + AppClientParams, +) +from algokit_utils.applications.app_factory import ( + AppFactory, + AppFactoryCreateMethodCallParams, + AppFactoryCreateWithSendParams, +) +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def app_spec() -> str: + return (Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json").read_text() + + +@pytest.fixture +def factory(algorand: AlgorandClient, funded_account: Account, app_spec: str) -> AppFactory: + """Create AppFactory fixture""" + return algorand.client.get_app_factory(app_spec=app_spec, default_sender=funded_account.address) + + +@pytest.fixture +def arc56_factory( + algorand: AlgorandClient, + funded_account: Account, +) -> AppFactory: + """Create AppFactory fixture""" + arc56_raw_spec = ( + Path(__file__).parent.parent / "artifacts" / "testing_app_arc56" / "arc56_app_spec.json" + ).read_text() + return algorand.client.get_app_factory(app_spec=arc56_raw_spec, default_sender=funded_account.address) + + +def test_create_app(factory: AppFactory) -> None: + """Test creating an app using the factory""" + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + } + ) + ) + + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + assert result.compiled_approval is not None + assert result.compiled_clear is not None + + +def test_create_app_with_constructor_deploy_time_params(algorand: AlgorandClient, app_spec: str) -> None: + """Test creating an app using the factory with constructor deploy time params""" + random_account = algorand.account.random() + dispenser_account = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + account_to_fund=random_account, + dispenser_account=dispenser_account.address, + min_spending_balance=AlgoAmount.from_algo(10), + min_funding_increment=AlgoAmount.from_algo(1), + ) + + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=random_account.address, + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + ) + + app_client, result = factory.send.bare.create() + + assert result.app_id > 0 + assert app_client.app_id == result.app_id + + +def test_create_app_with_oncomplete_overload(factory: AppFactory) -> None: + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + on_complete=OnComplete.OptInOC, + updatable=True, + deletable=True, + deploy_time_params={ + "VALUE": 1, + }, + ) + ) + + assert result.transaction.application_call + assert result.transaction.application_call.on_complete == OnComplete.OptInOC + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + + +def test_deploy_when_immutable_and_permanent(factory: AppFactory) -> None: + factory.deploy( + deletable=False, + updatable=False, + on_schema_break=OnSchemaBreak.Fail, + on_update=OnUpdate.Fail, + deploy_time_params={ + "VALUE": 1, + }, + ) + + +def test_deploy_app_create(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_create_abi(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_update(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + updatable=True, + ) + + _, updated_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.UpdateApp, + ) + + assert updated_app.operation_performed == OperationPerformed.Update + assert created_app.app_id == updated_app.app_id + assert created_app.app_address == updated_app.app_address + assert created_app.confirmation + assert created_app.updatable + assert created_app.updatable == updated_app.updatable + assert created_app.updated_round != updated_app.updated_round + assert created_app.created_round == updated_app.created_round + assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] + + +def test_deploy_app_update_abi(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + updatable=True, + ) + + _, updated_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.UpdateApp, + update_params=AppClientMethodCallParams(method="update_abi", args=["args_io"]), + ) + + assert updated_app.operation_performed == OperationPerformed.Update + assert updated_app.app_id == created_app.app_id + assert updated_app.app_address == created_app.app_address + assert updated_app.confirmation is not None + assert updated_app.created_round == created_app.created_round + assert updated_app.updated_round != updated_app.created_round + assert updated_app.updated_round == updated_app.confirmation["confirmed-round"] # type: ignore[call-overload] + assert updated_app.transaction.application_call + assert updated_app.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC + assert updated_app.return_value == "args_io" + + +def test_deploy_app_replace(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + deletable=True, + ) + + _, replaced_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.ReplaceApp, + ) + + assert replaced_app.operation_performed == OperationPerformed.Replace + assert replaced_app.app_id > created_app.app_id + assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) + assert replaced_app.confirmation is not None + assert replaced_app.delete_result is not None + assert replaced_app.delete_result.confirmation is not None + assert len(replaced_app.transactions) == 2 + assert replaced_app.delete_result.transaction.application_call + assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id + assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + + +def test_deploy_app_replace_abi(factory: AppFactory) -> None: + _, created_app = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + deletable=True, + populate_app_call_resources=False, + ) + + _, replaced_app = factory.deploy( + deploy_time_params={ + "VALUE": 2, + }, + on_update=OnUpdate.ReplaceApp, + create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + delete_params=AppClientMethodCallParams(method="delete_abi", args=["arg2_io"]), + ) + + assert replaced_app.operation_performed == OperationPerformed.Replace + assert replaced_app.app_id > created_app.app_id + assert replaced_app.app_address == algosdk.logic.get_application_address(replaced_app.app_id) + assert replaced_app.confirmation is not None + assert replaced_app.delete_result is not None + assert replaced_app.delete_result.confirmation is not None + assert len(replaced_app.transactions) == 2 + assert replaced_app.delete_result.transaction.application_call + assert replaced_app.delete_result.transaction.application_call.index == created_app.app_id + assert replaced_app.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + assert replaced_app.return_value == "arg_io" + assert replaced_app.delete_return_value == "arg2_io" + + +def test_create_then_call_app(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 1, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + call = app_client.send.call(AppClientMethodCallWithSendParams(method="call_abi", args=["test"])) + + assert call.return_value + assert call.return_value.return_value == "Hello, test" + + +def test_call_app_with_rekey(funded_account: Account, algorand: AlgorandClient, factory: AppFactory) -> None: + rekey_to = algorand.account.random() + + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 1, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + app_client.send.opt_in(AppClientMethodCallWithSendParams(method="opt_in", rekey_to=rekey_to.address)) + + # If the rekey didn't work this will throw + rekeyed_account = algorand.account.rekeyed(funded_account.address, rekey_to) + algorand.send.payment( + PaymentParams(amount=AlgoAmount.from_algo(0), sender=rekeyed_account.address, receiver=funded_account.address) + ) + + +def test_create_app_with_abi(factory: AppFactory) -> None: + _, call_return = factory.send.create( + AppFactoryCreateMethodCallParams( + method="create_abi", + args=["string_io"], + deploy_time_params={ + "UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + ) + ) + + assert call_return.return_value + # Fix return value issues + assert call_return.return_value.return_value == "string_io" + + +def test_update_app_with_abi(factory: AppFactory) -> None: + deploy_time_params = { + "UPDATABLE": 1, + "DELETABLE": 0, + "VALUE": 1, + } + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params=deploy_time_params, + ) + ) + + call_return = app_client.send.update( + AppClientMethodCallWithCompilationAndSendParams( + method="update_abi", + args=["string_io"], + deploy_time_params=deploy_time_params, + ) + ) + + assert call_return.return_value is not None + assert call_return.return_value.return_value == "string_io" + # TODO: fix this + # assert call_return.compiled_approval is not None + + +def test_delete_app_with_abi(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + AppFactoryCreateWithSendParams( + deploy_time_params={ + "UPDATABLE": 0, + "DELETABLE": 1, + "VALUE": 1, + }, + ) + ) + + call_return = app_client.send.delete( + AppClientMethodCallWithSendParams( + method="delete_abi", + args=["string_io"], + ) + ) + + assert call_return.return_value is not None + assert call_return.return_value.return_value == "string_io" + + +def test_export_import_sourcemaps( + factory: AppFactory, + algorand: AlgorandClient, + funded_account: Account, +) -> None: + # Export source maps from original client + client, app = factory.deploy(deploy_time_params={"VALUE": 1}) + old_sourcemaps = client.export_source_maps() + + # Create new client instance + new_client = AppClient( + AppClientParams( + app_id=app.app_id, + default_sender=funded_account.address, + default_signer=funded_account.signer, + algorand=algorand, + app_spec=client.app_spec, + ) + ) + + # Test error handling before importing source maps + with pytest.raises(LogicError) as exc_info: + new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + + assert "assert failed" in exc_info.value.message + + # Import source maps into new client + new_client.import_source_maps(old_sourcemaps) + + # Test error handling after importing source maps + with pytest.raises(LogicError) as exc_info: + new_client.send.call(AppClientMethodCallWithSendParams(method="error")) + + error = exc_info.value + assert ( + error.trace().strip() + == "// error\n\terror_7:\n\tproto 0 0\n\tintc_0 // 0\n\t// Deliberate error\n\tassert\t\t<-- Error\n\tretsub\n\t\n\t// create\n\tcreate_8:" # noqa: E501 + ) + assert error.pc == 885 + assert error.message == "assert failed pc=885" + assert len(error.transaction_id) == 52 + + +def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset( + arc56_factory: AppFactory, +) -> None: + client, _ = arc56_factory.deploy( + create_params=AppClientMethodCallParams(method="createApplication"), + deploy_time_params={ + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 123, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + ) + + with pytest.raises(Exception, match="this is an error"): + client.send.call(AppClientMethodCallWithSendParams(method="throwError")) + + +def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( + arc56_factory: AppFactory, + algorand: AlgorandClient, + funded_account: Account, +) -> None: + # Deploy app with template parameters + client, result = arc56_factory.deploy( + create_params=AppClientMethodCallParams(method="createApplication"), + deploy_time_params={ + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 0, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + ) + app_id = result.app_id + + # Create new client without source map from compilation + app_client = AppClient( + AppClientParams( + app_id=app_id, + default_sender=funded_account.address, + default_signer=funded_account.signer, + algorand=algorand, + app_spec=client.app_spec, + ) + ) + + # Test error handling + with pytest.raises(LogicError) as exc_info: + app_client.send.call(AppClientMethodCallWithSendParams(method="tmpl")) + + assert ( + exc_info.value.trace().strip() + == "// tests/example-contracts/arc56_templates/templates.algo.ts:14\n\t\t// assert(this.uint64TmplVar)\n\t\tintc 1 // TMPL_uint64TmplVar\n\t\tassert\n\t\tretsub\t\t<-- Error\n\t\n\t// specificLengthTemplateVar()void\n\t*abi_route_specificLengthTemplateVar:\n\t\t// execute specificLengthTemplateVar()void" # noqa: E501 + ) diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index 8c9c1002..57313d31 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -1,16 +1,26 @@ import pytest + from algokit_utils.applications.app_manager import AppManager from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account - +from algokit_utils.models.amount import AlgoAmount from tests.conftest import check_output_stability -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account def test_template_substitution() -> None: diff --git a/tests/applications/test_utils.py b/tests/applications/test_utils.py new file mode 100644 index 00000000..4806216c --- /dev/null +++ b/tests/applications/test_utils.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from algokit_utils.applications.utils import arc32_to_arc56 +from tests.utils import load_arc32_spec + +TEST_ARC32_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + + +def test_arc32_to_arc56() -> None: + arc32_app_spec = load_arc32_spec( + TEST_ARC32_SPEC_FILE_PATH, deletable=True, updatable=True, template_values={"VERSION": 1} + ) + + arc56_app_spec = arc32_to_arc56(arc32_app_spec) + + assert arc56_app_spec diff --git a/tests/transactions/artifacts/hello_world/approval.teal b/tests/artifacts/hello_world/approval.teal similarity index 100% rename from tests/transactions/artifacts/hello_world/approval.teal rename to tests/artifacts/hello_world/approval.teal diff --git a/tests/artifacts/hello_world/arc32_app_spec.json b/tests/artifacts/hello_world/arc32_app_spec.json new file mode 100644 index 00000000..d84bc32c --- /dev/null +++ b/tests/artifacts/hello_world/arc32_app_spec.json @@ -0,0 +1,55 @@ +{ + "hints": { + "hello(string)string": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "HelloWorld", + "methods": [ + { + "name": "hello", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "readonly": false, + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} diff --git a/tests/transactions/artifacts/hello_world/clear.teal b/tests/artifacts/hello_world/clear.teal similarity index 100% rename from tests/transactions/artifacts/hello_world/clear.teal rename to tests/artifacts/hello_world/clear.teal diff --git a/tests/artifacts/legacy_hello_world/arc32_app_spec.json b/tests/artifacts/legacy_hello_world/arc32_app_spec.json new file mode 100644 index 00000000..1ddf81b2 --- /dev/null +++ b/tests/artifacts/legacy_hello_world/arc32_app_spec.json @@ -0,0 +1,378 @@ +{ + "hints": { + "version()uint64": { + "call_config": { + "no_op": "CALL" + } + }, + "readonly(uint64)void": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "set_box(byte[4],string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box(byte[4])string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_box_readonly(byte[4])string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "update()void": { + "call_config": { + "update_application": "CALL" + } + }, + "update_args(string)void": { + "call_config": { + "update_application": "CALL" + } + }, + "delete()void": { + "call_config": { + "delete_application": "CALL" + } + }, + "delete_args(string)void": { + "call_config": { + "delete_application": "CALL" + } + }, + "create_opt_in()void": { + "call_config": { + "opt_in": "CREATE" + } + }, + "update_greeting(string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "create()void": { + "call_config": { + "no_op": "CREATE" + } + }, + "create_args(string)void": { + "call_config": { + "no_op": "CREATE" + } + }, + "hello(string)string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "hello_remember(string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_last()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "opt_in_args(string)void": { + "call_config": { + "opt_in": "CALL" + } + }, + "close_out()void": { + "call_config": { + "close_out": "CALL" + } + }, + "close_out_args(string)void": { + "call_config": { + "close_out": "CALL" + } + }, + "call_with_payment(pay)string": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4=" + }, + "state": { + "global": { + "num_byte_slices": 1, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": { + "greeting": { + "type": "bytes", + "key": "greeting", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "last": { + "type": "bytes", + "key": "last", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "HelloWorldApp", + "methods": [ + { + "name": "version", + "args": [], + "returns": { + "type": "uint64" + } + }, + { + "name": "readonly", + "args": [ + { + "type": "uint64", + "name": "error" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "get_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_box_readonly", + "args": [ + { + "type": "byte[4]", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "update", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "delete", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "delete_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create_opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "update_greeting", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "create", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "create_args", + "args": [ + { + "type": "string", + "name": "greeting" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "hello", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "hello_remember", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_last", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "opt_in_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "close_out", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "close_out_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "call_with_payment", + "args": [ + { + "type": "pay", + "name": "payment" + } + ], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "close_out": "CALL", + "delete_application": "CALL", + "no_op": "CREATE", + "opt_in": "CALL", + "update_application": "CALL" + } +} diff --git a/tests/artifacts/testing_app/arc32_app_spec.json b/tests/artifacts/testing_app/arc32_app_spec.json new file mode 100644 index 00000000..c308fc12 --- /dev/null +++ b/tests/artifacts/testing_app/arc32_app_spec.json @@ -0,0 +1,400 @@ +{ + "hints": { + "call_abi(string)string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "call_abi_txn(pay,string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "call_abi_foreign_refs()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + }, + "set_global(uint64,uint64,string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_local(uint64,uint64,string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box(byte[4],string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "error()void": { + "call_config": { + "no_op": "CALL" + } + }, + "create_abi(string)string": { + "call_config": { + "no_op": "CREATE" + } + }, + "update_abi(string)string": { + "call_config": { + "update_application": "CALL" + } + }, + "delete_abi(string)string": { + "call_config": { + "delete_application": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "default_value(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "constant", + "data": "default value" + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_abi(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "abi-method", + "data": { + "name": "default_value", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + } + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_global_state(uint64)uint64": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "global-state", + "data": "int1" + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "default_value_from_local_state(string)string": { + "read_only": true, + "default_arguments": { + "arg_with_default": { + "source": "local-state", + "data": "local_bytes1" + } + }, + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMCA1IFRNUExfVVBEQVRBQkxFIFRNUExfREVMRVRBQkxFCmJ5dGVjYmxvY2sgMHggMHgxNTFmN2M3NQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhmMTdlODBhNSAvLyAiY2FsbF9hYmkoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMxCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MGE5MmE4MWUgLy8gImNhbGxfYWJpX3R4bihwYXksc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDMwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YWQ3NTYwMmMgLy8gImNhbGxfYWJpX2ZvcmVpZ25fcmVmcygpc3RyaW5nIgo9PQpibnogbWFpbl9sMjkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGNmOGRlYSAvLyAic2V0X2dsb2JhbCh1aW50NjQsdWludDY0LHN0cmluZyxieXRlWzRdKXZvaWQiCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGNlYzI4MzRhIC8vICJzZXRfbG9jYWwodWludDY0LHVpbnQ2NCxzdHJpbmcsYnl0ZVs0XSl2b2lkIgo9PQpibnogbWFpbl9sMjcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGI0YTIzMCAvLyAic2V0X2JveChieXRlWzRdLHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0NGQwZGEwZCAvLyAiZXJyb3IoKXZvaWQiCj09CmJueiBtYWluX2wyNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDlkNTIzMDQwIC8vICJjcmVhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyNAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDNjYTVjZWI3IC8vICJ1cGRhdGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI3MWI0ZWU5IC8vICJkZWxldGVfYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU3NGI1NWM4IC8vICJkZWZhdWx0X3ZhbHVlKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDQ2ZDIxMWEzIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDBjZmNiYjAwIC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlKHVpbnQ2NCl1aW50NjQiCj09CmJueiBtYWluX2wxOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQwZjBiYWY4IC8vICJkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDE3CmVycgptYWluX2wxNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlY2FzdGVyXzMyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkZWZhdWx0dmFsdWVjYXN0ZXJfMzAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBvcHRpbmNhc3Rlcl8yOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWFiaWNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjM6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFiaWNhc3Rlcl8yNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYWJpY2FzdGVyXzI2CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBlcnJvcmNhc3Rlcl8yNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Ym94Y2FzdGVyXzI0CmludGNfMSAvLyAxCnJldHVybgptYWluX2wyNzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBzZXRsb2NhbGNhc3Rlcl8yMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjg6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgc2V0Z2xvYmFsY2FzdGVyXzIyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYml0eG5jYXN0ZXJfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNhbGxhYmljYXN0ZXJfMTkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w0MAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sMzkKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDM4CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wzNwplcnIKbWFpbl9sMzc6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGRlbGV0ZV8xMgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzg6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CmFzc2VydApjYWxsc3ViIHVwZGF0ZV8xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzk6CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CmFzc2VydApjYWxsc3ViIGNyZWF0ZV84CmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzgKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjYWxsX2FiaQpjYWxsYWJpXzA6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyYzIwIC8vICJIZWxsbywgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaXRvYQppdG9hXzE6CnByb3RvIDEgMQpmcmFtZV9kaWcgLTEKaW50Y18wIC8vIDAKPT0KYm56IGl0b2FfMV9sNQpmcmFtZV9kaWcgLTEKaW50Y18yIC8vIDEwCi8KaW50Y18wIC8vIDAKPgpibnogaXRvYV8xX2w0CmJ5dGVjXzAgLy8gIiIKaXRvYV8xX2wzOgpwdXNoYnl0ZXMgMHgzMDMxMzIzMzM0MzUzNjM3MzgzOSAvLyAiMDEyMzQ1Njc4OSIKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmludGNfMSAvLyAxCmV4dHJhY3QzCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gY2FsbF9hYmlfdHhuCmNhbGxhYml0eG5fMjoKcHJvdG8gMiAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NTM2NTZlNzQyMCAvLyAiU2VudCAiCmZyYW1lX2RpZyAtMgpndHhucyBBbW91bnQKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJlMjAgLy8gIi4gIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGNhbGxfYWJpX2ZvcmVpZ25fcmVmcwpjYWxsYWJpZm9yZWlnbnJlZnNfMzoKcHJvdG8gMCAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE3MDcwM2EyMCAvLyAiQXBwOiAiCnR4bmEgQXBwbGljYXRpb25zIDEKY2FsbHN1YiBpdG9hXzEKY29uY2F0CnB1c2hieXRlcyAweDJjMjA0MTczNzM2NTc0M2EyMCAvLyAiLCBBc3NldDogIgpjb25jYXQKdHhuYSBBc3NldHMgMApjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4MmMyMDQxNjM2MzZmNzU2ZTc0M2EyMCAvLyAiLCBBY2NvdW50OiAiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18wIC8vIDAKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKcHVzaGJ5dGVzIDB4M2EgLy8gIjoiCmNvbmNhdAp0eG5hIEFjY291bnRzIDAKaW50Y18xIC8vIDEKZ2V0Ynl0ZQpjYWxsc3ViIGl0b2FfMQpjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBzZXRfZ2xvYmFsCnNldGdsb2JhbF80Ogpwcm90byA0IDAKcHVzaGJ5dGVzIDB4Njk2ZTc0MzEgLy8gImludDEiCmZyYW1lX2RpZyAtNAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg2OTZlNzQzMiAvLyAiaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDYyNzk3NDY1NzMzMSAvLyAiYnl0ZXMxIgpmcmFtZV9kaWcgLTIKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcHVzaGJ5dGVzIDB4NjI3OTc0NjU3MzMyIC8vICJieXRlczIiCmZyYW1lX2RpZyAtMQphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHNldF9sb2NhbApzZXRsb2NhbF81Ogpwcm90byA0IDAKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMSAvLyAibG9jYWxfaW50MSIKZnJhbWVfZGlnIC00CmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2OTZlNzQzMiAvLyAibG9jYWxfaW50MiIKZnJhbWVfZGlnIC0zCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzEgLy8gImxvY2FsX2J5dGVzMSIKZnJhbWVfZGlnIC0yCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKdHhuIFNlbmRlcgpwdXNoYnl0ZXMgMHg2YzZmNjM2MTZjNWY2Mjc5NzQ2NTczMzIgLy8gImxvY2FsX2J5dGVzMiIKZnJhbWVfZGlnIC0xCmFwcF9sb2NhbF9wdXQKcmV0c3ViCgovLyBzZXRfYm94CnNldGJveF82Ogpwcm90byAyIDAKZnJhbWVfZGlnIC0yCmJveF9kZWwKcG9wCmZyYW1lX2RpZyAtMgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYm94X3B1dApyZXRzdWIKCi8vIGVycm9yCmVycm9yXzc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAovLyBEZWxpYmVyYXRlIGVycm9yCmFzc2VydApyZXRzdWIKCi8vIGNyZWF0ZQpjcmVhdGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKcHVzaGJ5dGVzIDB4NzY2MTZjNzU2NSAvLyAidmFsdWUiCnB1c2hpbnQgVE1QTF9WQUxVRSAvLyBUTVBMX1ZBTFVFCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2FiaQpjcmVhdGVhYmlfOToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB1cGRhdGUKdXBkYXRlXzEwOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKcmV0c3ViCgovLyB1cGRhdGVfYWJpCnVwZGF0ZWFiaV8xMToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDQgLy8gVE1QTF9VUERBVEFCTEUKLy8gQ2hlY2sgYXBwIGlzIHVwZGF0YWJsZQphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8xMjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FiaQpkZWxldGVhYmlfMTM6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xNDoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gZGVmYXVsdF92YWx1ZQpkZWZhdWx0dmFsdWVfMTU6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9hYmkKZGVmYXVsdHZhbHVlZnJvbWFiaV8xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKcHVzaGJ5dGVzIDB4NDE0MjQ5MmMyMCAvLyAiQUJJLCAiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fZ2xvYmFsX3N0YXRlCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZV8xNzoKcHJvdG8gMSAxCmludGNfMCAvLyAwCmZyYW1lX2RpZyAtMQpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fbG9jYWxfc3RhdGUKZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTg6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnB1c2hieXRlcyAweDRjNmY2MzYxNmMyMDczNzQ2MTc0NjUyYzIwIC8vICJMb2NhbCBzdGF0ZSwgIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gY2FsbF9hYmlfY2FzdGVyCmNhbGxhYmljYXN0ZXJfMTk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGxhYmlfMApmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBjYWxsX2FiaV90eG5fY2FzdGVyCmNhbGxhYml0eG5jYXN0ZXJfMjA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmludGNfMCAvLyAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDIKdHhuIEdyb3VwSW5kZXgKaW50Y18xIC8vIDEKLQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKY2FsbHN1YiBjYWxsYWJpdHhuXzIKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gY2FsbF9hYmlfZm9yZWlnbl9yZWZzX2Nhc3RlcgpjYWxsYWJpZm9yZWlnbnJlZnNjYXN0ZXJfMjE6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmNhbGxzdWIgY2FsbGFiaWZvcmVpZ25yZWZzXzMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gc2V0X2dsb2JhbF9jYXN0ZXIKc2V0Z2xvYmFsY2FzdGVyXzIyOgpwcm90byAwIDAKaW50Y18wIC8vIDAKZHVwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKYnRvaQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpidG9pCmZyYW1lX2J1cnkgMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmZyYW1lX2J1cnkgMgp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMApmcmFtZV9kaWcgMQpmcmFtZV9kaWcgMgpmcmFtZV9kaWcgMwpjYWxsc3ViIHNldGdsb2JhbF80CnJldHN1YgoKLy8gc2V0X2xvY2FsX2Nhc3RlcgpzZXRsb2NhbGNhc3Rlcl8yMzoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpmcmFtZV9idXJ5IDEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpmcmFtZV9idXJ5IDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgNApmcmFtZV9idXJ5IDMKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKZnJhbWVfZGlnIDIKZnJhbWVfZGlnIDMKY2FsbHN1YiBzZXRsb2NhbF81CnJldHN1YgoKLy8gc2V0X2JveF9jYXN0ZXIKc2V0Ym94Y2FzdGVyXzI0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDAKZnJhbWVfZGlnIDEKY2FsbHN1YiBzZXRib3hfNgpyZXRzdWIKCi8vIGVycm9yX2Nhc3RlcgplcnJvcmNhc3Rlcl8yNToKcHJvdG8gMCAwCmNhbGxzdWIgZXJyb3JfNwpyZXRzdWIKCi8vIGNyZWF0ZV9hYmlfY2FzdGVyCmNyZWF0ZWFiaWNhc3Rlcl8yNjoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgY3JlYXRlYWJpXzkKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gdXBkYXRlX2FiaV9jYXN0ZXIKdXBkYXRlYWJpY2FzdGVyXzI3Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiB1cGRhdGVhYmlfMTEKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVsZXRlX2FiaV9jYXN0ZXIKZGVsZXRlYWJpY2FzdGVyXzI4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWxldGVhYmlfMTMKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl8yOToKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTQKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Nhc3RlcgpkZWZhdWx0dmFsdWVjYXN0ZXJfMzA6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGRlZmF1bHR2YWx1ZV8xNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3ViCgovLyBkZWZhdWx0X3ZhbHVlX2Zyb21fYWJpX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tYWJpY2FzdGVyXzMxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBkZWZhdWx0dmFsdWVmcm9tYWJpXzE2CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGRlZmF1bHRfdmFsdWVfZnJvbV9nbG9iYWxfc3RhdGVfY2FzdGVyCmRlZmF1bHR2YWx1ZWZyb21nbG9iYWxzdGF0ZWNhc3Rlcl8zMjoKcHJvdG8gMCAwCmludGNfMCAvLyAwCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWdsb2JhbHN0YXRlXzE3CmZyYW1lX2J1cnkgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKaXRvYgpjb25jYXQKbG9nCnJldHN1YgoKLy8gZGVmYXVsdF92YWx1ZV9mcm9tX2xvY2FsX3N0YXRlX2Nhc3RlcgpkZWZhdWx0dmFsdWVmcm9tbG9jYWxzdGF0ZWNhc3Rlcl8zMzoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCmNhbGxzdWIgZGVmYXVsdHZhbHVlZnJvbWxvY2Fsc3RhdGVfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzEgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1Yg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" + }, + "state": { + "global": { + "num_byte_slices": 2, + "num_uints": 3 + }, + "local": { + "num_byte_slices": 2, + "num_uints": 2 + } + }, + "schema": { + "global": { + "declared": { + "bytes1": { + "type": "bytes", + "key": "bytes1", + "descr": "" + }, + "bytes2": { + "type": "bytes", + "key": "bytes2", + "descr": "" + }, + "int1": { + "type": "uint64", + "key": "int1", + "descr": "" + }, + "int2": { + "type": "uint64", + "key": "int2", + "descr": "" + }, + "value": { + "type": "uint64", + "key": "value", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "local_bytes1": { + "type": "bytes", + "key": "local_bytes1", + "descr": "" + }, + "local_bytes2": { + "type": "bytes", + "key": "local_bytes2", + "descr": "" + }, + "local_int1": { + "type": "uint64", + "key": "local_int1", + "descr": "" + }, + "local_int2": { + "type": "uint64", + "key": "local_int2", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "TestingApp", + "methods": [ + { + "name": "call_abi", + "args": [ + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "call_abi_txn", + "args": [ + { + "type": "pay", + "name": "txn" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "call_abi_foreign_refs", + "args": [], + "returns": { + "type": "string" + } + }, + { + "name": "set_global", + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_local", + "args": [ + { + "type": "uint64", + "name": "int1" + }, + { + "type": "uint64", + "name": "int2" + }, + { + "type": "string", + "name": "bytes1" + }, + { + "type": "byte[4]", + "name": "bytes2" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "set_box", + "args": [ + { + "type": "byte[4]", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "error", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "create_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "update_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "delete_abi", + "args": [ + { + "type": "string", + "name": "input" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "default_value", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "default_value_from_abi", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "default_value_from_global_state", + "args": [ + { + "type": "uint64", + "name": "arg_with_default" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "default_value_from_local_state", + "args": [ + { + "type": "string", + "name": "arg_with_default" + } + ], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "delete_application": "CALL", + "no_op": "CREATE", + "opt_in": "CREATE", + "update_application": "CALL" + } +} diff --git a/tests/artifacts/testing_app/contract.py b/tests/artifacts/testing_app/contract.py new file mode 100644 index 00000000..95159cbc --- /dev/null +++ b/tests/artifacts/testing_app/contract.py @@ -0,0 +1,185 @@ +from typing import Literal + +import beaker +import pyteal as pt +from beaker.lib.storage import BoxMapping +from pyteal.ast import CallConfig, MethodConfig + +UPDATABLE_TEMPLATE_NAME = "TMPL_UPDATABLE" +DELETABLE_TEMPLATE_NAME = "TMPL_DELETABLE" + + +class BareCallAppState: + value = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + bytes1 = beaker.GlobalStateValue(stack_type=pt.TealType.bytes) + bytes2 = beaker.GlobalStateValue(stack_type=pt.TealType.bytes) + int1 = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + int2 = beaker.GlobalStateValue(stack_type=pt.TealType.uint64) + local_bytes1 = beaker.LocalStateValue(stack_type=pt.TealType.bytes) + local_bytes2 = beaker.LocalStateValue(stack_type=pt.TealType.bytes) + local_int1 = beaker.LocalStateValue(stack_type=pt.TealType.uint64) + local_int2 = beaker.LocalStateValue(stack_type=pt.TealType.uint64) + box = BoxMapping(pt.abi.StaticBytes[Literal[4]], pt.abi.String) + + +app = beaker.Application("TestingApp", state=BareCallAppState) + + +@app.external(read_only=True) +def call_abi(value: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("Hello, "), value.get())) + + +# https://github.com/algorand/pyteal-utils/blob/main/pytealutils/strings/string.py#L63 +@pt.Subroutine(pt.TealType.bytes) +def itoa(i: pt.Expr) -> pt.Expr: + """itoa converts an integer to the ascii byte string it represents""" + return pt.If( + i == pt.Int(0), + pt.Bytes("0"), + pt.Concat( + pt.If(i / pt.Int(10) > pt.Int(0), itoa(i / pt.Int(10)), pt.Bytes("")), + pt.Extract(pt.Bytes("0123456789"), i % pt.Int(10), pt.Int(1)), + ), + ) + + +@app.external() +def call_abi_txn(txn: pt.abi.PaymentTransaction, value: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set( + pt.Concat( + pt.Bytes("Sent "), + itoa(txn.get().amount()), + pt.Bytes(". "), + value.get(), + ) + ) + + +@app.external(read_only=True) +def call_abi_foreign_refs(*, output: pt.abi.String) -> pt.Expr: + return output.set( + pt.Concat( + pt.Bytes("App: "), + itoa(pt.Txn.applications[1]), + pt.Bytes(", Asset: "), + itoa(pt.Txn.assets[0]), + pt.Bytes(", Account: "), + itoa(pt.GetByte(pt.Txn.accounts[0], pt.Int(0))), + pt.Bytes(":"), + itoa(pt.GetByte(pt.Txn.accounts[0], pt.Int(1))), + ) + ) + + +@app.external() +def set_global( + int1: pt.abi.Uint64, int2: pt.abi.Uint64, bytes1: pt.abi.String, bytes2: pt.abi.StaticBytes[Literal[4]] +) -> pt.Expr: + return pt.Seq( + app.state.int1.set(int1.get()), + app.state.int2.set(int2.get()), + app.state.bytes1.set(bytes1.get()), + app.state.bytes2.set(bytes2.get()), + ) + + +@app.external() +def set_local( + int1: pt.abi.Uint64, int2: pt.abi.Uint64, bytes1: pt.abi.String, bytes2: pt.abi.StaticBytes[Literal[4]] +) -> pt.Expr: + return pt.Seq( + app.state.local_int1.set(int1.get()), + app.state.local_int2.set(int2.get()), + app.state.local_bytes1.set(bytes1.get()), + app.state.local_bytes2.set(bytes2.get()), + ) + + +@app.external() +def set_box(name: pt.abi.StaticBytes[Literal[4]], value: pt.abi.String) -> pt.Expr: + return app.state.box[name.get()].set(value.get()) + + +@app.external() +def error() -> pt.Expr: + return pt.Assert(pt.Int(0), comment="Deliberate error") + + +@app.external( + authorize=beaker.Authorize.only_creator(), + bare=True, + method_config=MethodConfig(no_op=CallConfig.CREATE, opt_in=CallConfig.CREATE), +) +def create() -> pt.Expr: + return app.state.value.set(pt.Tmpl.Int("TMPL_VALUE")) + + +@app.create(authorize=beaker.Authorize.only_creator()) +def create_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return output.set(input.get()) + + +@app.update(authorize=beaker.Authorize.only_creator(), bare=True) +def update() -> pt.Expr: + return pt.Assert(pt.Tmpl.Int(UPDATABLE_TEMPLATE_NAME), comment="Check app is updatable") + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return pt.Seq( + pt.Assert(pt.Tmpl.Int(UPDATABLE_TEMPLATE_NAME), comment="Check app is updatable"), output.set(input.get()) + ) + + +@app.delete(authorize=beaker.Authorize.only_creator(), bare=True) +def delete() -> pt.Expr: + return pt.Assert(pt.Tmpl.Int(DELETABLE_TEMPLATE_NAME), comment="Check app is deletable") + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete_abi(input: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + return pt.Seq( + pt.Assert(pt.Tmpl.Int(DELETABLE_TEMPLATE_NAME), comment="Check app is deletable"), output.set(input.get()) + ) + + +@app.opt_in +def opt_in() -> pt.Expr: + return pt.Approve() + + +@app.external(read_only=True) +def default_value( + arg_with_default: pt.abi.String = "default value", + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(arg_with_default.get()) + + +@app.external(read_only=True) +def default_value_from_abi( + arg_with_default: pt.abi.String = default_value, + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("ABI, "), arg_with_default.get())) + + +@app.external(read_only=True) +def default_value_from_global_state( + arg_with_default: pt.abi.Uint64 = BareCallAppState.int1, + *, + output: pt.abi.Uint64, # type: ignore[assignment] +) -> pt.Expr: + return output.set(arg_with_default.get()) + + +@app.external(read_only=True) +def default_value_from_local_state( + arg_with_default: pt.abi.String = BareCallAppState.local_bytes1, + *, + output: pt.abi.String, # type: ignore[assignment] +) -> pt.Expr: + return output.set(pt.Concat(pt.Bytes("Local state, "), arg_with_default.get())) diff --git a/tests/artifacts/testing_app/sources.teal.map.json b/tests/artifacts/testing_app/sources.teal.map.json new file mode 100644 index 00000000..9ee43398 --- /dev/null +++ b/tests/artifacts/testing_app/sources.teal.map.json @@ -0,0 +1,22 @@ +{ + "approvalSourceMap": { + "version": 3, + "sources": [ + "" + ], + "names": [], + "mappings": ";AACA;;;;;;;;AACA;;;;;;;;AACA;;AACA;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;;;AACA;;;;;;AACA;AACA;;;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;AACA;;;AACA;;AACA;AACA;AACA;;;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;;;AACA;AACA;AAIA;;;AACA;AACA;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;AACA;AACA;AACA;;;AACA;AAEA;;;;;;;;;;;;AACA;;AACA;AACA;AACA;AACA;AACA;AACA;;;AAEA;;AACA;AACA;AACA;;;AACA;;;AAEA;;;AAEA;AAIA;;;AACA;AACA;;;;;;;AACA;;AACA;;AACA;;;AACA;AACA;;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;AACA;;;AACA;;;AACA;AACA;;;;;;;;;;;AACA;AACA;;;AACA;;;AACA;AACA;;;;;;;;;;;;;AACA;AACA;;;AACA;AACA;AACA;;;AACA;AACA;;;AACA;AACA;;;AACA;AACA;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;;;;;AACA;;AACA;AACA;;;;;;AACA;;AACA;AACA;;;;;;;;AACA;;AACA;;;AACA;AACA;;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;;AACA;;;;;;;;;;;;AACA;;AACA;AACA;;AACA;;;;;;;;;;;;AACA;;AACA;AACA;;AACA;;;;;;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;;;;;;;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;;AACA;AACA;AACA;;AACA;;AACA;;;AACA;AACA;AAIA;;;AACA;AAEA;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;;;;;;AACA;;AACA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAEA;AACA;;AAEA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;AAIA;;;AACA;AACA;;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;;AACA;;AACA;AAIA;;;AACA;AACA;;;;;;;;;;;;;;;AACA;;AACA;;;AACA;AACA;;AACA;;AACA;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;AACA;;;AACA;;AACA;;AACA;AACA;AACA;;AACA;;AACA;;AACA;AACA;AACA;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;AACA;AACA;;;AACA;AACA;;AACA;;;AACA;AACA;;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;;AACA;;AACA;;AACA;;AACA;;;AACA;AAIA;;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;;;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA;AACA;AAIA;;;AACA;AACA;AACA;;;AACA;;AACA;;AACA;;;AACA;;AACA;AACA;;AACA;AACA;AACA", + "pcToLocation": {}, + "sourceAndLineToPc": {} + }, + "clearSourceMap": { + "version": 3, + "sources": [ + "" + ], + "names": [], + "mappings": ";AACA;;AACA", + "pcToLocation": {}, + "sourceAndLineToPc": {} + } +} diff --git a/tests/artifacts/testing_app_arc56/arc56_app_spec.json b/tests/artifacts/testing_app_arc56/arc56_app_spec.json new file mode 100644 index 00000000..da275d16 --- /dev/null +++ b/tests/artifacts/testing_app_arc56/arc56_app_spec.json @@ -0,0 +1,681 @@ +{ + "name": "Templates", + "desc": "", + "methods": [ + { + "name": "tmpl", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "specificLengthTemplateVar", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "throwError", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "itobTemplateVar", + "args": [], + "returns": { + "type": "byte[]" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [ + "NoOp" + ], + "call": [] + } + } + ], + "arcs": [ + 4, + 56 + ], + "structs": {}, + "state": { + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "teal": 15, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 1, + 2 + ] + }, + { + "teal": 16, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 3 + ] + }, + { + "teal": 17, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 4, + 5 + ] + }, + { + "teal": 18, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 6 + ] + }, + { + "teal": 19, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 7, + 8 + ] + }, + { + "teal": 20, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 9 + ] + }, + { + "teal": 21, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35 + ] + }, + { + "teal": 25, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "The requested action is not implemented in this contract. Are you using the correct OnComplete? Did you set your app ID?", + "pc": [ + 36 + ] + }, + { + "teal": 30, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 37, + 38, + 39 + ] + }, + { + "teal": 31, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 40 + ] + }, + { + "teal": 32, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 41 + ] + }, + { + "teal": 36, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 42, + 43, + 44 + ] + }, + { + "teal": 40, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:13", + "pc": [ + 45 + ] + }, + { + "teal": 41, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:13", + "pc": [ + 46 + ] + }, + { + "teal": 45, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:14", + "pc": [ + 47 + ] + }, + { + "teal": 46, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:14", + "pc": [ + 48 + ] + }, + { + "teal": 47, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:12", + "pc": [ + 49 + ] + }, + { + "teal": 52, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 50, + 51, + 52 + ] + }, + { + "teal": 53, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 53 + ] + }, + { + "teal": 54, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 54 + ] + }, + { + "teal": 58, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 55, + 56, + 57 + ] + }, + { + "teal": 62, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 58 + ] + }, + { + "teal": 63, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 59 + ] + }, + { + "teal": 64, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 60 + ] + }, + { + "teal": 65, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:18", + "pc": [ + 61 + ] + }, + { + "teal": 66, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:17", + "pc": [ + 62 + ] + }, + { + "teal": 71, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 63, + 64, + 65 + ] + }, + { + "teal": 72, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 66 + ] + }, + { + "teal": 73, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 67 + ] + }, + { + "teal": 77, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 68, + 69, + 70 + ] + }, + { + "teal": 80, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:22", + "errorMessage": "this is an error", + "pc": [ + 71 + ] + }, + { + "teal": 81, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:21", + "pc": [ + 72 + ] + }, + { + "teal": 86, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 73, + 74, + 75, + 76, + 77, + 78 + ] + }, + { + "teal": 89, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 79, + 80, + 81 + ] + }, + { + "teal": 90, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 82 + ] + }, + { + "teal": 91, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 83 + ] + }, + { + "teal": 92, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 84 + ] + }, + { + "teal": 93, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 85, + 86, + 87 + ] + }, + { + "teal": 94, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 88 + ] + }, + { + "teal": 95, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 89 + ] + }, + { + "teal": 96, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 90 + ] + }, + { + "teal": 97, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 91 + ] + }, + { + "teal": 98, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 92 + ] + }, + { + "teal": 99, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 93 + ] + }, + { + "teal": 103, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 94, + 95, + 96 + ] + }, + { + "teal": 107, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:26", + "pc": [ + 97 + ] + }, + { + "teal": 108, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:26", + "pc": [ + 98 + ] + }, + { + "teal": 109, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:25", + "pc": [ + 99 + ] + }, + { + "teal": 112, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 100 + ] + }, + { + "teal": 113, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 101 + ] + }, + { + "teal": 116, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 102, + 103, + 104, + 105, + 106, + 107 + ] + }, + { + "teal": 117, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 108, + 109, + 110 + ] + }, + { + "teal": 118, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 111, + 112, + 113, + 114 + ] + }, + { + "teal": 121, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for create NoOp", + "pc": [ + 115 + ] + }, + { + "teal": 124, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 116, + 117, + 118, + 119, + 120, + 121 + ] + }, + { + "teal": 125, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 122, + 123, + 124, + 125, + 126, + 127 + ] + }, + { + "teal": 126, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 128, + 129, + 130, + 131, + 132, + 133 + ] + }, + { + "teal": 127, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 134, + 135, + 136, + 137, + 138, + 139 + ] + }, + { + "teal": 128, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 140, + 141, + 142 + ] + }, + { + "teal": 129, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "pc": [ + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152 + ] + }, + { + "teal": 132, + "source": "tests/example-contracts/arc56_templates/templates.algo.ts:3", + "errorMessage": "this contract does not implement the given ABI method for call NoOp", + "pc": [ + 153 + ] + } + ], + "pcOffsetMethod": "cblocks" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCmludGNibG9jayAxIFRNUExfdWludDY0VG1wbFZhcgpieXRlY2Jsb2NrIFRNUExfYnl0ZXNUbXBsVmFyIFRNUExfYnl0ZXM2NFRtcGxWYXIgVE1QTF9ieXRlczMyVG1wbFZhcgoKLy8gVGhpcyBURUFMIHdhcyBnZW5lcmF0ZWQgYnkgVEVBTFNjcmlwdCB2MC4xMDUuMwovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKcHVzaGludCA2CioKdHhuIE9uQ29tcGxldGlvbgorCnN3aXRjaCAqY2FsbF9Ob09wICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqY3JlYXRlX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVECgoqTk9UX0lNUExFTUVOVEVEOgoJLy8gVGhlIHJlcXVlc3RlZCBhY3Rpb24gaXMgbm90IGltcGxlbWVudGVkIGluIHRoaXMgY29udHJhY3QuIEFyZSB5b3UgdXNpbmcgdGhlIGNvcnJlY3QgT25Db21wbGV0ZT8gRGlkIHlvdSBzZXQgeW91ciBhcHAgSUQ/CgllcnIKCi8vIHRtcGwoKXZvaWQKKmFiaV9yb3V0ZV90bXBsOgoJLy8gZXhlY3V0ZSB0bXBsKCl2b2lkCgljYWxsc3ViIHRtcGwKCWludGMgMCAvLyAxCglyZXR1cm4KCi8vIHRtcGwoKTogdm9pZAp0bXBsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvYXJjNTZfdGVtcGxhdGVzL3RlbXBsYXRlcy5hbGdvLnRzOjEzCgkvLyBsb2codGhpcy5ieXRlc1RtcGxWYXIpCglieXRlYyAwIC8vIFRNUExfYnl0ZXNUbXBsVmFyCglsb2cKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9hcmM1Nl90ZW1wbGF0ZXMvdGVtcGxhdGVzLmFsZ28udHM6MTQKCS8vIGFzc2VydCh0aGlzLnVpbnQ2NFRtcGxWYXIpCglpbnRjIDEgLy8gVE1QTF91aW50NjRUbXBsVmFyCglhc3NlcnQKCXJldHN1YgoKLy8gc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcigpdm9pZAoqYWJpX3JvdXRlX3NwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXI6CgkvLyBleGVjdXRlIHNwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXIoKXZvaWQKCWNhbGxzdWIgc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcgoJaW50YyAwIC8vIDEKCXJldHVybgoKLy8gc3BlY2lmaWNMZW5ndGhUZW1wbGF0ZVZhcigpOiB2b2lkCnNwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXI6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9hcmM1Nl90ZW1wbGF0ZXMvdGVtcGxhdGVzLmFsZ28udHM6MTgKCS8vIGVkMjU1MTlWZXJpZnlCYXJlKHRoaXMuYnl0ZXNUbXBsVmFyLCB0aGlzLmJ5dGVzNjRUbXBsVmFyLCB0aGlzLmJ5dGVzMzJUbXBsVmFyKQoJYnl0ZWMgMCAvLyBUTVBMX2J5dGVzVG1wbFZhcgoJYnl0ZWMgMSAvLyBUTVBMX2J5dGVzNjRUbXBsVmFyCglieXRlYyAyIC8vIFRNUExfYnl0ZXMzMlRtcGxWYXIKCWVkMjU1MTl2ZXJpZnlfYmFyZQoJcmV0c3ViCgovLyB0aHJvd0Vycm9yKCl2b2lkCiphYmlfcm91dGVfdGhyb3dFcnJvcjoKCS8vIGV4ZWN1dGUgdGhyb3dFcnJvcigpdm9pZAoJY2FsbHN1YiB0aHJvd0Vycm9yCglpbnRjIDAgLy8gMQoJcmV0dXJuCgovLyB0aHJvd0Vycm9yKCk6IHZvaWQKdGhyb3dFcnJvcjoKCXByb3RvIDAgMAoKCS8vIHRoaXMgaXMgYW4gZXJyb3IKCWVycgoJcmV0c3ViCgovLyBpdG9iVGVtcGxhdGVWYXIoKWJ5dGVbXQoqYWJpX3JvdXRlX2l0b2JUZW1wbGF0ZVZhcjoKCS8vIFRoZSBBQkkgcmV0dXJuIHByZWZpeAoJcHVzaGJ5dGVzIDB4MTUxZjdjNzUKCgkvLyBleGVjdXRlIGl0b2JUZW1wbGF0ZVZhcigpYnl0ZVtdCgljYWxsc3ViIGl0b2JUZW1wbGF0ZVZhcgoJZHVwCglsZW4KCWl0b2IKCWV4dHJhY3QgNiAyCglzd2FwCgljb25jYXQKCWNvbmNhdAoJbG9nCglpbnRjIDAgLy8gMQoJcmV0dXJuCgovLyBpdG9iVGVtcGxhdGVWYXIoKTogYnl0ZXMKaXRvYlRlbXBsYXRlVmFyOgoJcHJvdG8gMCAxCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvYXJjNTZfdGVtcGxhdGVzL3RlbXBsYXRlcy5hbGdvLnRzOjI2CgkvLyByZXR1cm4gaXRvYih0aGlzLnVpbnQ2NFRtcGxWYXIpCglpbnRjIDEgLy8gVE1QTF91aW50NjRUbXBsVmFyCglpdG9iCglyZXRzdWIKCiphYmlfcm91dGVfY3JlYXRlQXBwbGljYXRpb246CglpbnRjIDAgLy8gMQoJcmV0dXJuCgoqY3JlYXRlX05vT3A6CglwdXNoYnl0ZXMgMHhiODQ0N2IzNiAvLyBtZXRob2QgImNyZWF0ZUFwcGxpY2F0aW9uKCl2b2lkIgoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAoJbWF0Y2ggKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbgoKCS8vIHRoaXMgY29udHJhY3QgZG9lcyBub3QgaW1wbGVtZW50IHRoZSBnaXZlbiBBQkkgbWV0aG9kIGZvciBjcmVhdGUgTm9PcAoJZXJyCgoqY2FsbF9Ob09wOgoJcHVzaGJ5dGVzIDB4OWE3MWQyYjQgLy8gbWV0aG9kICJ0bXBsKCl2b2lkIgoJcHVzaGJ5dGVzIDB4ZGY0ZDVjM2IgLy8gbWV0aG9kICJzcGVjaWZpY0xlbmd0aFRlbXBsYXRlVmFyKCl2b2lkIgoJcHVzaGJ5dGVzIDB4M2Q4NzBkODcgLy8gbWV0aG9kICJ0aHJvd0Vycm9yKCl2b2lkIgoJcHVzaGJ5dGVzIDB4YmMwYjE3MDYgLy8gbWV0aG9kICJpdG9iVGVtcGxhdGVWYXIoKWJ5dGVbXSIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfdG1wbCAqYWJpX3JvdXRlX3NwZWNpZmljTGVuZ3RoVGVtcGxhdGVWYXIgKmFiaV9yb3V0ZV90aHJvd0Vycm9yICphYmlfcm91dGVfaXRvYlRlbXBsYXRlVmFyCgoJLy8gdGhpcyBjb250cmFjdCBkb2VzIG5vdCBpbXBsZW1lbnQgdGhlIGdpdmVuIEFCSSBtZXRob2QgZm9yIGNhbGwgTm9PcAoJZXJy", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + }, + "templateVariables": { + "bytesTmplVar": { + "type": "byte[]" + }, + "uint64TmplVar": { + "type": "uint64" + }, + "bytes32TmplVar": { + "type": "byte[32]" + }, + "bytes64TmplVar": { + "type": "byte[64]" + } + }, + "scratchVariables": { + "bytesTmplVar": { + "type": "byte[]", + "slot": 200 + }, + "uint64TmplVar": { + "type": "uint64", + "slot": 201 + }, + "bytes32TmplVar": { + "type": "byte[32]", + "slot": 202 + }, + "bytes64TmplVar": { + "type": "byte[64]", + "slot": 203 + } + }, + "compilerInfo": { + "compiler": "algod", + "compilerVersion": { + "major": 3, + "minor": 26, + "patch": 0, + "commitHash": "0d10b244" + } + } +} \ No newline at end of file diff --git a/tests/artifacts/testing_app_puya/arc32_app_spec.json b/tests/artifacts/testing_app_puya/arc32_app_spec.json new file mode 100644 index 00000000..d8518906 --- /dev/null +++ b/tests/artifacts/testing_app_puya/arc32_app_spec.json @@ -0,0 +1,184 @@ +{ + "hints": { + "set_box_bytes(string,byte[])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_str(string,string)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_int(string,uint32)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_int512(string,uint512)void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_box_static(string,byte[4])void": { + "call_config": { + "no_op": "CALL" + } + }, + "set_struct(string,(string,uint64))void": { + "call_config": { + "no_op": "CALL" + }, + "structs": { + "value": { + "name": "DummyStruct", + "elements": [ + [ + "name", + "string" + ], + [ + "id", + "uint64" + ] + ] + } + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuYXBwcm92YWxfcHJvZ3JhbToKICAgIGludGNibG9jayAxIDAKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5fX3B1eWFfYXJjNF9yb3V0ZXJfXygpIC0+IHVpbnQ2NDoKX19wdXlhX2FyYzRfcm91dGVyX186CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICBwcm90byAwIDEKICAgIHR4biBOdW1BcHBBcmdzCiAgICBieiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgyMDJmYTczYSAweGRmN2VlYTRhIDB4MzY4OGVkMmMgMHg1ZDE3MjBkZCAweGY4MDY2NjVjIDB4ODFkMjYwZTIgLy8gbWV0aG9kICJzZXRfYm94X2J5dGVzKHN0cmluZyxieXRlW10pdm9pZCIsIG1ldGhvZCAic2V0X2JveF9zdHIoc3RyaW5nLHN0cmluZyl2b2lkIiwgbWV0aG9kICJzZXRfYm94X2ludChzdHJpbmcsdWludDMyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfaW50NTEyKHN0cmluZyx1aW50NTEyKXZvaWQiLCBtZXRob2QgInNldF9ib3hfc3RhdGljKHN0cmluZyxieXRlWzRdKXZvaWQiLCBtZXRob2QgInNldF9zdHJ1Y3Qoc3RyaW5nLChzdHJpbmcsdWludDY0KSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9ieXRlc19yb3V0ZUAyIF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RyX3JvdXRlQDMgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2ludDUxMl9yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfc3RhdGljX3JvdXRlQDYgX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3CiAgICBpbnRjXzEgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X2J5dGVzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgZXh0cmFjdCAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjAKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X2J5dGVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0cl9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0cgogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X2JveF9pbnRfcm91dGVANDoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjExCiAgICAvLyBjbGFzcyBUZXN0UHV5YUJveGVzKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2V0X2JveF9pbnQKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NldF9ib3hfaW50NTEyX3JvdXRlQDU6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjMyCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNldF9ib3hfaW50NTEyCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZXRfYm94X3N0YXRpY19yb3V0ZUA2OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MTEKICAgIC8vIGNsYXNzIFRlc3RQdXlhQm94ZXMoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzYKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZXRfYm94X3N0YXRpYwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2V0X3N0cnVjdF9yb3V0ZUA3OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgc2V0X3N0cnVjdAogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTQKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDE0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToxMQogICAgLy8gY2xhc3MgVGVzdFB1eWFCb3hlcyhBUkM0Q29udHJhY3QpOgogICAgaW50Y18xIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2J5dGVzKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfYnl0ZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjIwLTIxCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZXRfYm94X2J5dGVzKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogQnl0ZXMpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjIKICAgIC8vIHNlbGYuYm94X2J5dGVzW25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2J5dGVzIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGR1cAogICAgYm94X2RlbAogICAgcG9wCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X3N0cihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X3N0cjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjQtMjUKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfc3RyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5TdHJpbmcpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MjYKICAgIC8vIHNlbGYuYm94X3N0cltuYW1lXSA9IHZhbHVlCiAgICBwdXNoYnl0ZXMgImJveF9zdHIiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZHVwCiAgICBib3hfZGVsCiAgICBwb3AKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfaW50KG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfaW50OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weToyOC0yOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9pbnQoc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlVJbnQzMikgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozMAogICAgLy8gc2VsZi5ib3hfaW50W25hbWVdID0gdmFsdWUKICAgIHB1c2hieXRlcyAiYm94X2ludCIKICAgIGZyYW1lX2RpZyAtMgogICAgY29uY2F0CiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZDMuY29udHJhY3QuVGVzdFB1eWFCb3hlcy5zZXRfYm94X2ludDUxMihuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfYm94X2ludDUxMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6MzItMzMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNldF9ib3hfaW50NTEyKHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogYXJjNC5VSW50NTEyKSAtPiBOb25lOgogICAgcHJvdG8gMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjM0CiAgICAvLyBzZWxmLmJveF9pbnQ1MTJbbmFtZV0gPSB2YWx1ZQogICAgcHVzaGJ5dGVzICJib3hfaW50NTEyIgogICAgZnJhbWVfZGlnIC0yCiAgICBjb25jYXQKICAgIGZyYW1lX2RpZyAtMQogICAgYm94X3B1dAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkMy5jb250cmFjdC5UZXN0UHV5YUJveGVzLnNldF9ib3hfc3RhdGljKG5hbWU6IGJ5dGVzLCB2YWx1ZTogYnl0ZXMpIC0+IHZvaWQ6CnNldF9ib3hfc3RhdGljOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTozNi0zOQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2V0X2JveF9zdGF0aWMoCiAgICAvLyAgICAgc2VsZiwgbmFtZTogYXJjNC5TdHJpbmcsIHZhbHVlOiBhcmM0LlN0YXRpY0FycmF5W2FyYzQuQnl0ZSwgTGl0ZXJhbFs0XV0KICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0MAogICAgLy8gc2VsZi5ib3hfc3RhdGljW25hbWVdID0gdmFsdWUuY29weSgpCiAgICBwdXNoYnl0ZXMgImJveF9zdGF0aWMiCiAgICBmcmFtZV9kaWcgLTIKICAgIGNvbmNhdAogICAgZnJhbWVfZGlnIC0xCiAgICBib3hfcHV0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuc2V0X3N0cnVjdChuYW1lOiBieXRlcywgdmFsdWU6IGJ5dGVzKSAtPiB2b2lkOgpzZXRfc3RydWN0OgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkMy9jb250cmFjdC5weTo0Mi00MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBzZXRfc3RydWN0KHNlbGYsIG5hbWU6IGFyYzQuU3RyaW5nLCB2YWx1ZTogRHVtbXlTdHJ1Y3QpIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZDMvY29udHJhY3QucHk6NDQKICAgIC8vIGFzc2VydCBuYW1lLmJ5dGVzID09IHZhbHVlLm5hbWUuYnl0ZXMsICJOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0IgogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzEgLy8gMAogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBOYW1lIG11c3QgbWF0Y2ggaWQgb2Ygc3RydWN0CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQzL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBvcC5Cb3gucHV0KG5hbWUuYnl0ZXMsIHZhbHVlLmJ5dGVzKQogICAgZnJhbWVfZGlnIC0yCiAgICBmcmFtZV9kaWcgLTEKICAgIGJveF9wdXQKICAgIHJldHN1Ygo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQzLmNvbnRyYWN0LlRlc3RQdXlhQm94ZXMuY2xlYXJfc3RhdGVfcHJvZ3JhbToKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "TestPuyaBoxes", + "methods": [ + { + "name": "set_box_bytes", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[]", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_str", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "string", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_int", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint32", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_int512", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "uint512", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_box_static", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "byte[4]", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + }, + { + "name": "set_struct", + "args": [ + { + "type": "string", + "name": "name" + }, + { + "type": "(string,uint64)", + "name": "value" + } + ], + "readonly": false, + "returns": { + "type": "void" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} diff --git a/tests/artifacts/testing_app_puya/contract.py b/tests/artifacts/testing_app_puya/contract.py new file mode 100644 index 00000000..7074dd6b --- /dev/null +++ b/tests/artifacts/testing_app_puya/contract.py @@ -0,0 +1,43 @@ +from typing import Literal + +from algopy import ARC4Contract, BoxMap, Bytes, arc4, op + + +class DummyStruct(arc4.Struct): + name: arc4.String + id: arc4.UInt64 + + +class TestPuyaBoxes(ARC4Contract): + def __init__(self) -> None: + self.box_bytes = BoxMap(arc4.String, Bytes) + self.box_bytes2 = BoxMap(Bytes, Bytes) + self.box_str = BoxMap(arc4.String, arc4.String) + self.box_int = BoxMap(arc4.String, arc4.UInt32) + self.box_int512 = BoxMap(arc4.String, arc4.UInt512) + self.box_static = BoxMap(arc4.String, arc4.StaticArray[arc4.Byte, Literal[4]]) + + @arc4.abimethod + def set_box_bytes(self, name: arc4.String, value: Bytes) -> None: + self.box_bytes[name] = value + + @arc4.abimethod + def set_box_str(self, name: arc4.String, value: arc4.String) -> None: + self.box_str[name] = value + + @arc4.abimethod + def set_box_int(self, name: arc4.String, value: arc4.UInt32) -> None: + self.box_int[name] = value + + @arc4.abimethod + def set_box_int512(self, name: arc4.String, value: arc4.UInt512) -> None: + self.box_int512[name] = value + + @arc4.abimethod + def set_box_static(self, name: arc4.String, value: arc4.StaticArray[arc4.Byte, Literal[4]]) -> None: + self.box_static[name] = value.copy() + + @arc4.abimethod() + def set_struct(self, name: arc4.String, value: DummyStruct) -> None: + assert name.bytes == value.name.bytes, "Name must match id of struct" + op.Box.put(name.bytes, value.bytes) diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index 61e5c255..2d5ea4e4 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -1,6 +1,7 @@ -import algosdk import pytest -from algokit_utils import Account, get_account +from algosdk.atomic_transaction_composer import AccountTransactionSigner + +from algokit_utils import Account from algokit_utils.assets.asset_manager import ( AccountAssetInformation, AssetInformation, @@ -12,26 +13,32 @@ AssetCreateParams, PaymentParams, ) -from algosdk.atomic_transaction_composer import AccountTransactionSigner -from tests.conftest import get_unique_name +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() -@pytest.fixture() -def sender(funded_account: Account) -> Account: - return funded_account - -@pytest.fixture() -def receiver(algod_client: algosdk.v2client.algod.AlgodClient) -> Account: - return get_account(algod_client, get_unique_name()) +@pytest.fixture +def sender(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def receiver(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + return new_account def test_get_by_id(algorand: AlgorandClient, sender: Account) -> None: diff --git a/tests/clients/algorand_client/__init__.py b/tests/clients/algorand_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py new file mode 100644 index 00000000..a7637f58 --- /dev/null +++ b/tests/clients/algorand_client/test_transfer.py @@ -0,0 +1,427 @@ +import httpx +import pytest +from pytest_httpx._httpx_mock import HTTPXMock + +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.clients.dispenser_api_client import DispenserApiConfig, TestNetDispenserApiClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AssetOptInParams, + AssetTransferParams, + PaymentParams, +) +from tests.conftest import generate_test_asset + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + result = algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(5), + note=b"Transfer 5 Algos", + ) + ) + + account_info = algorand.account.get_information(second_account) + + assert result.transaction.payment + assert result.transaction.payment.amt == 5_000_000 + + assert result.transaction.payment.sender == funded_account.address == result.confirmation["txn"]["txn"]["snd"] # type: ignore # noqa: PGH003 + assert account_info["amount"] == 5_000_000 + + +def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(1), + lease=b"test", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(2), + lease=b"test", + ) + ) + + +def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(1), + lease=b"\x01\x02\x03\x04", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=second_account.address, + amount=AlgoAmount.from_algos(2), + lease=b"\x01\x02\x03\x04", + ) + ) + + +def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=1, + lease=b"test", + ) + ) + + with pytest.raises(Exception, match="overlapping lease"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=2, + lease=b"test", + ) + ) + + +def test_transfer_asa_receiver_not_opted_in( + algorand: AlgorandClient, + funded_account: Account, +) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + + with pytest.raises(Exception, match="receiver error: must optin"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=1, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + +def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + with pytest.raises(Exception, match=f"asset {test_asset_id} missing from {second_account.address}"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=second_account.address, + receiver=funded_account.address, + asset_id=test_asset_id, + amount=1, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + +def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + with pytest.raises(Exception, match=f"asset 123123 missing from {funded_account.address}"): + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=123123, + amount=5, + note=b"Transfer asset with wrong id", + ) + ) + + +def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + second_account_info = algorand.asset.get_account_information(second_account, test_asset_id) + assert second_account_info.balance == 5 + + test_account_info = algorand.asset.get_account_information(funded_account, test_asset_id) + assert test_account_info.balance == 95 + + +def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_account: Account) -> None: + test_asset_id = generate_test_asset(algorand, funded_account, 100) + second_account = algorand.account.random() + clawback_account = algorand.account.random() + + algorand.account.ensure_funded( + account_to_fund=second_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + algorand.account.ensure_funded( + account_to_fund=clawback_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=clawback_account.address, + asset_id=test_asset_id, + ) + ) + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=clawback_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + ) + ) + + clawback_from_info = algorand.asset.get_account_information(clawback_account, test_asset_id) + assert clawback_from_info.balance == 5 + + algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=second_account.address, + asset_id=test_asset_id, + amount=5, + note=b"Transfer 5 assets with id %d" % test_asset_id, + clawback_target=clawback_account.address, + ) + ) + + second_account_info = algorand.asset.get_account_information(second_account, test_asset_id) + assert second_account_info.balance == 5 + + clawback_account_info = algorand.asset.get_account_information(clawback_account, test_asset_id) + assert clawback_account_info.balance == 0 + + test_account_info = algorand.asset.get_account_information(funded_account, test_asset_id) + assert test_account_info.balance == 95 + + +MINIMUM_BALANCE = AlgoAmount.from_micro_algos( + 100_000 +) # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance + + +def test_ensure_funded(algorand: AlgorandClient, funded_account: Account) -> None: + test_account = algorand.account.random() + response = algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1), + ) + assert response is not None + + to_account_info = algorand.account.get_information(test_account) + assert isinstance(to_account_info, dict) + actual_amount = to_account_info.get("amount") + assert actual_amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + + +def test_ensure_funded_uses_dispenser_by_default( + algorand: AlgorandClient, +) -> None: + second_account = algorand.account.random() + dispenser = algorand.account.dispenser_from_environment() + + result = algorand.account.ensure_funded_from_environment( + account_to_fund=second_account, + min_spending_balance=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + + assert result is not None + assert result.transaction.payment is not None + assert result.transaction.payment.sender == dispenser.address + + account_info = algorand.account.get_information(second_account) + assert account_info["amount"] == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + + +def test_ensure_funded_respects_minimum_funding_increment(algorand: AlgorandClient, funded_account: Account) -> None: + test_account = algorand.account.random() + response = algorand.account.ensure_funded( + account_to_fund=test_account, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_micro_algo(1), + min_funding_increment=AlgoAmount.from_algos(1), + ) + assert response is not None + + to_account_info = algorand.account.get_information(test_account) + assert isinstance(to_account_info, dict) + actual_amount = to_account_info.get("amount") + assert actual_amount == AlgoAmount.from_algos(1) + + +def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: + algorand = AlgorandClient.test_net() + account_to_fund = algorand.account.random() + monkeypatch.setenv( + "ALGOKIT_DISPENSER_ACCESS_TOKEN", + "dummy", + ) + httpx_mock.add_response( + url=f"{DispenserApiConfig.BASE_URL}/fund/0", + method="POST", + json={"amount": 1, "txID": "dummy_tx_id"}, + ) + + result = algorand.account.ensure_funded_from_testnet_dispenser_api( + account_to_fund=account_to_fund, + dispenser_client=TestNetDispenserApiClient(), + min_spending_balance=AlgoAmount.from_micro_algo(1), + ) + assert result is not None + assert result.transaction_id == "dummy_tx_id" + assert result.amount_funded == AlgoAmount.from_micro_algo(1) + + +def test_ensure_funded_testnet_api_bad_response(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: + algorand = AlgorandClient.test_net() + account_to_fund = algorand.account.random() + monkeypatch.setenv( + "ALGOKIT_DISPENSER_ACCESS_TOKEN", + "dummy", + ) + httpx_mock.add_exception( + httpx.HTTPStatusError( + "Limit exceeded", + request=httpx.Request("POST", f"{DispenserApiConfig.BASE_URL}/fund"), + response=httpx.Response( + 400, + request=httpx.Request("POST", f"{DispenserApiConfig.BASE_URL}/fund"), + json={ + "code": "fund_limit_exceeded", + "limit": 10_000_000, + "resetsAt": "2023-09-19T10:07:34.024Z", + }, + ), + ), + url=f"{DispenserApiConfig.BASE_URL}/fund/0", + method="POST", + ) + + with pytest.raises(Exception, match="fund_limit_exceeded"): + algorand.account.ensure_funded_from_testnet_dispenser_api( + account_to_fund=account_to_fund, + dispenser_client=TestNetDispenserApiClient(), + min_spending_balance=AlgoAmount.from_micro_algo(1), + ) + + +def test_rekey_works(algorand: AlgorandClient, funded_account: Account) -> None: + second_account = algorand.account.random() + + algorand.account.rekey_account(funded_account, second_account, note=b"rekey") + + # This will throw if the rekey wasn't successful + algorand.send.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(1), + signer=second_account.signer, + ) + ) diff --git a/tests/clients/test_algorand_client.py b/tests/clients/test_algorand_client.py deleted file mode 100644 index ce0f90d5..00000000 --- a/tests/clients/test_algorand_client.py +++ /dev/null @@ -1,223 +0,0 @@ -# TODO: Update tests for latest version of algokit-utils -# import json -# from pathlib import Path - -# import pytest -# from algokit_utils import Account, ApplicationClient -# from algokit_utils.accounts.account_manager import AddressAndSigner -# from algokit_utils.clients.algorand_client import ( -# AlgorandClient, -# AppMethodCallParams, -# AssetCreateParams, -# AssetOptInParams, -# PaymentParams, -# ) -# from algosdk.abi import Contract -# from algosdk.atomic_transaction_composer import AtomicTransactionComposer - - -# @pytest.fixture() -# def algorand(funded_account: Account) -> AlgorandClient: -# client = AlgorandClient.default_local_net() -# client.set_signer(sender=funded_account.address, signer=funded_account.signer) -# return client - - -# @pytest.fixture() -# def alice(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: -# acct = algorand.account.random() -# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) -# return acct - - -# @pytest.fixture() -# def bob(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: -# acct = algorand.account.random() -# algorand.send.payment(PaymentParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) -# return acct - - -# @pytest.fixture() -# def app_client(algorand: AlgorandClient, alice: AddressAndSigner) -> ApplicationClient: -# client = ApplicationClient( -# algorand.client.algod, -# Path(__file__).parent / "app_algorand_client.json", -# sender=alice.address, -# signer=alice.signer, -# ) -# client.create(call_abi_method="createApplication") -# return client - - -# @pytest.fixture() -# def contract() -> Contract: -# with Path.open(Path(__file__).parent / "app_algorand_client.json") as f: -# return Contract.from_json(json.dumps(json.load(f)["contract"])) - - -# def test_send_payment(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: -# amount = 100_000 - -# alice_pre_balance = algorand.account.get_information(alice.address)["amount"] -# bob_pre_balance = algorand.account.get_information(bob.address)["amount"] -# result = algorand.send.payment(PaymentParams(sender=alice.address, receiver=bob.address, amount=amount)) -# alice_post_balance = algorand.account.get_information(alice.address)["amount"] -# bob_post_balance = algorand.account.get_information(bob.address)["amount"] - -# assert result["confirmation"] is not None -# assert alice_post_balance == alice_pre_balance - 1000 - amount -# assert bob_post_balance == bob_pre_balance + amount - - -# def test_send_asset_create(algorand: AlgorandClient, alice: AddressAndSigner) -> None: -# total = 100 - -# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) -# asset_index = result["confirmation"]["asset-index"] - -# assert asset_index > 0 - - -# def test_asset_opt_in(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: -# total = 100 - -# result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) -# asset_index = result["confirmation"]["asset-index"] - -# algorand.send.asset_opt_in(AssetOptInParams(sender=bob.address, asset_id=asset_index)) - -# assert algorand.account.get_asset_information(bob.address, asset_index) is not None - - -# DO_MATH_VALUE = 3 - - -# def test_add_atc(algorand: AlgorandClient, app_client: ApplicationClient, alice: AddressAndSigner) -> None: -# atc = AtomicTransactionComposer() -# app_client.compose_call(atc, call_abi_method="doMath", a=1, b=2, operation="sum") - -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_atc(atc) -# .execute() -# ) -# assert result.abi_results[0].return_value == DO_MATH_VALUE - - -# def test_add_method_call( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("doMath"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[1, 2, "sum"], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == DO_MATH_VALUE - - -# def test_add_method_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# result = ( -# algorand.new_group() -# .add_payment(PaymentParams(sender=alice.address, amount=0, receiver=alice.address)) -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[pay_arg], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address - - -# def test_add_method_call_with_method_call_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# hello_world_call = AppMethodCallParams( -# method=contract.get_method_by_name("helloWorld"), sender=alice.address, app_id=app_client.app_id -# ) -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("methodArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[hello_world_call], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == "Hello, World!" -# assert result.abi_results[1].return_value == app_client.app_id - - -# def test_add_method_call_with_method_call_arg_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# txn_arg_call = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg] -# ) -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("nestedTxnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[txn_arg_call], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address -# assert result.abi_results[1].return_value == app_client.app_id - - -# def test_add_method_call_with_two_method_call_args_with_txn_arg( -# algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -# ) -> None: -# pay_arg_1 = PaymentParams(sender=alice.address, receiver=alice.address, amount=1) -# txn_arg_call_1 = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[pay_arg_1], -# note=b"1", -# ) - -# pay_arg_2 = PaymentParams(sender=alice.address, receiver=alice.address, amount=2) -# txn_arg_call_2 = AppMethodCallParams( -# method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg_2] -# ) - -# result = ( -# algorand.new_group() -# .add_method_call( -# AppMethodCallParams( -# method=contract.get_method_by_name("doubleNestedTxnArg"), -# sender=alice.address, -# app_id=app_client.app_id, -# args=[txn_arg_call_1, txn_arg_call_2], -# ) -# ) -# .execute() -# ) -# assert result.abi_results[0].return_value == alice.address -# assert result.abi_results[1].return_value == alice.address -# assert result.abi_results[2].return_value == app_client.app_id diff --git a/tests/conftest.py b/tests/conftest.py index 18021c21..9499465e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,9 @@ from typing import TYPE_CHECKING from uuid import uuid4 -import algosdk.transaction import pytest +from dotenv import load_dotenv + from algokit_utils import ( DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, @@ -16,20 +17,13 @@ ApplicationSpecification, EnsureBalanceParameters, ensure_funded, - get_account, - get_algod_client, - get_indexer_client, - get_kmd_client_from_algod_client, replace_template_variables, ) -from dotenv import load_dotenv - -from legacy_v2_tests import app_client_test +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.transactions.transaction_composer import AssetCreateParams if TYPE_CHECKING: - from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient @pytest.fixture(autouse=True, scope="session") @@ -127,77 +121,30 @@ def is_opted_in(client_fixture: ApplicationClient) -> bool: return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) -@pytest.fixture(scope="session") -def algod_client() -> "AlgodClient": - return get_algod_client() - - -@pytest.fixture(scope="session") -def kmd_client(algod_client: "AlgodClient") -> "KMDClient": - return get_kmd_client_from_algod_client(algod_client) - - -@pytest.fixture(scope="session") -def indexer_client() -> "IndexerClient": - return get_indexer_client() - - -@pytest.fixture() -def creator(algod_client: "AlgodClient") -> Account: - creator_name = get_unique_name() - return get_account(algod_client, creator_name) - - -@pytest.fixture(scope="session") -def funded_account(algod_client: "AlgodClient") -> Account: - creator_name = get_unique_name() - return get_account(algod_client, creator_name) - - -@pytest.fixture(scope="session") -def app_spec() -> ApplicationSpecification: - app_spec = app_client_test.app.build() - path = Path(__file__).parent / "app_client_test.json" - path.write_text(app_spec.to_json()) - return read_spec("app_client_test.json", deletable=True, updatable=True, template_values={"VERSION": 1}) - - -def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int | None) -> int: +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None) -> int: if total is None: total = math.floor(random.random() * 100) + 20 decimals = 0 asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}" - params = algod_client.suggested_params() - - txn = algosdk.transaction.AssetConfigTxn( - sender=sender.address, - sp=params, - total=total * 10**decimals, - decimals=decimals, - default_frozen=False, - unit_name="", - asset_name=asset_name, - manager=sender.address, - reserve=sender.address, - freeze=sender.address, - clawback=sender.address, - url="https://path/to/my/asset/details", - metadata_hash=None, - note=None, - lease=None, - rekey_to=None, + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=decimals, + default_frozen=False, + unit_name="CFG", + asset_name=asset_name, + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) ) - signed_transaction = txn.sign(sender.private_key) - algod_client.send_transaction(signed_transaction) - ptx = algod_client.pending_transaction_info(txn.get_txid()) - - if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): - return ptx["asset-index"] - else: - raise ValueError("Unexpected response from pending_transaction_info") + return int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] def assure_funds(algod_client: "AlgodClient", account: Account) -> None: diff --git a/tests/test_transaction_composer.py b/tests/test_transaction_composer.py index 5ea937ec..a7096c83 100644 --- a/tests/test_transaction_composer.py +++ b/tests/test_transaction_composer.py @@ -1,6 +1,13 @@ from typing import TYPE_CHECKING import pytest +from algosdk.transaction import ( + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -13,27 +20,29 @@ SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.transaction import ( - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algokit_utils.transactions.models import Arc2TransactionNote -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient) -> Account: secondary_name = get_unique_name() return get_account(algorand.client.algod, secondary_name) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 619668e8..9217cc08 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -3,6 +3,14 @@ import algosdk import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -16,28 +24,29 @@ SendAtomicTransactionComposerResults, TransactionComposer, ) -from algosdk.transaction import ( - ApplicationCallTxn, - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algokit_utils.transactions.models import Arc2TransactionNote -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() + +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient) -> Account: secondary_name = get_unique_name() return get_account(algorand.client.algod, secondary_name) @@ -82,7 +91,7 @@ def test_add_asset_create(algorand: AlgorandClient, funded_account: Account) -> composer.add_asset_create(params) built = composer.build_transactions() - response = composer.execute(max_rounds_to_wait=20) + response = composer.send(max_rounds_to_wait=20) created_asset = algorand.client.algod.asset_info( algorand.client.algod.pending_transaction_info(response.tx_ids[0])["asset-index"] # type: ignore[call-overload] )["params"] @@ -138,7 +147,7 @@ def test_add_asset_config(algorand: AlgorandClient, funded_account: Account, fun assert txn.index == asset_before_config_index assert txn.manager == funded_secondary_account.address - composer.execute(max_rounds_to_wait=20) + composer.send(max_rounds_to_wait=20) updated_asset = algorand.client.algod.asset_info(asset_id=asset_before_config_index)["params"] # type: ignore[call-overload] assert updated_asset["manager"] == funded_secondary_account.address @@ -165,7 +174,7 @@ def test_add_app_create(algorand: AlgorandClient, funded_account: Account) -> No assert txn.sender == funded_account.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" - composer.execute(max_rounds_to_wait=20) + composer.send(max_rounds_to_wait=20) def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Account) -> None: @@ -173,8 +182,8 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, ) - approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() - clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() composer.add_app_create( AppCreateParams( sender=funded_account.address, @@ -183,7 +192,7 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, ) ) - response = composer.execute() + response = composer.send() app_id = algorand.client.algod.pending_transaction_info(response.tx_ids[0])["application-index"] # type: ignore[call-overload] composer = TransactionComposer( @@ -204,8 +213,8 @@ def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: Acco assert isinstance(built.transactions[0], ApplicationCallTxn) txn = built.transactions[0] assert txn.sender == funded_account.address - response = composer.execute(max_rounds_to_wait=20) - assert response.returns[-1] == "Hello, world" + response = composer.send(max_rounds_to_wait=20) + assert response.returns[-1].return_value == "Hello, world" def test_simulate(algorand: AlgorandClient, funded_account: Account) -> None: diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index ec84d650..3c944041 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -2,6 +2,18 @@ import algosdk import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + ApplicationCreateTxn, + AssetConfigTxn, + AssetCreateTxn, + AssetDestroyTxn, + AssetFreezeTxn, + AssetTransferTxn, + KeyregTxn, + PaymentTxn, +) + from algokit_utils._legacy_v2.account import get_account from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.account import Account @@ -19,29 +31,26 @@ OnlineKeyRegistrationParams, PaymentParams, ) -from algosdk.transaction import ( - ApplicationCallTxn, - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - AssetDestroyTxn, - AssetFreezeTxn, - AssetTransferTxn, - KeyregTxn, - PaymentTxn, -) - from legacy_v2_tests.conftest import get_unique_name -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def funded_secondary_account(algorand: AlgorandClient, funded_account: Account) -> Account: secondary_name = get_unique_name() account = get_account(algorand.client.algod, secondary_name) @@ -210,8 +219,8 @@ def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: Account) -> None: - approval_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "approval.teal").read_text() - clear_state_program = Path(Path(__file__).parent / "artifacts" / "hello_world" / "clear.teal").read_text() + approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() # First create the app create_result = algorand.send.app_create( @@ -222,7 +231,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, ) ) - app_id = algorand.client.algod.pending_transaction_info(create_result.tx_id)["application-index"] # type: ignore[call-overload] + app_id = algorand.client.algod.pending_transaction_info(create_result.tx_ids[0])["application-index"] # type: ignore[call-overload] # Then test creating a method call transaction result = algorand.create_transaction.app_call_method_call( diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index b8514cdb..def636fd 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -1,15 +1,18 @@ -from typing import TYPE_CHECKING, cast +from pathlib import Path from unittest.mock import MagicMock, patch +import algosdk import pytest -from algokit_utils import ( - Account, - get_account, -) + +from algokit_utils import Account +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.clients.algorand_client import AlgorandClient from algokit_utils.models.amount import AlgoAmount from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCall, + AppCallParams, AppCreateParams, AssetConfigParams, AssetCreateParams, @@ -23,47 +26,86 @@ TransactionComposer, ) from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender -from algosdk.transaction import ( - ApplicationCreateTxn, - AssetConfigTxn, - AssetCreateTxn, - AssetDestroyTxn, - AssetFreezeTxn, - AssetTransferTxn, - PaymentTxn, -) -from tests.conftest import get_unique_name -if TYPE_CHECKING: - import algosdk +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_local_net() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account -@pytest.fixture() +@pytest.fixture def sender(funded_account: Account) -> Account: return funded_account -@pytest.fixture() -def receiver(algod_client: "algosdk.v2client.algod.AlgodClient") -> Account: - return get_account(algod_client, get_unique_name()) +@pytest.fixture +def receiver(algorand: AlgorandClient) -> Account: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + return new_account + + +@pytest.fixture +def raw_hello_world_arc32_app_spec() -> str: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def test_hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def test_hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: Account, test_hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = test_hello_world_arc32_app_spec.global_state_schema + local_schema = test_hello_world_arc32_app_spec.local_state_schema + response = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=test_hello_world_arc32_app_spec.approval_program, + clear_state_program=test_hello_world_arc32_app_spec.clear_program, + schema={ + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "global_bytes": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_bytes": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + }, + ) + ) + return response.app_id -@pytest.fixture() -def transaction_sender( - algod_client: "algosdk.v2client.algod.AlgodClient", sender: Account -) -> AlgorandClientTransactionSender: +@pytest.fixture +def transaction_sender(algorand: AlgorandClient, sender: Account) -> AlgorandClientTransactionSender: def new_group() -> TransactionComposer: return TransactionComposer( - algod=algod_client, + algod=algorand.client.algod, get_signer=lambda _: sender.signer, ) return AlgorandClientTransactionSender( new_group=new_group, - asset_manager=AssetManager(algod_client, new_group), - app_manager=AppManager(algod_client), - algod_client=algod_client, + asset_manager=AssetManager(algorand.client.algod, new_group), + app_manager=AppManager(algorand.client.algod), + algod_client=algorand.client.algod, ) @@ -79,7 +121,8 @@ def test_payment(transaction_sender: AlgorandClientTransactionSender, sender: Ac assert len(result.tx_ids) == 1 assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - txn = cast(PaymentTxn, result.transaction) + txn = result.transaction.payment + assert txn assert txn.sender == sender.address assert txn.receiver == receiver.address assert txn.amt == amount.micro_algos @@ -100,7 +143,8 @@ def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_create(params) assert len(result.tx_ids) == 1 assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] - txn = cast(AssetCreateTxn, result.transaction) + txn = result.transaction.asset_config + assert txn assert txn.sender == sender.address assert txn.total == total assert txn.decimals == 0 @@ -135,10 +179,12 @@ def test_asset_config(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_config(config_params) assert len(result.tx_ids) == 1 - assert isinstance(result.transaction, AssetConfigTxn) - assert result.transaction.sender == sender.address - assert result.transaction.index == asset_id - assert result.transaction.manager == receiver.address + assert result.transaction.asset_config + txn = result.transaction.asset_config + assert txn + assert txn.sender == sender.address + assert txn.index == asset_id + assert txn.manager == receiver.address def test_asset_freeze( @@ -171,7 +217,9 @@ def test_asset_freeze( result = transaction_sender.asset_freeze(freeze_params) assert len(result.tx_ids) == 1 - txn = cast(AssetFreezeTxn, result.transaction) + assert result.transaction.asset_freeze + txn = result.transaction.asset_freeze + assert txn assert txn.sender == sender.address assert txn.index == asset_id assert txn.target == sender.address @@ -202,7 +250,8 @@ def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, send result = transaction_sender.asset_destroy(destroy_params) assert len(result.tx_ids) == 1 - txn = cast(AssetDestroyTxn, result.transaction) + txn = result.transaction.asset_config + assert txn assert txn.sender == sender.address assert txn.index == asset_id @@ -244,7 +293,8 @@ def test_asset_transfer( result = transaction_sender.asset_transfer(transfer_params) assert len(result.tx_ids) == 1 - txn = cast(AssetTransferTxn, result.transaction) + txn = result.transaction.asset_transfer + assert txn assert txn.sender == sender.address assert txn.index == asset_id assert txn.receiver == receiver.address @@ -275,7 +325,8 @@ def test_asset_opt_in(transaction_sender: AlgorandClientTransactionSender, sende result = transaction_sender.asset_opt_in(opt_in_params) assert len(result.tx_ids) == 1 - txn = cast(AssetTransferTxn, result.transaction) + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer assert txn.sender == receiver.address assert txn.index == asset_id assert txn.amount == 0 @@ -315,7 +366,8 @@ def test_asset_opt_out(transaction_sender: AlgorandClientTransactionSender, send ) result = transaction_sender.asset_opt_out(params=opt_out_params) - txn = cast(AssetTransferTxn, result.transaction) + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer assert txn.sender == receiver.address assert txn.index == asset_id assert txn.amount == 0 @@ -336,13 +388,40 @@ def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: result = transaction_sender.app_create(params) assert result.app_id > 0 assert result.app_address - txn = cast(ApplicationCreateTxn, result.transaction) + + assert result.transaction.application_call + txn = result.transaction.application_call assert txn.sender == sender.address assert txn.approval_program == b"\x06\x81\x01" assert txn.clear_program == b"\x06\x81\x01" -# TODO: add remaining app call and app method call tests +def test_app_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account +) -> None: + params = AppCallParams( + app_id=test_hello_world_arc32_app_id, + sender=sender.address, + args=[b"\x02\xbe\xce\x11", b"test"], + ) + + result = transaction_sender.app_call(params) + assert not result.return_value # TODO: improve checks + + +def test_app_call_method_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: Account +) -> None: + params = AppCallMethodCall( + app_id=test_hello_world_arc32_app_id, + sender=sender.address, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["test"], + ) + + result = transaction_sender.app_call_method_call(params) + assert result.return_value + assert result.return_value.return_value == "Hello2, test" @patch("logging.Logger.debug") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..612ea60d --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,29 @@ +from pathlib import Path + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME + + +def load_arc32_spec( + path: Path, + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> ApplicationSpecification: + spec = ApplicationSpecification.from_json(path.read_text(encoding="utf-8")) + + template_variables = template_values or {} + if updatable is not None: + template_variables["UPDATABLE"] = int(updatable) + + if deletable is not None: + template_variables["DELETABLE"] = int(deletable) + + spec.approval_program = ( + AppManager.replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) + return spec