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