diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index ea7a5d9f..1e9256f4 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -45,12 +45,8 @@ jobs: - name: Check types with mypy run: poetry run mypy - - name: Check docs are up to date - run: | - poetry run poe docs - git diff --quiet --exit-code \ - ':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \ - ':!docs/html/apidocs/algokit_utils/algokit_utils.html' \ - ':!docs/html/searchindex.js' \ - ':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \ - docs/ + # TODO: Restore before prod release of v3 + # - name: Check docs are up to date + # run: | + # poetry run poe docs-md-only + # git diff --exit-code ':!docs/markdown/autoapi/index.md' ':!docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md' docs diff --git a/.gitignore b/.gitignore index 81433e4c..e7713f87 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ cython_debug/ /docs/source/apidocs !docs/html + +# Received approval test files +*.received.* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10c320aa..107998e0 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,11 @@ repos: additional_dependencies: [] minimum_pre_commit_version: "2.9.2" files: "^(src|tests)/" + exclude: "^tests/artifacts/" + - id: docstrings-check + name: docstrings-check + description: "Check docstrings for correctness" + entry: poetry run poe docstrings-check + language: system + types: [python] + files: "^(src)/" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..49cabdde --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "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/README.md b/README.md index b6e83f2e..820cfac7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. > **Note** @@ -19,13 +19,17 @@ This library can be installed using pip, e.g.: pip install algokit-utils ``` +## Migration from `v2.x` to `v3.x` + +Refer to the [v3 migration guide](./docs/source/v3-migration-guide.md) for more information on how to migrate to latest version of `algokit-utils-py`. + ## Guiding principles This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles). ## Contributing -This is an open source project managed by the Algorand Foundation. +This is an open source project managed by the Algorand Foundation. See the [AlgoKit contributing page](https://github.com/algorandfoundation/algokit-cli/blob/main/CONTRIBUTING.MD) to learn about making improvements. To successfully run the tests in this repository you need to be running LocalNet via [AlgoKit](https://github.com/algorandfoundation/algokit-cli): diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..dc1312ab --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/markdown/apidocs/algokit_utils/algokit_utils.md b/docs/markdown/apidocs/algokit_utils/algokit_utils.md deleted file mode 100644 index 8c991230..00000000 --- a/docs/markdown/apidocs/algokit_utils/algokit_utils.md +++ /dev/null @@ -1,1095 +0,0 @@ -# [`algokit_utils`](#module-algokit_utils) - -## Data - -### algokit_utils.AppSpecStateDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Type defining Application Specification state entries - -### algokit_utils.DELETABLE_TEMPLATE_NAME - -None - -Template variable name used to control if a smart contract is deletable or not at deployment - -### algokit_utils.DefaultArgumentType *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Literal values describing the types of default argument sources - -### algokit_utils.MethodConfigDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type - -### algokit_utils.NOTE_PREFIX - -‘ALGOKIT_DEPLOYER:j’ - -ARC-0002 compliant note prefix for algokit_utils deployed applications - -### algokit_utils.OnCompleteActionName *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -String literals representing on completion transaction types - -### algokit_utils.TemplateValueDict *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Dictionary of `dict[str, int | str | bytes]` representing template variable names and values - -### algokit_utils.TemplateValueMapping *: [TypeAlias](https://docs.python.org/3/library/typing.html#typing.TypeAlias)* - -None - -Mapping of `str` to `int | str | bytes` representing template variable names and values - -### algokit_utils.UPDATABLE_TEMPLATE_NAME - -None - -Template variable name used to control if a smart contract is updatable or not at deployment - -## Classes - -### *class* algokit_utils.ABICallArgs - -Bases: [`algokit_utils.deploy.DeployCallArgs`](#algokit_utils.DeployCallArgs), `algokit_utils.deploy.ABICall` - -ABI Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.ABICallArgsDict - -Bases: [`algokit_utils.deploy.DeployCallArgsDict`](#algokit_utils.DeployCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -ABI Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.ABICreateCallArgs - -Bases: [`algokit_utils.deploy.DeployCreateCallArgs`](#algokit_utils.DeployCreateCallArgs), `algokit_utils.deploy.ABICall` - -ABI Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.ABICreateCallArgsDict - -Bases: [`algokit_utils.deploy.DeployCreateCallArgsDict`](#algokit_utils.DeployCreateCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -ABI Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.ABITransactionResponse - -Bases: [`algokit_utils.models.TransactionResponse`](#algokit_utils.TransactionResponse), [`typing.Generic`](https://docs.python.org/3/library/typing.html#typing.Generic)[`algokit_utils.models.ReturnType`] - -Response for an ABI call - -#### decode_error *: [Exception](https://docs.python.org/3/library/exceptions.html#Exception) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Details of error that occurred when attempting to decode raw_value - -#### method *: algosdk.abi.Method* - -None - -ABI method used to make call - -#### raw_value *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)* - -None - -The raw response before ABI decoding - -#### return_value *: algokit_utils.models.ReturnType* - -None - -Decoded ABI result - -#### tx_info *: [dict](https://docs.python.org/3/library/stdtypes.html#dict)* - -None - -Details of transaction - -### *class* algokit_utils.Account - -Holds the private_key and address for an account - -#### address *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -‘field(…)’ - -Address for this account - -#### private_key *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Base64 encoded private key - -#### *property* public_key *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)* - -The public key for this account - -#### *property* signer *: [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner)* - -An AccountTransactionSigner for this account - -### *class* algokit_utils.AlgoClientConfig - -Connection details for connecting to an [`algosdk.v2client.algod.AlgodClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) or -[`algosdk.v2client.indexer.IndexerClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) - -#### server *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud` - -#### token *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -API Token to authenticate with the service - -### *class* algokit_utils.AppDeployMetaData - -Metadata about an application stored in a transaction note during creation. - -The note is serialized as JSON and prefixed with [`NOTE_PREFIX`](#algokit_utils.NOTE_PREFIX) and stored in the transaction note field -as part of [`ApplicationClient.deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.AppLookup - -Cache of [`AppMetaData`](#algokit_utils.AppMetaData) for a specific `creator` - -Can be used as an argument to [`ApplicationClient`](#algokit_utils.ApplicationClient) to reduce the number of calls when deploying multiple -apps or discovering multiple app_ids - -### *class* algokit_utils.AppMetaData - -Bases: [`algokit_utils.deploy.AppReference`](#algokit_utils.AppReference), [`algokit_utils.deploy.AppDeployMetaData`](#algokit_utils.AppDeployMetaData) - -Metadata about a deployed app - -### *class* algokit_utils.AppReference - -Information about an Algorand app - -### *class* algokit_utils.ApplicationClient(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), app_spec: [algokit_utils.application_specification.ApplicationSpecification](#algokit_utils.ApplicationSpecification) | [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), \*, app_id: [int](https://docs.python.org/3/library/functions.html#int) = 0, creator: [str](https://docs.python.org/3/library/stdtypes.html#str) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, indexer_client: IndexerClient | [None](https://docs.python.org/3/library/constants.html#None) = None, existing_deployments: [algokit_utils.deploy.AppLookup](#algokit_utils.AppLookup) | [None](https://docs.python.org/3/library/constants.html#None) = None, signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, suggested_params: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None) = None, template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_name: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) - -A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app - -### Initialization - -ApplicationClient can be created with an app_id to interact with an existing application, alternatively -it can be created with a creator and indexer_client specified to find existing applications by name and creator. - -* **Parameters:** - * **algod_client** (*AlgodClient*) – AlgoSDK algod client - * **app_spec** ([*ApplicationSpecification*](#algokit_utils.ApplicationSpecification) *|* *Path*) – An Application Specification or the path to one - * **app_id** ([*int*](https://docs.python.org/3/library/functions.html#int)) – The app_id of an existing application, to instead find the application by creator and name - use the creator and indexer_client parameters - * **creator** ([*str*](https://docs.python.org/3/library/stdtypes.html#str) *|* [*Account*](#algokit_utils.Account)) – The address or Account of the app creator to resolve the app_id - * **indexer_client** (*IndexerClient*) – AlgoSDK indexer client, only required if deploying or finding app_id by - creator and app name - * **existing_deployments** ([*AppLookup*](#algokit_utils.AppLookup)) – - * **signer** (*TransactionSigner* *|* [*Account*](#algokit_utils.Account)) – Account or signer to use to sign transactions, if not specified and - creator was passed as an Account will use that. - * **sender** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – Address to use as the sender for all transactions, will use the address associated with the - signer if not specified. - * **template_values** ([*TemplateValueMapping*](#algokit_utils.TemplateValueMapping)) – Values to use for TMPL_\* template variables, dictionary keys should - *NOT* include the TMPL_ prefix - * **app_name** ([*str*](https://docs.python.org/3/library/stdtypes.html#str) *|* [*None*](https://docs.python.org/3/library/constants.html#None)) – Name of application to use when deploying, defaults to name defined on the - Application Specification - -#### add_method_call(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, abi_args: algokit_utils.models.ABIArgsDict | [None](https://docs.python.org/3/library/constants.html#None) = None, app_id: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, on_complete: [algosdk.transaction.OnComplete](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.OnComplete) = transaction.OnComplete.NoOpOC, local_schema: [algosdk.transaction.StateSchema](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.StateSchema) | [None](https://docs.python.org/3/library/constants.html#None) = None, global_schema: [algosdk.transaction.StateSchema](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.StateSchema) | [None](https://docs.python.org/3/library/constants.html#None) = None, approval_program: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None) = None, clear_program: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None) = None, extra_pages: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None, call_config: [algokit_utils.application_specification.CallConfig](#algokit_utils.CallConfig) = au_spec.CallConfig.CALL) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a transaction to the AtomicTransactionComposer passed - -#### call(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.OnCompleteCallParameters](#algokit_utils.OnCompleteCallParameters) | [algokit_utils.models.OnCompleteCallParametersDict](#algokit_utils.OnCompleteCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with specified parameters - -#### clear_state(transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) - -Submits a signed transaction with on_complete=ClearState - -#### close_out(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=CloseOut - -#### compose_call(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.OnCompleteCallParameters](#algokit_utils.OnCompleteCallParameters) | [algokit_utils.models.OnCompleteCallParametersDict](#algokit_utils.OnCompleteCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with specified parameters to atc - -#### compose_clear_state(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_args: [list](https://docs.python.org/3/library/stdtypes.html#list)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=ClearState to atc - -#### compose_close_out(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=CloseOut to ac - -#### compose_create(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.CreateCallParameters](#algokit_utils.CreateCallParameters) | [algokit_utils.models.CreateCallParametersDict](#algokit_utils.CreateCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with application id == 0 and the schema and source of client’s app_spec to atc - -#### compose_delete(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=DeleteApplication to atc - -#### compose_opt_in(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=OptIn to atc - -#### compose_update(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), /, call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [None](https://docs.python.org/3/library/constants.html#None) - -Adds a signed transaction with on_complete=UpdateApplication to atc - -#### create(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.CreateCallParameters](#algokit_utils.CreateCallParameters) | [algokit_utils.models.CreateCallParametersDict](#algokit_utils.CreateCallParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with application id == 0 and the schema and source of client’s app_spec - -#### delete(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=DeleteApplication - -#### deploy(version: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, allow_update: [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, allow_delete: [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, on_update: [algokit_utils.deploy.OnUpdate](#algokit_utils.OnUpdate) = au_deploy.OnUpdate.Fail, on_schema_break: [algokit_utils.deploy.OnSchemaBreak](#algokit_utils.OnSchemaBreak) = au_deploy.OnSchemaBreak.Fail, template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping) | [None](https://docs.python.org/3/library/constants.html#None) = None, create_args: [algokit_utils.deploy.ABICreateCallArgs](#algokit_utils.ABICreateCallArgs) | [algokit_utils.deploy.ABICreateCallArgsDict](#algokit_utils.ABICreateCallArgsDict) | [algokit_utils.deploy.DeployCreateCallArgs](#algokit_utils.DeployCreateCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None, update_args: [algokit_utils.deploy.ABICallArgs](#algokit_utils.ABICallArgs) | [algokit_utils.deploy.ABICallArgsDict](#algokit_utils.ABICallArgsDict) | [algokit_utils.deploy.DeployCallArgs](#algokit_utils.DeployCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None, delete_args: [algokit_utils.deploy.ABICallArgs](#algokit_utils.ABICallArgs) | [algokit_utils.deploy.ABICallArgsDict](#algokit_utils.ABICallArgsDict) | [algokit_utils.deploy.DeployCallArgs](#algokit_utils.DeployCallArgs) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.deploy.DeployResponse](#algokit_utils.DeployResponse) - -Deploy an application and update client to reference it. - -Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator -account, including deploy-time template placeholder substitutions. -To understand the architecture decisions behind this functionality please see -[https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md) - -#### NOTE -If there is a breaking state schema change to an existing app (and `on_schema_break` is set to -‘ReplaceApp’ the existing app will be deleted and re-created. - -#### NOTE -If there is an update (different TEAL code) to an existing app (and `on_update` is set to ‘ReplaceApp’) -the existing app will be deleted and re-created. - -* **Parameters:** - * **version** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – version to use when creating or updating app, if None version will be auto incremented - * **signer** ([*algosdk.atomic_transaction_composer.TransactionSigner*](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner)) – signer to use when deploying app - , if None uses self.signer - * **sender** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – sender address to use when deploying app, if None uses self.sender - * **allow_delete** ([*bool*](https://docs.python.org/3/library/functions.html#bool)) – Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app - can be deleted - * **allow_update** ([*bool*](https://docs.python.org/3/library/functions.html#bool)) – Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app - can be updated - * **on_update** ([*OnUpdate*](#algokit_utils.OnUpdate)) – Determines what action to take if an application update is required - * **on_schema_break** ([*OnSchemaBreak*](#algokit_utils.OnSchemaBreak)) – Determines what action to take if an application schema requirements - has increased beyond the current allocation - * **template_values** ([*dict*](https://docs.python.org/3/library/stdtypes.html#dict) *[*[*str*](https://docs.python.org/3/library/stdtypes.html#str) *,* [*int*](https://docs.python.org/3/library/functions.html#int) *|*[*str*](https://docs.python.org/3/library/stdtypes.html#str) *|*[*bytes*](https://docs.python.org/3/library/stdtypes.html#bytes) *]*) – Values to use for `TMPL_*` template variables, dictionary keys - should *NOT* include the TMPL_ prefix - * **create_args** ([*ABICreateCallArgs*](#algokit_utils.ABICreateCallArgs)) – Arguments used when creating an application - * **update_args** ([*ABICallArgs*](#algokit_utils.ABICallArgs) *|* [*ABICallArgsDict*](#algokit_utils.ABICallArgsDict)) – Arguments used when updating an application - * **delete_args** ([*ABICallArgs*](#algokit_utils.ABICallArgs) *|* [*ABICallArgsDict*](#algokit_utils.ABICallArgsDict)) – Arguments used when deleting an application -* **Return DeployResponse:** - details action taken and relevant transactions -* **Raises:** - **DeploymentError** – If the deployment failed - -#### export_source_map() → [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) - -Export approval source map to JSON, can be later re-imported with `import_source_map` - -#### get_global_state(\*, raw: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)] - -Gets the global state info associated with app_id - -#### get_local_state(account: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*, raw: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)] - -Gets the local state info for associated app_id and account/sender - -#### get_signer_sender(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None), [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)] - -Return signer and sender, using default values on client if not specified - -Will use provided values if given, otherwise will fall back to values defined on client. -If no sender is specified then will attempt to obtain sender from signer - -#### import_source_map(source_map_json: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [None](https://docs.python.org/3/library/constants.html#None) - -Import approval source from JSON exported by `export_source_map` - -#### opt_in(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=OptIn - -#### prepare(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, app_id: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None) = None, template_values: [algokit_utils.deploy.TemplateValueDict](#algokit_utils.TemplateValueDict) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.application_client.ApplicationClient](#algokit_utils.ApplicationClient) - -Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. -Will also substitute provided template_values into the associated app_spec in the copy - -#### resolve(to_resolve: [algokit_utils.application_specification.DefaultArgumentDict](#algokit_utils.DefaultArgumentDict)) → [int](https://docs.python.org/3/library/functions.html#int) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) - -Resolves the default value for an ABI method, based on app_spec - -#### resolve_signer_sender(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None) = None, sender: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Return signer and sender, using default values on client if not specified - -Will use provided values if given, otherwise will fall back to values defined on client. -If no sender is specified then will attempt to obtain sender from signer - -* **Raises:** - [**ValueError**](https://docs.python.org/3/library/exceptions.html#ValueError) – Raised if a signer or sender is not provided. See `get_signer_sender` - for variant with no exception - -#### update(call_abi_method: algokit_utils.models.ABIMethod | [bool](https://docs.python.org/3/library/functions.html#bool) | [None](https://docs.python.org/3/library/constants.html#None) = None, transaction_parameters: [algokit_utils.models.TransactionParameters](#algokit_utils.TransactionParameters) | [algokit_utils.models.TransactionParametersDict](#algokit_utils.TransactionParametersDict) | [None](https://docs.python.org/3/library/constants.html#None) = None, \*\*abi_kwargs: algokit_utils.models.ABIArgType) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) | [algokit_utils.models.ABITransactionResponse](#algokit_utils.ABITransactionResponse) - -Submits a signed transaction with on_complete=UpdateApplication - -### *class* algokit_utils.ApplicationSpecification - -ARC-0032 application specification - -See [https://github.com/algorandfoundation/ARCs/pull/150](https://github.com/algorandfoundation/ARCs/pull/150) - -#### export(directory: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [None](https://docs.python.org/3/library/constants.html#None) - -write out the artifacts generated by the application to disk - -Args: -directory(optional): path to the directory where the artifacts should be written - -### *class* algokit_utils.CallConfig - -Bases: [`enum.IntFlag`](https://docs.python.org/3/library/enum.html#enum.IntFlag) - -Describes the type of calls a method can be used for based on [`algosdk.transaction.OnComplete`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.OnComplete) type - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -#### ALL - -3 - -Handle the specified on completion type for both create and normal application calls - -#### CALL - -1 - -Only handle the specified on completion type for application calls - -#### CREATE - -2 - -Only handle the specified on completion type for application create calls - -#### NEVER - -0 - -Never handle the specified on completion type - -### *class* algokit_utils.CreateCallParameters - -Bases: [`algokit_utils.models.OnCompleteCallParameters`](#algokit_utils.OnCompleteCallParameters) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.create/compose_create methods - -### *class* algokit_utils.CreateCallParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), [`algokit_utils.models.OnCompleteCallParametersDict`](#algokit_utils.OnCompleteCallParametersDict) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.create/compose_create methods - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.CreateTransactionParameters - -Bases: [`algokit_utils.models.TransactionParameters`](#algokit_utils.TransactionParameters) - -Additional parameters that can be included in a transaction when calling a create method - -### *class* algokit_utils.DefaultArgumentDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -DefaultArgument is a container for any arguments that may -be resolved prior to calling some target method - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployCallArgs - -Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.DeployCallArgsDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Parameters used to update or delete an application when calling -[`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployCreateCallArgs - -Bases: [`algokit_utils.deploy.DeployCallArgs`](#algokit_utils.DeployCallArgs) - -Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### *class* algokit_utils.DeployCreateCallArgsDict - -Bases: [`algokit_utils.deploy.DeployCallArgsDict`](#algokit_utils.DeployCallArgsDict), [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Parameters used to create an application when calling [`deploy()`](#algokit_utils.ApplicationClient.deploy) - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.DeployResponse - -Describes the action taken during deployment, related transactions and the [`AppMetaData`](#algokit_utils.AppMetaData) - -### *class* algokit_utils.EnsureBalanceParameters - -Parameters for ensuring an account has a minimum number of µALGOs - -#### account_to_fund *: [algokit_utils.models.Account](#algokit_utils.Account) | [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -The account address that will receive the µALGOs - -#### fee_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call - -#### funding_source *: [algokit_utils.models.Account](#algokit_utils.Account) | [algosdk.atomic_transaction_composer.AccountTransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AccountTransactionSigner) | [algokit_utils.dispenser_api.TestNetDispenserApiClient](#algokit_utils.TestNetDispenserApiClient) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -The account (with private key) or signer that will send the µALGOs, -will use `get_dispenser_account` by default. Alternatively you can pass an instance of [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) -which will allow you to interact with [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md). - -#### max_fee_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional)The maximum fee that you are happy to pay (default: unbounded) - -if this is set it’s possible the transaction could get rejected during network congestion - -#### min_funding_increment_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int)* - -0 - -When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets -called often on an active account) - -#### min_spending_balance_micro_algos *: [int](https://docs.python.org/3/library/functions.html#int)* - -None - -The minimum balance of ALGOs that the account should have available to spend (i.e. on top of -minimum balance requirement) - -#### note *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -The (optional) transaction note, default: “Funding account to meet minimum requirement - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -(optional) transaction parameters - -### *class* algokit_utils.EnsureFundedResponse - -Response for ensuring an account has a minimum number of µALGOs - -#### transaction_id *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -The amount of µALGOs that were funded - -### *class* algokit_utils.MethodHints - -MethodHints provides hints to the caller about how to call the method - -### *class* algokit_utils.OnCompleteCallParameters - -Bases: [`algokit_utils.models.TransactionParameters`](#algokit_utils.TransactionParameters) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.call/compose_call methods - -### *class* algokit_utils.OnCompleteCallParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict), [`algokit_utils.models.TransactionParametersDict`](#algokit_utils.TransactionParametersDict) - -Additional parameters that can be included in a transaction when using the -ApplicationClient.call/compose_call methods - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -### *class* algokit_utils.OnSchemaBreak(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Action to take if an Application’s schema has breaking changes - -### Initialization - -#### AppendApp - -3 - -Create a new Application - -#### Fail - -0 - -Fail the deployment - -#### ReplaceApp - -2 - -Create a new Application and delete the old Application in a single transaction - -### *class* algokit_utils.OnUpdate(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Action to take if an Application has been updated - -### Initialization - -#### AppendApp - -3 - -Create a new application - -#### Fail - -0 - -Fail the deployment - -#### ReplaceApp - -2 - -Create a new Application and delete the old Application in a single transaction - -#### UpdateApp - -1 - -Update the Application with the new approval and clear programs - -### *class* algokit_utils.OperationPerformed(\*args, \*\*kwds) - -Bases: [`enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) - -Describes the actions taken during deployment - -### Initialization - -#### Create - -1 - -No existing Application was found, created a new Application - -#### Nothing - -0 - -An existing Application was found - -#### Replace - -3 - -An existing Application was found, but was out of date, created a new Application and deleted the original - -#### Update - -2 - -An existing Application was found, but was out of date, updated to latest version - -### *class* algokit_utils.Program(program: [str](https://docs.python.org/3/library/stdtypes.html#str), client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) - -A compiled TEAL program - -### Initialization - -Fully compile the program source to binary and generate a -source map for matching pc to line number - -### *class* algokit_utils.TestNetDispenserApiClient(auth_token: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) = None, request_timeout: [int](https://docs.python.org/3/library/functions.html#int) = DISPENSER_REQUEST_TIMEOUT) - -Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). -To get started create a new access token via `algokit dispenser login --ci` -and pass it to the client constructor as `auth_token`. -Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, -and it will be auto loaded. If both are set, the constructor argument takes precedence. - -Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. - -### Initialization - -#### fund(address: [str](https://docs.python.org/3/library/stdtypes.html#str), amount: [int](https://docs.python.org/3/library/functions.html#int), asset_id: [int](https://docs.python.org/3/library/functions.html#int)) → algokit_utils.dispenser_api.DispenserFundResponse - -Fund an account with Algos from the dispenser API - -#### get_limit(address: [str](https://docs.python.org/3/library/stdtypes.html#str)) → algokit_utils.dispenser_api.DispenserLimitResponse - -Get current limit for an account with Algos from the dispenser API - -#### refund(refund_txn_id: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [None](https://docs.python.org/3/library/constants.html#None) - -Register a refund for a transaction with the dispenser API - -### *class* algokit_utils.TransactionParameters - -Additional parameters that can be included in a transaction - -#### accounts *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[str](https://docs.python.org/3/library/stdtypes.html#str)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Accounts to include in transaction - -#### boxes *: [collections.abc.Sequence](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [bytearray](https://docs.python.org/3/library/stdtypes.html#bytearray) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)]] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Box references to include in transaction. A sequence of (app id, box key) tuples - -#### foreign_apps *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -List of foreign apps (by app id) to include in transaction - -#### foreign_assets *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)] | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -List of foreign assets (by asset id) to include in transaction - -#### lease *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Lease value for this transaction - -#### note *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Note for this transaction - -#### rekey_to *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Address to rekey to - -#### sender *: [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Sender of this transaction - -#### signer *: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Signer to use when signing this transaction - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -SuggestedParams to use for this transaction - -### *class* algokit_utils.TransactionParametersDict - -Bases: [`typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) - -Additional parameters that can be included in a transaction - -### Initialization - -Initialize self. See help(type(self)) for accurate signature. - -#### accounts *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[str](https://docs.python.org/3/library/stdtypes.html#str)]* - -None - -Accounts to include in transaction - -#### boxes *: [collections.abc.Sequence](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)[[tuple](https://docs.python.org/3/library/stdtypes.html#tuple)[[int](https://docs.python.org/3/library/functions.html#int), [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [bytearray](https://docs.python.org/3/library/stdtypes.html#bytearray) | [str](https://docs.python.org/3/library/stdtypes.html#str) | [int](https://docs.python.org/3/library/functions.html#int)]]* - -None - -Box references to include in transaction. A sequence of (app id, box key) tuples - -#### foreign_apps *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]* - -None - -List of foreign apps (by app id) to include in transaction - -#### foreign_assets *: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]* - -None - -List of foreign assets (by asset id) to include in transaction - -#### lease *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Lease value for this transaction - -#### note *: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Note for this transaction - -#### rekey_to *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Address to rekey to - -#### sender *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Sender of this transaction - -#### signer *: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner)* - -None - -Signer to use when signing this transaction - -#### suggested_params *: [algosdk.transaction.SuggestedParams](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.SuggestedParams)* - -None - -SuggestedParams to use for this transaction - -### *class* algokit_utils.TransactionResponse - -Response for a non ABI call - -#### confirmed_round *: [int](https://docs.python.org/3/library/functions.html#int) | [None](https://docs.python.org/3/library/constants.html#None)* - -None - -Round transaction was confirmed, `None` if call was a from a dry-run - -#### *static* from_atr(result: [algosdk.atomic_transaction_composer.AtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionResponse) | [algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse), transaction_index: [int](https://docs.python.org/3/library/functions.html#int) = -1) → [algokit_utils.models.TransactionResponse](#algokit_utils.TransactionResponse) - -Returns either an ABITransactionResponse or a TransactionResponse based on the type of the transaction -referred to by transaction_index - -* **Parameters:** - * **result** (*AtomicTransactionResponse*) – Result containing one or more transactions - * **transaction_index** ([*int*](https://docs.python.org/3/library/functions.html#int)) – Which transaction in the result to return, defaults to -1 (the last transaction) - -#### tx_id *: [str](https://docs.python.org/3/library/stdtypes.html#str)* - -None - -Transaction Id - -### *class* algokit_utils.TransferAssetParameters - -Bases: `algokit_utils._transfer.TransferParametersBase` - -Parameters for transferring assets between accounts - -Args: -asset_id (int): The asset id that will be transfered -amount (int): The amount to send -clawback_from (str | None): An address of a target account from which to perform a clawback operation. Please -note, in such cases senderAccount must be equal to clawback field on ASA metadata. - -### *class* algokit_utils.TransferParameters - -Bases: `algokit_utils._transfer.TransferParametersBase` - -Parameters for transferring µALGOs between accounts - -## Functions - -### algokit_utils.create_kmd_wallet_account(kmd_client: [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Creates a wallet with specified name - -### algokit_utils.ensure_funded(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._ensure_funded.EnsureBalanceParameters](#algokit_utils.EnsureBalanceParameters)) → [algokit_utils._ensure_funded.EnsureFundedResponse](#algokit_utils.EnsureFundedResponse) | [None](https://docs.python.org/3/library/constants.html#None) - -Funds a given account using a funding source such that it has a certain amount of algos free to spend -(accounting for ALGOs locked in minimum balance requirement) -see [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) - -Args: -client (AlgodClient): An instance of the AlgodClient class from the AlgoSDK library. -parameters (EnsureBalanceParameters): An instance of the EnsureBalanceParameters class that -specifies the account to fund and the minimum spending balance. - -Returns: -PaymentTxn | str | None: If funds are needed, the function returns a payment transaction or a -string indicating that the dispenser API was used. If no funds are needed, the function returns None. - -### algokit_utils.execute_atc_with_logic_error(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), approval_program: [str](https://docs.python.org/3/library/stdtypes.html#str), wait_rounds: [int](https://docs.python.org/3/library/functions.html#int) = 4, approval_source_map: [algosdk.source_map.SourceMap](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/source_map.html#algosdk.source_map.SourceMap) | [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [algosdk.source_map.SourceMap](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/source_map.html#algosdk.source_map.SourceMap) | [None](https://docs.python.org/3/library/constants.html#None)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.atomic_transaction_composer.AtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionResponse) - -Calls `AtomicTransactionComposer.execute()` on provided `atc`, but will parse any errors -and raise a `LogicError` if possible - -#### NOTE -`approval_program` and `approval_source_map` are required to be able to parse any errors into a -`LogicError` - -### algokit_utils.get_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), fund_with_algos: [float](https://docs.python.org/3/library/functions.html#float) = 1000, kmd_client: KMDClient | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns an Algorand account with private key loaded by convention based on the given name identifier. - -### Convention - -**Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret -Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a -secret storage service rather than the file system. - -**LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn’t exist it will -create it and fund the account for you - -This allows you to write code that will work seamlessly in production and local development (LocalNet) without -manual config locally (including when you reset the LocalNet). - -### Example - -If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get -that private key loaded into an account object: - -```python -account = get_account('ACCOUNT', algod) -``` - -If that code runs against LocalNet then a wallet called ‘ACCOUNT’ will automatically be created with an account -that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. - -### algokit_utils.get_account_from_mnemonic(mnemonic: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Convert a mnemonic (25 word passphrase) into an Account - -### algokit_utils.get_algod_client(config: [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) - -Returns an [`algosdk.v2client.algod.AlgodClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient) from `config` or environment - -If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN` - -### algokit_utils.get_app_id_from_tx_id(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), tx_id: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [int](https://docs.python.org/3/library/functions.html#int) - -Finds the app_id for provided transaction id - -### algokit_utils.get_creator_apps(indexer: [algosdk.v2client.indexer.IndexerClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient), creator_account: [algokit_utils.models.Account](#algokit_utils.Account) | [str](https://docs.python.org/3/library/stdtypes.html#str)) → [algokit_utils.deploy.AppLookup](#algokit_utils.AppLookup) - -Returns a mapping of Application names to [`AppMetaData`](#algokit_utils.AppMetaData) for all Applications created by specified -creator that have a transaction note containing [`AppDeployMetaData`](#algokit_utils.AppDeployMetaData) - -### algokit_utils.get_default_localnet_config(config: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)[algod, indexer, kmd]) → [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) - -Returns the client configuration to point to the default LocalNet - -### algokit_utils.get_dispenser_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet - -### algokit_utils.get_indexer_client(config: [algokit_utils.network_clients.AlgoClientConfig](#algokit_utils.AlgoClientConfig) | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algosdk.v2client.indexer.IndexerClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) - -Returns an [`algosdk.v2client.indexer.IndexerClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/indexer.html#algosdk.v2client.indexer.IndexerClient) from `config` or environment. - -If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN` - -### algokit_utils.get_kmd_client_from_algod_client(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient) - -Returns an [`algosdk.kmd.KMDClient`](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient) from supplied `client` - -Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, -or 4002 by default - -### algokit_utils.get_kmd_wallet_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), kmd_client: [algosdk.kmd.KMDClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/kmd.html#algosdk.kmd.KMDClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), predicate: Callable[[[dict](https://docs.python.org/3/library/stdtypes.html#dict)[[str](https://docs.python.org/3/library/stdtypes.html#str), Any]], [bool](https://docs.python.org/3/library/functions.html#bool)] | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) | [None](https://docs.python.org/3/library/constants.html#None) - -Returns wallet matching specified name and predicate or None if not found - -### algokit_utils.get_localnet_default_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns the default Account in a LocalNet instance - -### algokit_utils.get_next_version(current_version: [str](https://docs.python.org/3/library/stdtypes.html#str)) → [str](https://docs.python.org/3/library/stdtypes.html#str) - -Calculates the next version from `current_version` - -Next version is calculated by finding a semver like -version string and incrementing the lower. This function is used by [`ApplicationClient.deploy()`](#algokit_utils.ApplicationClient.deploy) when -a version is not specified, and is intended mostly for convenience during local development. - -* **Params str current_version:** - An existing version string with a semver like version contained within it, - some valid inputs and incremented outputs: - `1` -> `2` - `1.0` -> `1.1` - `v1.1` -> `v1.2` - `v1.1-beta1` -> `v1.2-beta1` - `v1.2.3.4567` -> `v1.2.3.4568` - `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` -* **Raises:** - **DeploymentFailedError** – If `current_version` cannot be parsed - -### algokit_utils.get_or_create_kmd_wallet_account(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), name: [str](https://docs.python.org/3/library/stdtypes.html#str), fund_with_algos: [float](https://docs.python.org/3/library/functions.html#float) = 1000, kmd_client: KMDClient | [None](https://docs.python.org/3/library/constants.html#None) = None) → [algokit_utils.models.Account](#algokit_utils.Account) - -Returns a wallet with specified name, or creates one if not found - -### algokit_utils.get_sender_from_signer(signer: [algosdk.atomic_transaction_composer.TransactionSigner](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.TransactionSigner) | [None](https://docs.python.org/3/library/constants.html#None)) → [str](https://docs.python.org/3/library/stdtypes.html#str) | [None](https://docs.python.org/3/library/constants.html#None) - -Returns the associated address of a signer, return None if no address found - -### algokit_utils.is_localnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `devnet-v1` or `sandnet-v1` - -### algokit_utils.is_mainnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `mainnet-v1` - -### algokit_utils.is_testnet(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient)) → [bool](https://docs.python.org/3/library/functions.html#bool) - -Returns True if client genesis is `testnet-v1` - -### algokit_utils.num_extra_program_pages(approval: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes), clear: [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)) → [int](https://docs.python.org/3/library/functions.html#int) - -Calculate minimum number of extra_pages required for provided approval and clear programs - -### algokit_utils.opt_in(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), account: [algokit_utils.models.Account](#algokit_utils.Account), asset_ids: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[int](https://docs.python.org/3/library/functions.html#int), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, -it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases -its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). - -Args: -algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. -account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. -asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. -Returns: -dict[int, str]: A dictionary where the keys are the asset IDs and the values -are the transaction IDs for opting-in to each asset. - -### algokit_utils.opt_out(algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), account: [algokit_utils.models.Account](#algokit_utils.Account), asset_ids: [list](https://docs.python.org/3/library/stdtypes.html#list)[[int](https://docs.python.org/3/library/functions.html#int)]) → [dict](https://docs.python.org/3/library/stdtypes.html#dict)[[int](https://docs.python.org/3/library/functions.html#int), [str](https://docs.python.org/3/library/stdtypes.html#str)] - -Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. -The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) -The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. - -It’s essential to note that an account can only opt_out of an asset if its balance of that asset is zero. - -Args: -algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. -account (Account): An instance of the Account class that holds the private key and address for an account. -asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. -Returns: -dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of -the executed transactions. - -### algokit_utils.persist_sourcemaps(\*, sources: [list](https://docs.python.org/3/library/stdtypes.html#list)[algokit_utils._debugging.PersistSourceMapInput], project_root: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), with_sources: [bool](https://docs.python.org/3/library/functions.html#bool) = True, persist_mappings: [bool](https://docs.python.org/3/library/functions.html#bool) = False) → [None](https://docs.python.org/3/library/constants.html#None) - -Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts. -Args: -sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects. -project_root (Path): The root directory of the project. -client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. -with_sources (bool): If True, it will dump teal source files along with sourcemaps. -Default is True, as needed by an AlgoKit AVM debugger. -persist_mappings (bool): Enables legacy behavior of persisting the `sources.avm.json` mappings to -the project root. Default is False, given that the AlgoKit AVM VSCode extension will manage the mappings. - -### algokit_utils.replace_template_variables(program: [str](https://docs.python.org/3/library/stdtypes.html#str), template_values: [algokit_utils.deploy.TemplateValueMapping](#algokit_utils.TemplateValueMapping)) → [str](https://docs.python.org/3/library/stdtypes.html#str) - -Replaces `TMPL_*` variables in `program` with `template_values` - -#### NOTE -`template_values` keys should *NOT* be prefixed with `TMPL_` - -### algokit_utils.simulate_and_persist_response(atc: [algosdk.atomic_transaction_composer.AtomicTransactionComposer](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.AtomicTransactionComposer), project_root: [pathlib.Path](https://docs.python.org/3/library/pathlib.html#pathlib.Path), algod_client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), buffer_size_mb: [float](https://docs.python.org/3/library/functions.html#float) = 256) → [algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/atomic_transaction_composer.html#algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse) - -Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, -and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. - -* **Parameters:** - * **atc** – An `AtomicTransactionComposer` object representing the atomic transactions to be - simulated and persisted. - * **project_root** – A `Path` object representing the root directory of the project. - * **algod_client** – An `AlgodClient` object representing the Algorand client. - * **buffer_size_mb** – The size of the trace buffer in megabytes. Defaults to 256mb. -* **Returns:** - None - -Returns: -SimulateAtomicTransactionResponse: The simulated response after persisting it -for AlgoKit AVM Debugger consumption. - -### algokit_utils.transfer(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._transfer.TransferParameters](#algokit_utils.TransferParameters)) → [algosdk.transaction.PaymentTxn](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.PaymentTxn) - -Transfer µALGOs between accounts - -### algokit_utils.transfer_asset(client: [algosdk.v2client.algod.AlgodClient](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/v2client/algod.html#algosdk.v2client.algod.AlgodClient), parameters: [algokit_utils._transfer.TransferAssetParameters](#algokit_utils.TransferAssetParameters)) → [algosdk.transaction.AssetTransferTxn](https://py-algorand-sdk.readthedocs.io/en/latest/algosdk/transaction.html#algosdk.transaction.AssetTransferTxn) - -Transfer assets between accounts diff --git a/docs/markdown/autoapi/account_manager/index.md b/docs/markdown/autoapi/account_manager/index.md new file mode 100644 index 00000000..afa7273e --- /dev/null +++ b/docs/markdown/autoapi/account_manager/index.md @@ -0,0 +1 @@ +# account_manager diff --git a/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md new file mode 100644 index 00000000..f3707775 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md @@ -0,0 +1,603 @@ +# algokit_utils.accounts.account_manager + +## Classes + +| [`EnsureFundedResult`](#algokit_utils.accounts.account_manager.EnsureFundedResult) | Result from performing an ensure funded call. | +|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [`EnsureFundedFromTestnetDispenserApiResult`](#algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult) | Result from performing an ensure funded call using TestNet dispenser API. | +| [`AccountInformation`](#algokit_utils.accounts.account_manager.AccountInformation) | Information about an Algorand account's current status, balance and other properties. | +| [`AccountManager`](#algokit_utils.accounts.account_manager.AccountManager) | Creates and keeps track of signing accounts that can sign transactions for a sending address. | + +## Module Contents + +### *class* algokit_utils.accounts.account_manager.EnsureFundedResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendSingleTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `_CommonEnsureFundedParams` + +Result from performing an ensure funded call. + +### *class* algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult + +Bases: `_CommonEnsureFundedParams` + +Result from performing an ensure funded call using TestNet dispenser API. + +### *class* algokit_utils.accounts.account_manager.AccountInformation + +Information about an Algorand account’s current status, balance and other properties. + +See https://developer.algorand.org/docs/rest-apis/algod/#account for detailed field descriptions. + +* **Variables:** + * **address** (*str*) – The account’s address + * **amount** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s current balance + * **amount_without_pending_rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s balance without the pending rewards + * **min_balance** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The account’s minimum required balance + * **pending_rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The amount of pending rewards + * **rewards** ([*AlgoAmount*](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)) – The amount of rewards earned + * **round** (*int*) – The round for which this information is relevant + * **status** (*str*) – The account’s status (e.g., ‘Offline’, ‘Online’) + * **total_apps_opted_in** (*int* *|**None*) – Number of applications this account has opted into + * **total_assets_opted_in** (*int* *|**None*) – Number of assets this account has opted into + * **total_box_bytes** (*int* *|**None*) – Total number of box bytes used by this account + * **total_boxes** (*int* *|**None*) – Total number of boxes used by this account + * **total_created_apps** (*int* *|**None*) – Number of applications created by this account + * **total_created_assets** (*int* *|**None*) – Number of assets created by this account + * **apps_local_state** (*list* *[**dict* *]* *|**None*) – Local state of applications this account has opted into + * **apps_total_extra_pages** (*int* *|**None*) – Number of extra pages allocated to applications + * **apps_total_schema** (*dict* *|**None*) – Total schema for all applications + * **assets** (*list* *[**dict* *]* *|**None*) – Assets held by this account + * **auth_addr** (*str* *|**None*) – If rekeyed, the authorized address + * **closed_at_round** (*int* *|**None*) – Round when this account was closed + * **created_apps** (*list* *[**dict* *]* *|**None*) – Applications created by this account + * **created_assets** (*list* *[**dict* *]* *|**None*) – Assets created by this account + * **created_at_round** (*int* *|**None*) – Round when this account was created + * **deleted** (*bool* *|**None*) – Whether this account is deleted + * **incentive_eligible** (*bool* *|**None*) – Whether this account is eligible for incentives + * **last_heartbeat** (*int* *|**None*) – Last heartbeat round for this account + * **last_proposed** (*int* *|**None*) – Last round this account proposed a block + * **participation** (*dict* *|**None*) – Participation information for this account + * **reward_base** (*int* *|**None*) – Base reward for this account + * **sig_type** (*str* *|**None*) – Signature type for this account + +#### address *: str* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### amount_without_pending_rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### min_balance *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### pending_rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### rewards *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### round *: int* + +#### status *: str* + +#### total_apps_opted_in *: int | None* *= None* + +#### total_assets_opted_in *: int | None* *= None* + +#### total_box_bytes *: int | None* *= None* + +#### total_boxes *: int | None* *= None* + +#### total_created_apps *: int | None* *= None* + +#### total_created_assets *: int | None* *= None* + +#### apps_local_state *: list[dict] | None* *= None* + +#### apps_total_extra_pages *: int | None* *= None* + +#### apps_total_schema *: dict | None* *= None* + +#### assets *: list[dict] | None* *= None* + +#### auth_addr *: str | None* *= None* + +#### closed_at_round *: int | None* *= None* + +#### created_apps *: list[dict] | None* *= None* + +#### created_assets *: list[dict] | None* *= None* + +#### created_at_round *: int | None* *= None* + +#### deleted *: bool | None* *= None* + +#### incentive_eligible *: bool | None* *= None* + +#### last_heartbeat *: int | None* *= None* + +#### last_proposed *: int | None* *= None* + +#### participation *: dict | None* *= None* + +#### reward_base *: int | None* *= None* + +#### sig_type *: str | None* *= None* + +### *class* algokit_utils.accounts.account_manager.AccountManager(client_manager: [algokit_utils.clients.client_manager.ClientManager](../../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)) + +Creates and keeps track of signing accounts that can sign transactions for a sending address. + +This class provides functionality to create, track, and manage various types of accounts including +mnemonic-based, rekeyed, multisig, and logic signature accounts. + +* **Parameters:** + **client_manager** – The ClientManager client to use for algod and kmd clients +* **Example:** + +```pycon +>>> account_manager = AccountManager(client_manager) +``` + +#### *property* kmd *: [algokit_utils.accounts.kmd_account_manager.KmdAccountManager](../kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager)* + +#### set_default_signer(signer: algosdk.atomic_transaction_composer.TransactionSigner | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +If this isn’t set and a transaction needs signing for a given sender +then an error will be thrown from get_signer / get_account. + +* **Parameters:** + **signer** – A TransactionSigner signer to use. +* **Returns:** + The AccountManager so method calls can be chained +* **Example:** + +```pycon +>>> signer_account = account_manager.random() +>>> account_manager.set_default_signer(signer_account.signer) +>>> # When signing a transaction, if there is no signer registered for the sender +>>> # then the default signer will be used +>>> signer = account_manager.get_signer("{SENDERADDRESS}") +``` + +#### set_signer(sender: str, signer: algosdk.atomic_transaction_composer.TransactionSigner) → typing_extensions.Self + +Tracks the given TransactionSigner against the given sender address for later signing. + +* **Parameters:** + * **sender** – The sender address to use this signer for + * **signer** – The TransactionSigner to sign transactions with for the given sender +* **Returns:** + The AccountManager instance for method chaining +* **Example:** + +```pycon +>>> account_manager.set_signer("SENDERADDRESS", transaction_signer) +``` + +#### set_signers(\*, another_account_manager: [AccountManager](#algokit_utils.accounts.account_manager.AccountManager), overwrite_existing: bool = True) → typing_extensions.Self + +Merges the given AccountManager into this one. + +* **Parameters:** + * **another_account_manager** – The AccountManager to merge into this one + * **overwrite_existing** – Whether to overwrite existing signers in this manager +* **Returns:** + The AccountManager instance for method chaining + +#### set_signer_from_account(account: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Tracks the given account for later signing. + +Note: If you are generating accounts via the various methods on AccountManager +(like random, from_mnemonic, logic_sig, etc.) then they automatically get tracked. + +* **Parameters:** + **account** – The account to register +* **Returns:** + The AccountManager instance for method chaining +* **Example:** + +```pycon +>>> account_manager = AccountManager(client_manager) +>>> account_manager.set_signer_from_account(SigningAccount.new_account()) +>>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) +>>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) +``` + +#### get_signer(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → algosdk.atomic_transaction_composer.TransactionSigner + +Returns the TransactionSigner for the given sender address. + +If no signer has been registered for that address then the default signer is used if registered. + +* **Parameters:** + **sender** – The sender address or account +* **Returns:** + The TransactionSigner +* **Raises:** + **ValueError** – If no signer is found and no default signer is set +* **Example:** + +```pycon +>>> signer = account_manager.get_signer("SENDERADDRESS") +``` + +#### get_account(sender: str) → [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol) + +Returns the TransactionSignerAccountProtocol for the given sender address. + +* **Parameters:** + **sender** – The sender address +* **Returns:** + The TransactionSignerAccountProtocol +* **Raises:** + **ValueError** – If no account is found or if the account is not a regular account +* **Example:** + +```pycon +>>> sender = account_manager.random() +>>> # ... +>>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered +>>> account = account_manager.get_account(sender) +``` + +#### get_information(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [AccountInformation](#algokit_utils.accounts.account_manager.AccountInformation) + +Returns the given sender account’s current status, balance and spendable amounts. + +See [https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress](https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress) +for response data schema details. + +* **Parameters:** + **sender** – The address or account compliant with TransactionSignerAccountProtocol protocol to look up +* **Returns:** + The account information +* **Example:** + +```pycon +>>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +>>> account_info = account_manager.get_information(address) +``` + +#### from_mnemonic(\*, mnemonic: str, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. + +* **Parameters:** + * **mnemonic** – The mnemonic secret representing the private key of an account + * **sender** – Optional address to use as the sender +* **Returns:** + The account + +#### WARNING +Be careful how the mnemonic is handled. Never commit it into source control and ideally load it +from the environment (ideally via a secret storage service) rather than the file system. + +* **Example:** + +```pycon +>>> account = account_manager.from_mnemonic("mnemonic secret ...") +``` + +#### from_environment(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with private key loaded by convention from environment variables. + +This allows you to write code that will work seamlessly in production and local development (LocalNet) +without manual config locally (including when you reset the LocalNet). + +* **Parameters:** + * **name** – The name identifier of the account + * **fund_with** – Optional amount to fund the account with when it gets created + +(when targeting LocalNet) +:returns: The account +:raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} + +#### NOTE +Convention: +: * **Non-LocalNet:** will load {NAME}_MNEMONIC as a mnemonic secret. + If {NAME}_SENDER is defined then it will use that for the sender address + (i.e. to support rekeyed accounts) + * **LocalNet:** will load the account from a KMD wallet called {NAME} and if that wallet doesn’t exist + it will create it and fund the account for you + +* **Example:** + +```pycon +>>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: +>>> account = account_manager.from_environment('MY_ACCOUNT') +>>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created +>>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser +``` + +#### from_kmd(name: str, predicate: collections.abc.Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account with private key loaded from the given KMD wallet. + +* **Parameters:** + * **name** – The name of the wallet to retrieve an account from + * **predicate** – Optional filter to use to find the account + * **sender** – Optional sender address to use this signer for (aka a rekeyed account) +* **Returns:** + The account +* **Raises:** + **ValueError** – If unable to find KMD account with given name and predicate +* **Example:** + +```pycon +>>> # Get default funded account in a LocalNet: +>>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', +... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 +... ) +``` + +#### logicsig(program: bytes, args: list[bytes] | None = None) → algokit_utils.models.account.LogicSigAccount + +Tracks and returns an account that represents a logic signature. + +* **Parameters:** + * **program** – The bytes that make up the compiled logic signature + * **args** – Optional (binary) arguments to pass into the logic signature +* **Returns:** + A logic signature account wrapper +* **Example:** + +```pycon +>>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) +``` + +#### multisig(metadata: [algokit_utils.models.account.MultisigMetadata](../../models/account/index.md#algokit_utils.models.account.MultisigMetadata), signing_accounts: list[[algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount)]) → [algokit_utils.models.account.MultiSigAccount](../../models/account/index.md#algokit_utils.models.account.MultiSigAccount) + +Tracks and returns an account that supports partial or full multisig signing. + +* **Parameters:** + * **metadata** – The metadata for the multisig account + * **signing_accounts** – The signers that are currently present +* **Returns:** + A multisig account wrapper +* **Example:** + +```pycon +>>> account = account_manager.multi_sig( +... version=1, +... threshold=1, +... addrs=["ADDRESS1...", "ADDRESS2..."], +... signing_accounts=[account1, account2] +... ) +``` + +#### random() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns a new, random Algorand account. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.random() +``` + +#### localnet_dispenser() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + +This account can be used to fund other accounts. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.localnet_dispenser() +``` + +#### dispenser_from_environment() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Returns an account (with private key loaded) that can act as a dispenser from environment variables. + +If environment variables are not present, returns the default LocalNet dispenser account. + +* **Returns:** + The account +* **Example:** + +```pycon +>>> account = account_manager.dispenser_from_environment() +``` + +#### rekeyed(\*, sender: str, account: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [algokit_utils.models.account.TransactionSignerAccount](../../models/account/index.md#algokit_utils.models.account.TransactionSignerAccount) | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. + +* **Parameters:** + * **sender** – The account or address to use as the sender + * **account** – The account to use as the signer for this new rekeyed account +* **Returns:** + The rekeyed account +* **Example:** + +```pycon +>>> account = account.from_mnemonic("mnemonic secret ...") +>>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") +``` + +#### rekey_account(account: str, rekey_to: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol), \*, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, suppress_log: bool | None = None) → [algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Rekey an account to a new address. + +* **Parameters:** + * **account** – The account to rekey + * **rekey_to** – The address or account to rekey to + * **signer** – Optional transaction signer + * **note** – Optional transaction note + * **lease** – Optional transaction lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional max fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **suppress_log** – Optional flag to suppress logging +* **Returns:** + The result of the transaction and the transaction that was sent + +#### WARNING +Please be careful with this function and be sure to read the +[official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +* **Example:** + +```pycon +>>> # Basic example (with string addresses): +>>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) +>>> # Basic example (with signer accounts): +>>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) +>>> # Advanced example: +>>> algorand.account.rekey_account({ +... account: "ACCOUNTADDRESS", +... rekey_to: "NEWADDRESS", +... lease: 'lease', +... note: 'note', +... first_valid_round: 1000, +... validity_window: 10, +... extra_fee: AlgoAmount.from_micro_algo(1000), +... static_fee: AlgoAmount.from_micro_algo(1000), +... max_fee: AlgoAmount.from_micro_algo(3000), +... suppress_log: True, +... }) +``` + +#### ensure_funded(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_account: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None + +Funds a given account using a dispenser account as a funding source. + +Ensures the given account has a certain amount of Algo free to spend (accounting for +Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **dispenser_account** – The account to use as a dispenser funding source + * **min_spending_balance** – The minimum balance of Algo that the account + +should have available to spend +:param min_funding_increment: Optional minimum funding increment +:param send_params: Parameters for the send operation, defaults to None +:param signer: Optional transaction signer +:param rekey_to: Optional rekey address +:param note: Optional transaction note +:param lease: Optional transaction lease +:param static_fee: Optional static fee +:param extra_fee: Optional extra fee +:param max_fee: Optional maximum fee +:param validity_window: Optional validity window +:param first_valid_round: Optional first valid round +:param last_valid_round: Optional last valid round +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, +or None if no funds were needed + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) +>>> # With configuration: +>>> algorand.account.ensure_funded( +... "ACCOUNTADDRESS", +... "DISPENSERADDRESS", +... algokit.algo(1), +... min_funding_increment=algokit.algo(2), +... fee=AlgoAmount.from_micro_algo(1000), +... suppress_log=True +... ) +``` + +#### ensure_funded_from_environment(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None + +Ensure an account is funded from a dispenser account configured in environment. + +Uses a dispenser account retrieved from the environment, per the dispenser_from_environment method, +as a funding source such that the given account has a certain amount of Algo free to spend +(accounting for Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **min_spending_balance** – The minimum balance of Algo that the account should have available to + +spend +:param min_funding_increment: Optional minimum funding increment +:param send_params: Parameters for the send operation, defaults to None +:param signer: Optional transaction signer +:param rekey_to: Optional rekey address +:param note: Optional transaction note +:param lease: Optional transaction lease +:param static_fee: Optional static fee +:param extra_fee: Optional extra fee +:param max_fee: Optional maximum fee +:param validity_window: Optional validity window +:param first_valid_round: Optional first valid round +:param last_valid_round: Optional last valid round +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or +None if no funds were needed + +#### NOTE +The dispenser account is retrieved from the account mnemonic stored in +process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER +if it’s a rekeyed account, or against default LocalNet if no environment variables present. + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) +>>> # With configuration: +>>> algorand.account.ensure_funded_from_environment( +... "ACCOUNTADDRESS", +... algokit.algo(1), +... min_funding_increment=algokit.algo(2), +... fee=AlgoAmount.from_micro_algo(1000), +... suppress_log=True +... ) +``` + +#### ensure_funded_from_testnet_dispenser_api(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_client: [algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient](../../clients/dispenser_api_client/index.md#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [EnsureFundedFromTestnetDispenserApiResult](#algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult) | None + +Ensure an account is funded using the TestNet Dispenser API. + +Uses the TestNet Dispenser API as a funding source such that the account has a certain amount +of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance) for details. + +* **Parameters:** + * **account_to_fund** – The account to fund + * **dispenser_client** – The TestNet dispenser funding client + * **min_spending_balance** – The minimum balance of Algo that the account should have + +available to spend +:param min_funding_increment: Optional minimum funding increment +:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or +None if no funds were needed +:raises ValueError: If attempting to fund on non-TestNet network + +* **Example:** + +```pycon +>>> # Basic example: +>>> algorand.account.ensure_funded_from_testnet_dispenser_api( +... "ACCOUNTADDRESS", +... algorand.client.get_testnet_dispenser_from_environment(), +... algokit.algo(1) +... ) +>>> # With configuration: +>>> algorand.account.ensure_funded_from_testnet_dispenser_api( +... "ACCOUNTADDRESS", +... algorand.client.get_testnet_dispenser_from_environment(), +... algokit.algo(1), +... min_funding_increment=algokit.algo(2) +... ) +``` diff --git a/docs/markdown/autoapi/algokit_utils/accounts/index.md b/docs/markdown/autoapi/algokit_utils/accounts/index.md new file mode 100644 index 00000000..97b69c7e --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/index.md @@ -0,0 +1,6 @@ +# algokit_utils.accounts + +## Submodules + +* [algokit_utils.accounts.account_manager](account_manager/index.md) +* [algokit_utils.accounts.kmd_account_manager](kmd_account_manager/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md new file mode 100644 index 00000000..5041cc9b --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/accounts/kmd_account_manager/index.md @@ -0,0 +1,71 @@ +# algokit_utils.accounts.kmd_account_manager + +## Classes + +| [`KmdAccount`](#algokit_utils.accounts.kmd_account_manager.KmdAccount) | Account retrieved from KMD with signing capabilities, extending base Account. | +|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [`KmdAccountManager`](#algokit_utils.accounts.kmd_account_manager.KmdAccountManager) | Provides abstractions over KMD that makes it easier to get and manage accounts. | + +## Module Contents + +### *class* algokit_utils.accounts.kmd_account_manager.KmdAccount(private_key: str, address: str | None = None) + +Bases: [`algokit_utils.models.account.SigningAccount`](../../models/account/index.md#algokit_utils.models.account.SigningAccount) + +Account retrieved from KMD with signing capabilities, extending base Account. + +Provides an account implementation that can be used to sign transactions using keys stored in KMD. + +* **Parameters:** + * **private_key** – Base64 encoded private key + * **address** – Optional address override for rekeyed accounts, defaults to None + +### *class* algokit_utils.accounts.kmd_account_manager.KmdAccountManager(client_manager: [algokit_utils.clients.client_manager.ClientManager](../../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)) + +Provides abstractions over KMD that makes it easier to get and manage accounts. + +#### kmd() → algosdk.kmd.KMDClient + +Returns the KMD client, initializing it if needed. + +* **Raises:** + **Exception** – If KMD client is not configured and not running against LocalNet +* **Returns:** + The KMD client + +#### get_wallet_account(wallet_name: str, predicate: collections.abc.Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) | None + +Returns an Algorand signing account with private key loaded from the given KMD wallet. + +Retrieves an account from a KMD wallet that matches the given predicate, or a random account +if no predicate is provided. + +* **Parameters:** + * **wallet_name** – The name of the wallet to retrieve an account from + * **predicate** – Optional filter to use to find the account (otherwise gets a random account from the wallet) + * **sender** – Optional sender address to use this signer for (aka a rekeyed account) +* **Returns:** + The signing account or None if no matching wallet or account was found + +#### get_or_create_wallet_account(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) + +Gets or creates a funded account in a KMD wallet of the given name. + +Provides idempotent access to accounts from LocalNet without specifying the private key. + +* **Parameters:** + * **name** – The name of the wallet to retrieve / create + * **fund_with** – The number of Algos to fund the account with when created +* **Returns:** + An Algorand account with private key loaded + +#### get_localnet_dispenser_account() → [KmdAccount](#algokit_utils.accounts.kmd_account_manager.KmdAccount) + +Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + +Retrieves the default funded account from LocalNet that can be used to fund other accounts. + +* **Raises:** + **Exception** – If not running against LocalNet or dispenser account not found +* **Returns:** + The default LocalNet dispenser account diff --git a/docs/markdown/autoapi/algokit_utils/algorand/index.md b/docs/markdown/autoapi/algokit_utils/algorand/index.md new file mode 100644 index 00000000..3e0f3115 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/algorand/index.md @@ -0,0 +1,156 @@ +# algokit_utils.algorand + +## Classes + +| [`AlgorandClient`](#algokit_utils.algorand.AlgorandClient) | A client that brokers easy access to Algorand functionality. | +|--------------------------------------------------------------|----------------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.algorand.AlgorandClient(config: [algokit_utils.models.network.AlgoClientConfigs](../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) | [algokit_utils.clients.client_manager.AlgoSdkClients](../clients/client_manager/index.md#algokit_utils.clients.client_manager.AlgoSdkClients)) + +A client that brokers easy access to Algorand functionality. + +#### set_default_validity_window(validity_window: int) → typing_extensions.Self + +Sets the default validity window for transactions. + +* **Parameters:** + **validity_window** – The number of rounds between the first and last valid rounds +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_default_signer(signer: algosdk.atomic_transaction_composer.TransactionSigner | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +* **Parameters:** + **signer** – The signer to use, either a TransactionSigner or a TransactionSignerAccountProtocol +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_signer(sender: str, signer: algosdk.atomic_transaction_composer.TransactionSigner) → typing_extensions.Self + +Tracks the given account for later signing. + +* **Parameters:** + * **sender** – The sender address to use this signer for + * **signer** – The signer to sign transactions with for the given sender +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_signer_account(signer: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → typing_extensions.Self + +Sets the default signer to use if no other signer is specified. + +* **Parameters:** + **signer** – The signer to use, either a TransactionSigner or a TransactionSignerAccountProtocol +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_suggested_params(suggested_params: algosdk.transaction.SuggestedParams, until: float | None = None) → typing_extensions.Self + +Sets a cache value to use for suggested params. + +* **Parameters:** + * **suggested_params** – The suggested params to use + * **until** – A timestamp until which to cache, or if not specified then the timeout is used +* **Returns:** + The AlgorandClient so method calls can be chained + +#### set_suggested_params_timeout(timeout: int) → typing_extensions.Self + +Sets the timeout for caching suggested params. + +* **Parameters:** + **timeout** – The timeout in milliseconds +* **Returns:** + The AlgorandClient so method calls can be chained + +#### get_suggested_params() → algosdk.transaction.SuggestedParams + +Get suggested params for a transaction (either cached or from algod if the cache is stale or empty) + +#### new_group() → [algokit_utils.transactions.transaction_composer.TransactionComposer](../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Start a new TransactionComposer transaction group + +#### *property* client *: [algokit_utils.clients.client_manager.ClientManager](../clients/client_manager/index.md#algokit_utils.clients.client_manager.ClientManager)* + +Get clients, including algosdk clients and app clients. + +#### *property* account *: [algokit_utils.accounts.account_manager.AccountManager](../accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager)* + +Get or create accounts that can sign transactions. + +#### *property* asset *: [algokit_utils.assets.asset_manager.AssetManager](../assets/asset_manager/index.md#algokit_utils.assets.asset_manager.AssetManager)* + +Get or create assets. + +#### *property* app *: [algokit_utils.applications.app_manager.AppManager](../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager)* + +#### *property* app_deployer *: [algokit_utils.applications.app_deployer.AppDeployer](../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployer)* + +Get or create applications. + +#### *property* send *: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender)* + +Methods for sending a transaction and waiting for confirmation + +#### *property* create_transaction *: [algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator](../transactions/transaction_creator/index.md#algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator)* + +Methods for building transactions + +#### *static* default_localnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at default LocalNet ports and API token. + +* **Returns:** + The AlgorandClient + +#### *static* testnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at TestNet using AlgoNode. + +* **Returns:** + The AlgorandClient + +#### *static* mainnet() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing at MainNet using AlgoNode. + +* **Returns:** + The AlgorandClient + +#### *static* from_clients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient pointing to the given client(s). + +* **Parameters:** + * **algod** – The algod client to use + * **indexer** – The indexer client to use + * **kmd** – The kmd client to use +* **Returns:** + The AlgorandClient + +#### *static* from_environment() → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient loading the configuration from environment variables. + +Retrieve configurations from environment variables when defined or get defaults. + +Expects to be called from a Python environment. + +* **Returns:** + The AlgorandClient + +#### *static* from_config(algod_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig), indexer_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None, kmd_config: [algokit_utils.models.network.AlgoClientNetworkConfig](../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → [AlgorandClient](#algokit_utils.algorand.AlgorandClient) + +Returns an AlgorandClient from the given config. + +* **Parameters:** + * **algod_config** – The config to use for the algod client + * **indexer_config** – The config to use for the indexer client + * **kmd_config** – The config to use for the kmd client +* **Returns:** + The AlgorandClient diff --git a/docs/markdown/autoapi/algokit_utils/applications/abi/index.md b/docs/markdown/autoapi/algokit_utils/applications/abi/index.md new file mode 100644 index 00000000..94e80d66 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/abi/index.md @@ -0,0 +1,161 @@ +# algokit_utils.applications.abi + +## Attributes + +| [`ABIValue`](#algokit_utils.applications.abi.ABIValue) | | +|--------------------------------------------------------------------------------|----| +| [`ABIStruct`](#algokit_utils.applications.abi.ABIStruct) | | +| [`Arc56ReturnValueType`](#algokit_utils.applications.abi.Arc56ReturnValueType) | | +| [`ABIType`](#algokit_utils.applications.abi.ABIType) | | +| [`ABIArgumentType`](#algokit_utils.applications.abi.ABIArgumentType) | | + +## Classes + +| [`ABIReturn`](#algokit_utils.applications.abi.ABIReturn) | Represents the return value from an ABI method call. | +|--------------------------------------------------------------|--------------------------------------------------------| +| [`BoxABIValue`](#algokit_utils.applications.abi.BoxABIValue) | Represents an ABI value stored in a box. | + +## Functions + +| [`get_arc56_value`](#algokit_utils.applications.abi.get_arc56_value)(→ Arc56ReturnValueType) | Gets the ARC-56 formatted return value from an ABI return. | +|---------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| [`get_abi_encoded_value`](#algokit_utils.applications.abi.get_abi_encoded_value)(→ bytes) | Encodes a value according to its ABI type. | +| [`get_abi_decoded_value`](#algokit_utils.applications.abi.get_abi_decoded_value)(→ ABIValue) | Decodes a value according to its ABI type. | +| [`get_abi_tuple_from_abi_struct`](#algokit_utils.applications.abi.get_abi_tuple_from_abi_struct)(→ list[Any]) | Converts an ABI struct to a tuple representation. | +| [`get_abi_tuple_type_from_abi_struct_definition`](#algokit_utils.applications.abi.get_abi_tuple_type_from_abi_struct_definition)(...) | Creates a TupleType from a struct definition. | +| [`get_abi_struct_from_abi_tuple`](#algokit_utils.applications.abi.get_abi_struct_from_abi_tuple)(→ dict[str, Any]) | Converts a decoded tuple to an ABI struct. | + +## Module Contents + +### algokit_utils.applications.abi.ABIValue *: TypeAlias* *= bool | int | str | bytes | bytearray | list['ABIValue'] | tuple['ABIValue'] | dict[str, 'ABIValue']* + +### algokit_utils.applications.abi.ABIStruct *: TypeAlias* *= dict[str, list[dict[str, 'ABIValue']]]* + +### algokit_utils.applications.abi.Arc56ReturnValueType *: TypeAlias* *= ABIValue | ABIStruct | None* + +### algokit_utils.applications.abi.ABIType *: TypeAlias* *= algosdk.abi.ABIType* + +### algokit_utils.applications.abi.ABIArgumentType *: TypeAlias* *= algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType* + +### *class* algokit_utils.applications.abi.ABIReturn(result: algosdk.atomic_transaction_composer.ABIResult) + +Represents the return value from an ABI method call. + +Wraps the raw return value and decoded value along with any decode errors. + +* **Variables:** + * **result** – The ABIResult object containing the method call results + * **raw_value** – The raw return value from the method call + * **value** – The decoded return value from the method call + * **method** – The ABI method definition + * **decode_error** – The exception that occurred during decoding, if any + +#### raw_value *: bytes | None* *= None* + +#### value *: ABIValue | None* *= None* + +#### method *: algosdk.abi.method.Method | None* *= None* + +#### decode_error *: Exception | None* *= None* + +#### *property* is_success *: bool* + +Returns True if the ABI call was successful (no decode error) + +* **Returns:** + True if no decode error occurred, False otherwise + +#### get_arc56_value(method: [algokit_utils.applications.app_spec.arc56.Method](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Method) | algosdk.abi.method.Method, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → Arc56ReturnValueType + +Gets the ARC-56 formatted return value. + +* **Parameters:** + * **method** – The ABI method definition + * **structs** – Dictionary of struct definitions +* **Returns:** + The decoded return value in ARC-56 format + +### algokit_utils.applications.abi.get_arc56_value(abi_return: [ABIReturn](#algokit_utils.applications.abi.ABIReturn), method: [algokit_utils.applications.app_spec.arc56.Method](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Method) | algosdk.abi.method.Method, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → Arc56ReturnValueType + +Gets the ARC-56 formatted return value from an ABI return. + +* **Parameters:** + * **abi_return** – The ABI return value to decode + * **method** – The ABI method definition + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If there was an error decoding the return value +* **Returns:** + The decoded return value in ARC-56 format + +### algokit_utils.applications.abi.get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → bytes + +Encodes a value according to its ABI type. + +* **Parameters:** + * **value** – The value to encode + * **type_str** – The ABI type string + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If the value cannot be encoded for the given type +* **Returns:** + The ABI encoded bytes + +### algokit_utils.applications.abi.get_abi_decoded_value(value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → ABIValue + +Decodes a value according to its ABI type. + +* **Parameters:** + * **value** – The value to decode + * **type_str** – The ABI type string or type object + * **structs** – Dictionary of struct definitions +* **Returns:** + The decoded ABI value + +### algokit_utils.applications.abi.get_abi_tuple_from_abi_struct(struct_value: dict[str, Any], struct_fields: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → list[Any] + +Converts an ABI struct to a tuple representation. + +* **Parameters:** + * **struct_value** – The struct value as a dictionary + * **struct_fields** – List of struct field definitions + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If a required field is missing from the struct +* **Returns:** + The struct as a tuple + +### algokit_utils.applications.abi.get_abi_tuple_type_from_abi_struct_definition(struct_def: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → algosdk.abi.TupleType + +Creates a TupleType from a struct definition. + +* **Parameters:** + * **struct_def** – The struct field definitions + * **structs** – Dictionary of struct definitions +* **Raises:** + **ValueError** – If a field type is invalid +* **Returns:** + The TupleType representing the struct + +### algokit_utils.applications.abi.get_abi_struct_from_abi_tuple(decoded_tuple: Any, struct_fields: list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[algokit_utils.applications.app_spec.arc56.StructField](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.StructField)]]) → dict[str, Any] + +Converts a decoded tuple to an ABI struct. + +* **Parameters:** + * **decoded_tuple** – The tuple to convert + * **struct_fields** – List of struct field definitions + * **structs** – Dictionary of struct definitions +* **Returns:** + The tuple as a struct dictionary + +### *class* algokit_utils.applications.abi.BoxABIValue + +Represents an ABI value stored in a box. + +* **Variables:** + * **name** – The name of the box + * **value** – The ABI value stored in the box + +#### name *: [algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)* + +#### value *: ABIValue* diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md new file mode 100644 index 00000000..7a135152 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_client/index.md @@ -0,0 +1,578 @@ +# algokit_utils.applications.app_client + +## Attributes + +| [`CreateOnComplete`](#algokit_utils.applications.app_client.CreateOnComplete) | | +|---------------------------------------------------------------------------------|----| + +## Classes + +| [`AppClientCompilationResult`](#algokit_utils.applications.app_client.AppClientCompilationResult) | Result of compiling an application's TEAL code. | +|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| +| [`AppClientCompilationParams`](#algokit_utils.applications.app_client.AppClientCompilationParams) | Parameters for compiling an application's TEAL code. | +| [`FundAppAccountParams`](#algokit_utils.applications.app_client.FundAppAccountParams) | Parameters for funding an application's account. | +| [`AppClientCallParams`](#algokit_utils.applications.app_client.AppClientCallParams) | Parameters for calling an application. | +| [`BaseAppClientMethodCallParams`](#algokit_utils.applications.app_client.BaseAppClientMethodCallParams) | Base parameters for application method calls. | +| [`AppClientMethodCallParams`](#algokit_utils.applications.app_client.AppClientMethodCallParams) | Parameters for application method calls. | +| [`AppClientBareCallParams`](#algokit_utils.applications.app_client.AppClientBareCallParams) | Parameters for bare application calls. | +| [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema) | Schema for application creation. | +| [`AppClientBareCallCreateParams`](#algokit_utils.applications.app_client.AppClientBareCallCreateParams) | Parameters for creating application with bare call. | +| [`AppClientMethodCallCreateParams`](#algokit_utils.applications.app_client.AppClientMethodCallCreateParams) | Parameters for creating application with method call. | +| [`AppClientParams`](#algokit_utils.applications.app_client.AppClientParams) | Full parameters for creating an app client | +| [`AppClient`](#algokit_utils.applications.app_client.AppClient) | A client for interacting with an Algorand smart contract application. | + +## Functions + +| [`get_constant_block_offset`](#algokit_utils.applications.app_client.get_constant_block_offset)(→ int) | Calculate the offset after constant blocks in TEAL program. | +|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| + +## Module Contents + +### algokit_utils.applications.app_client.get_constant_block_offset(program: bytes) → int + +Calculate the offset after constant blocks in TEAL program. + +Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations. + +* **Parameters:** + **program** – The compiled TEAL program as bytes +* **Returns:** + The maximum offset position after any constant block operations + +### algokit_utils.applications.app_client.CreateOnComplete + +### *class* algokit_utils.applications.app_client.AppClientCompilationResult + +Result of compiling an application’s TEAL code. + +Contains the compiled approval and clear state programs along with optional compilation artifacts. + +* **Variables:** + * **approval_program** – The compiled approval program bytes + * **clear_state_program** – The compiled clear state program bytes + * **compiled_approval** – Optional compilation artifacts for approval program + * **compiled_clear** – Optional compilation artifacts for clear state program + +#### approval_program *: bytes* + +#### clear_state_program *: bytes* + +#### compiled_approval *: [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None* *= None* + +#### compiled_clear *: [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCompilationParams + +Bases: `TypedDict` + +Parameters for compiling an application’s TEAL code. + +* **Variables:** + * **deploy_time_params** – Optional template parameters to use during compilation + * **updatable** – Optional flag indicating if app should be updatable + * **deletable** – Optional flag indicating if app should be deletable + +#### deploy_time_params *: algokit_utils.models.state.TealTemplateParams | None* + +#### updatable *: bool | None* + +#### deletable *: bool | None* + +### *class* algokit_utils.applications.app_client.FundAppAccountParams + +Parameters for funding an application’s account. + +* **Variables:** + * **sender** – Optional sender address + * **signer** – Optional transaction signer + * **rekey_to** – Optional address to rekey to + * **note** – Optional transaction note + * **lease** – Optional lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window in rounds + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **amount** – Amount to fund + * **close_remainder_to** – Optional address to close remainder to + * **on_complete** – Optional on complete action + +#### sender *: str | None* *= None* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### note *: bytes | None* *= None* + +#### lease *: bytes | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### close_remainder_to *: str | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCallParams + +Parameters for calling an application. + +* **Variables:** + * **method** – Optional ABI method name or signature + * **args** – Optional arguments to pass to method + * **boxes** – Optional box references to load + * **accounts** – Optional account addresses to load + * **apps** – Optional app IDs to load + * **assets** – Optional asset IDs to load + * **lease** – Optional lease + * **sender** – Optional sender address + * **note** – Optional transaction note + * **send_params** – Optional parameters to control transaction sending + +#### method *: str | None* *= None* + +#### args *: list | None* *= None* + +#### boxes *: list | None* *= None* + +#### accounts *: list[str] | None* *= None* + +#### apps *: list[int] | None* *= None* + +#### assets *: list[int] | None* *= None* + +#### lease *: str | bytes | None* *= None* + +#### sender *: str | None* *= None* + +#### note *: bytes | dict | str | None* *= None* + +#### send_params *: dict | None* *= None* + +### *class* algokit_utils.applications.app_client.BaseAppClientMethodCallParams + +Bases: `Generic`[`ArgsT`, `MethodT`] + +Base parameters for application method calls. + +* **Variables:** + * **method** – Method to call + * **args** – Optional arguments to pass to method + * **account_references** – Optional account references + * **app_references** – Optional application references + * **asset_references** – Optional asset references + * **box_references** – Optional box references + * **extra_fee** – Optional extra fee + * **first_valid_round** – Optional first valid round + * **lease** – Optional lease + * **max_fee** – Optional maximum fee + * **note** – Optional note + * **rekey_to** – Optional rekey to address + * **sender** – Optional sender address + * **signer** – Optional transaction signer + * **static_fee** – Optional static fee + * **validity_window** – Optional validity window + * **last_valid_round** – Optional last valid round + * **on_complete** – Optional on complete action + +#### method *: MethodT* + +#### args *: ArgsT | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: collections.abc.Sequence[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### lease *: bytes | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### note *: bytes | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### sender *: str | None* *= None* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientMethodCallParams + +Bases: [`BaseAppClientMethodCallParams`](#algokit_utils.applications.app_client.BaseAppClientMethodCallParams)[`collections.abc.Sequence`[`algokit_utils.applications.abi.ABIValue | algokit_utils.applications.abi.ABIStruct | algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument | None`], `str`] + +Parameters for application method calls. + +### *class* algokit_utils.applications.app_client.AppClientBareCallParams + +Parameters for bare application calls. + +* **Variables:** + * **signer** – Optional transaction signer + * **rekey_to** – Optional rekey to address + * **lease** – Optional lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round + * **sender** – Optional sender address + * **note** – Optional note + * **args** – Optional arguments + * **account_references** – Optional account references + * **app_references** – Optional application references + * **asset_references** – Optional asset references + * **box_references** – Optional box references + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### rekey_to *: str | None* *= None* + +#### lease *: bytes | None* *= None* + +#### static_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### extra_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### max_fee *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None* *= None* + +#### validity_window *: int | None* *= None* + +#### first_valid_round *: int | None* *= None* + +#### last_valid_round *: int | None* *= None* + +#### sender *: str | None* *= None* + +#### note *: bytes | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientCreateSchema + +Schema for application creation. + +* **Variables:** + * **extra_program_pages** – Optional number of extra program pages + * **schema** – Optional application creation schema + +#### extra_program_pages *: int | None* *= None* + +#### schema *: [algokit_utils.transactions.transaction_composer.AppCreateSchema](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientBareCallCreateParams + +Bases: [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema), [`AppClientBareCallParams`](#algokit_utils.applications.app_client.AppClientBareCallParams) + +Parameters for creating application with bare call. + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientMethodCallCreateParams + +Bases: [`AppClientCreateSchema`](#algokit_utils.applications.app_client.AppClientCreateSchema), [`AppClientMethodCallParams`](#algokit_utils.applications.app_client.AppClientMethodCallParams) + +Parameters for creating application with method call. + +#### on_complete *: CreateOnComplete | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClientParams + +Full parameters for creating an app client + +#### app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str* + +#### algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### app_id *: int* + +#### app_name *: str | None* *= None* + +#### default_sender *: str | None* *= None* + +#### default_signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### approval_source_map *: algosdk.source_map.SourceMap | None* *= None* + +#### clear_source_map *: algosdk.source_map.SourceMap | None* *= None* + +### *class* algokit_utils.applications.app_client.AppClient(params: [AppClientParams](#algokit_utils.applications.app_client.AppClientParams)) + +A client for interacting with an Algorand smart contract application. + +Provides a high-level interface for interacting with Algorand smart contracts, including +methods for calling application methods, managing state, and handling transactions. + +* **Parameters:** + **params** – Parameters for creating the app client + +#### *property* algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +Get the Algorand client instance. + +* **Returns:** + The Algorand client used by this app client + +#### *property* app_id *: int* + +Get the application ID. + +* **Returns:** + The ID of the Algorand application + +#### *property* app_address *: str* + +Get the application’s Algorand address. + +* **Returns:** + The Algorand address associated with this application + +#### *property* app_name *: str* + +Get the application name. + +* **Returns:** + The name of the application + +#### *property* app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract)* + +Get the application specification. + +* **Returns:** + The ARC-56 contract specification for this application + +#### *property* state *: \_StateAccessor* + +Get the state accessor. + +* **Returns:** + The state accessor for this application + +#### *property* params *: \_MethodParamsBuilder* + +Get the method parameters builder. + +* **Returns:** + The method parameters builder for this application + +#### *property* send *: \_TransactionSender* + +Get the transaction sender. + +* **Returns:** + The transaction sender for this application + +#### *property* create_transaction *: \_TransactionCreator* + +Get the transaction creator. + +* **Returns:** + The transaction creator for this application + +#### *static* normalise_app_spec(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str) → [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +Normalize an application specification to ARC-56 format. + +* **Parameters:** + **app_spec** – The application specification to normalize +* **Returns:** + The normalized ARC-56 contract specification +* **Raises:** + **ValueError** – If the app spec format is invalid + +#### *static* from_network(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create an AppClient instance from network information. + +* **Parameters:** + * **app_spec** – The application specification + * **algorand** – The Algorand client instance + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Returns:** + A new AppClient instance +* **Raises:** + **Exception** – If no app ID is found for the network + +#### *static* from_creator_and_name(creator_address: str, app_name: str, app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../app_spec/arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract) | str, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create an AppClient instance from creator address and application name. + +* **Parameters:** + * **creator_address** – The address of the application creator + * **app_name** – The name of the application + * **app_spec** – The application specification + * **algorand** – The Algorand client instance + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache +* **Returns:** + A new AppClient instance +* **Raises:** + **ValueError** – If the app is not found for the creator and name + +#### *static* compile(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract), app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), compilation_params: [AppClientCompilationParams](#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [AppClientCompilationResult](#algokit_utils.applications.app_client.AppClientCompilationResult) + +Compile the application’s TEAL code. + +* **Parameters:** + * **app_spec** – The application specification + * **app_manager** – The application manager instance + * **compilation_params** – Optional compilation parameters +* **Returns:** + The compilation result +* **Raises:** + **ValueError** – If attempting to compile without source or byte code + +#### compile_app(compilation_params: [AppClientCompilationParams](#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [AppClientCompilationResult](#algokit_utils.applications.app_client.AppClientCompilationResult) + +Compile the application’s TEAL code. + +* **Parameters:** + **compilation_params** – Optional compilation parameters +* **Returns:** + The compilation result + +#### clone(app_name: str | None = \_MISSING, default_sender: str | None = \_MISSING, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = \_MISSING, approval_source_map: algosdk.source_map.SourceMap | None = \_MISSING, clear_source_map: algosdk.source_map.SourceMap | None = \_MISSING) → [AppClient](#algokit_utils.applications.app_client.AppClient) + +Create a cloned AppClient instance with optionally overridden parameters. + +* **Parameters:** + * **app_name** – Optional new application name + * **default_sender** – Optional new default sender + * **default_signer** – Optional new default signer + * **approval_source_map** – Optional new approval source map + * **clear_source_map** – Optional new clear source map +* **Returns:** + A new AppClient instance with the specified parameters + +#### export_source_maps() → [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps) + +Export the application’s source maps. + +* **Returns:** + The application’s source maps +* **Raises:** + **ValueError** – If source maps haven’t been loaded + +#### import_source_maps(source_maps: [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps)) → None + +Import source maps for the application. + +* **Parameters:** + **source_maps** – The source maps to import +* **Raises:** + **ValueError** – If source maps are invalid or missing + +#### get_local_state(address: str) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get local state for an account. + +* **Parameters:** + **address** – The account address +* **Returns:** + The account’s local state for this application + +#### get_global_state() → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the application’s global state. + +* **Returns:** + The application’s global state + +#### get_box_names() → list[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)] + +Get all box names for the application. + +* **Returns:** + List of box names + +#### get_box_value(name: algokit_utils.models.state.BoxIdentifier) → bytes + +Get the value of a box. + +* **Parameters:** + **name** – The box identifier +* **Returns:** + The box value as bytes + +#### get_box_value_from_abi_type(name: algokit_utils.models.state.BoxIdentifier, abi_type: algokit_utils.applications.abi.ABIType) → algokit_utils.applications.abi.ABIValue + +Get a box value decoded according to an ABI type. + +* **Parameters:** + * **name** – The box identifier + * **abi_type** – The ABI type to decode as +* **Returns:** + The decoded box value + +#### get_box_values(filter_func: collections.abc.Callable[[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)], bool] | None = None) → list[[algokit_utils.models.state.BoxValue](../../models/state/index.md#algokit_utils.models.state.BoxValue)] + +Get values for multiple boxes. + +* **Parameters:** + **filter_func** – Optional function to filter box names +* **Returns:** + List of box values + +#### get_box_values_from_abi_type(abi_type: algokit_utils.applications.abi.ABIType, filter_func: collections.abc.Callable[[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)], bool] | None = None) → list[[algokit_utils.applications.abi.BoxABIValue](../abi/index.md#algokit_utils.applications.abi.BoxABIValue)] + +Get multiple box values decoded according to an ABI type. + +* **Parameters:** + * **abi_type** – The ABI type to decode as + * **filter_func** – Optional function to filter box names +* **Returns:** + List of decoded box values + +#### fund_app_account(params: [FundAppAccountParams](#algokit_utils.applications.app_client.FundAppAccountParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [algokit_utils.transactions.transaction_sender.SendSingleTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Fund the application’s account. + +* **Parameters:** + * **params** – The funding parameters + * **send_params** – Send parameters, defaults to None +* **Returns:** + The transaction result diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md new file mode 100644 index 00000000..0719b1a3 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_deployer/index.md @@ -0,0 +1,128 @@ +# algokit_utils.applications.app_deployer + +## Attributes + +| [`APP_DEPLOY_NOTE_DAPP`](#algokit_utils.applications.app_deployer.APP_DEPLOY_NOTE_DAPP) | | +|-------------------------------------------------------------------------------------------|----| + +## Classes + +| [`AppDeploymentMetaData`](#algokit_utils.applications.app_deployer.AppDeploymentMetaData) | Metadata about an application stored in a transaction note during creation. | +|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| [`ApplicationReference`](#algokit_utils.applications.app_deployer.ApplicationReference) | Information about an Algorand app | +| [`ApplicationMetaData`](#algokit_utils.applications.app_deployer.ApplicationMetaData) | Complete metadata about a deployed app | +| [`ApplicationLookup`](#algokit_utils.applications.app_deployer.ApplicationLookup) | Cache of {py:class}\`ApplicationMetaData\` for a specific creator | +| [`AppDeployParams`](#algokit_utils.applications.app_deployer.AppDeployParams) | Parameters for deploying an app | +| [`AppDeployResult`](#algokit_utils.applications.app_deployer.AppDeployResult) | | +| [`AppDeployer`](#algokit_utils.applications.app_deployer.AppDeployer) | Manages deployment and deployment metadata of applications | + +## Module Contents + +### algokit_utils.applications.app_deployer.APP_DEPLOY_NOTE_DAPP *: str* *= 'ALGOKIT_DEPLOYER'* + +### *class* algokit_utils.applications.app_deployer.AppDeploymentMetaData + +Metadata about an application stored in a transaction note during creation. + +#### name *: str* + +#### version *: str* + +#### deletable *: bool | None* + +#### updatable *: bool | None* + +#### dictify() → dict[str, str | bool] + +### *class* algokit_utils.applications.app_deployer.ApplicationReference + +Information about an Algorand app + +#### app_id *: int* + +#### app_address *: str* + +### *class* algokit_utils.applications.app_deployer.ApplicationMetaData + +Complete metadata about a deployed app + +#### reference *: [ApplicationReference](#algokit_utils.applications.app_deployer.ApplicationReference)* + +#### deploy_metadata *: [AppDeploymentMetaData](#algokit_utils.applications.app_deployer.AppDeploymentMetaData)* + +#### created_round *: int* + +#### updated_round *: int* + +#### deleted *: bool* *= False* + +#### *property* app_id *: int* + +#### *property* app_address *: str* + +#### *property* name *: str* + +#### *property* version *: str* + +#### *property* deletable *: bool | None* + +#### *property* updatable *: bool | None* + +### *class* algokit_utils.applications.app_deployer.ApplicationLookup + +Cache of {py:class}\`ApplicationMetaData\` for a specific creator + +Can be used as an argument to {py:class}\`ApplicationClient\` to reduce the number of calls when deploying multiple +apps or discovering multiple app_ids + +#### creator *: str* + +#### apps *: dict[str, [ApplicationMetaData](#algokit_utils.applications.app_deployer.ApplicationMetaData)]* + +### *class* algokit_utils.applications.app_deployer.AppDeployParams + +Parameters for deploying an app + +#### metadata *: [AppDeploymentMetaData](#algokit_utils.applications.app_deployer.AppDeploymentMetaData)* + +#### deploy_time_params *: algokit_utils.models.state.TealTemplateParams | None* *= None* + +#### on_schema_break *: Literal['replace', 'fail', 'append'] | [algokit_utils.applications.enums.OnSchemaBreak](../enums/index.md#algokit_utils.applications.enums.OnSchemaBreak) | None* *= None* + +#### on_update *: Literal['update', 'replace', 'fail', 'append'] | [algokit_utils.applications.enums.OnUpdate](../enums/index.md#algokit_utils.applications.enums.OnUpdate) | None* *= None* + +#### create_params *: [algokit_utils.transactions.transaction_composer.AppCreateParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams) | [algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)* + +#### update_params *: [algokit_utils.transactions.transaction_composer.AppUpdateParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams) | [algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)* + +#### delete_params *: [algokit_utils.transactions.transaction_composer.AppDeleteParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams) | [algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)* + +#### existing_deployments *: [ApplicationLookup](#algokit_utils.applications.app_deployer.ApplicationLookup) | None* *= None* + +#### ignore_cache *: bool* *= False* + +#### max_fee *: int | None* *= None* + +#### send_params *: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None* *= None* + +### *class* algokit_utils.applications.app_deployer.AppDeployResult + +#### app *: [ApplicationMetaData](#algokit_utils.applications.app_deployer.ApplicationMetaData)* + +#### operation_performed *: [algokit_utils.applications.enums.OperationPerformed](../enums/index.md#algokit_utils.applications.enums.OperationPerformed)* + +#### create_result *: [algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### update_result *: [algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### delete_result *: [algokit_utils.transactions.transaction_sender.SendAppTransactionResult](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +### *class* algokit_utils.applications.app_deployer.AppDeployer(app_manager: [algokit_utils.applications.app_manager.AppManager](../app_manager/index.md#algokit_utils.applications.app_manager.AppManager), transaction_sender: [algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender), indexer: algosdk.v2client.indexer.IndexerClient | None = None) + +Manages deployment and deployment metadata of applications + +#### deploy(deployment: [AppDeployParams](#algokit_utils.applications.app_deployer.AppDeployParams)) → [AppDeployResult](#algokit_utils.applications.app_deployer.AppDeployResult) + +#### get_creator_apps_by_name(\*, creator_address: str, ignore_cache: bool = False) → [ApplicationLookup](#algokit_utils.applications.app_deployer.ApplicationLookup) + +Get apps created by an account diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md new file mode 100644 index 00000000..57bbc718 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_factory/index.md @@ -0,0 +1,114 @@ +# algokit_utils.applications.app_factory + +## Classes + +| [`AppFactoryParams`](#algokit_utils.applications.app_factory.AppFactoryParams) | | +|--------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------| +| [`AppFactoryCreateParams`](#algokit_utils.applications.app_factory.AppFactoryCreateParams) | | +| [`AppFactoryCreateMethodCallParams`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams) | | +| [`AppFactoryCreateMethodCallResult`](#algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult) | Base class for transaction results. | +| [`SendAppFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | | +| [`SendAppUpdateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | | +| [`SendAppCreateFactoryTransactionResult`](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | | +| [`AppFactoryDeployResult`](#algokit_utils.applications.app_factory.AppFactoryDeployResult) | Result from deploying an application via AppFactory | +| [`AppFactory`](#algokit_utils.applications.app_factory.AppFactory) | | + +## Module Contents + +### *class* algokit_utils.applications.app_factory.AppFactoryParams + +#### algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str* + +#### app_name *: str | None* *= None* + +#### default_sender *: str | None* *= None* + +#### default_signer *: algosdk.atomic_transaction_composer.TransactionSigner | None* *= None* + +#### version *: str | None* *= None* + +#### compilation_params *: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None* *= None* + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateParams + +Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientBareCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallParams + +Bases: `_AppFactoryCreateBaseParams`, [`algokit_utils.applications.app_client.AppClientMethodCallParams`](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) + +### *class* algokit_utils.applications.app_factory.AppFactoryCreateMethodCallResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendSingleTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `Generic`[`ABIReturnT`] + +Base class for transaction results. + +Represents the result of sending a single transaction. + +#### app_id *: int* + +#### app_address *: str* + +#### compiled_approval *: Any | None* *= None* + +#### compiled_clear *: Any | None* *= None* + +#### abi_return *: ABIReturnT | None* *= None* + +### *class* algokit_utils.applications.app_factory.SendAppFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +### *class* algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +### *class* algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult + +Bases: [`algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult`](../../transactions/transaction_sender/index.md#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[`algokit_utils.applications.abi.Arc56ReturnValueType`](../abi/index.md#algokit_utils.applications.abi.Arc56ReturnValueType)] + +### *class* algokit_utils.applications.app_factory.AppFactoryDeployResult + +Result from deploying an application via AppFactory + +#### app *: [algokit_utils.applications.app_deployer.ApplicationMetaData](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationMetaData)* + +#### operation_performed *: algokit_utils.applications.app_deployer.OperationPerformed* + +#### create_result *: [SendAppCreateFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppCreateFactoryTransactionResult) | None* *= None* + +#### update_result *: [SendAppUpdateFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppUpdateFactoryTransactionResult) | None* *= None* + +#### delete_result *: [SendAppFactoryTransactionResult](#algokit_utils.applications.app_factory.SendAppFactoryTransactionResult) | None* *= None* + +#### *classmethod* from_deploy_result(response: [algokit_utils.applications.app_deployer.AppDeployResult](../app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployResult), deploy_params: [algokit_utils.applications.app_deployer.AppDeployParams](../app_deployer/index.md#algokit_utils.applications.app_deployer.AppDeployParams), app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract), app_compilation_data: [algokit_utils.applications.app_client.AppClientCompilationResult](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationResult) | None = None) → typing_extensions.Self + +### *class* algokit_utils.applications.app_factory.AppFactory(params: [AppFactoryParams](#algokit_utils.applications.app_factory.AppFactoryParams)) + +#### *property* app_name *: str* + +#### *property* app_spec *: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract)* + +#### *property* algorand *: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)* + +#### *property* params *: \_MethodParamsBuilder* + +#### *property* send *: \_TransactionSender* + +#### *property* create_transaction *: \_TransactionCreator* + +#### deploy(\*, on_update: algokit_utils.applications.app_deployer.OnUpdate | None = None, on_schema_break: algokit_utils.applications.app_deployer.OnSchemaBreak | None = None, create_params: [algokit_utils.applications.app_client.AppClientMethodCallCreateParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallCreateParams) | [algokit_utils.applications.app_client.AppClientBareCallCreateParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallCreateParams) | None = None, update_params: [algokit_utils.applications.app_client.AppClientMethodCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) | [algokit_utils.applications.app_client.AppClientBareCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) | None = None, delete_params: [algokit_utils.applications.app_client.AppClientMethodCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientMethodCallParams) | [algokit_utils.applications.app_client.AppClientBareCallParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientBareCallParams) | None = None, existing_deployments: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, ignore_cache: bool = False, app_name: str | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → tuple[[algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient), [AppFactoryDeployResult](#algokit_utils.applications.app_factory.AppFactoryDeployResult)] + +Deploy the application with the specified parameters. + +#### get_app_client_by_id(app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient) + +#### get_app_client_by_creator_and_name(creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../app_client/index.md#algokit_utils.applications.app_client.AppClient) + +#### export_source_maps() → [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps) + +#### import_source_maps(source_maps: [algokit_utils.models.application.AppSourceMaps](../../models/application/index.md#algokit_utils.models.application.AppSourceMaps)) → None + +#### compile(compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [algokit_utils.applications.app_client.AppClientCompilationResult](../app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationResult) diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md new file mode 100644 index 00000000..a29a20ec --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_manager/index.md @@ -0,0 +1,202 @@ +# algokit_utils.applications.app_manager + +## Attributes + +| [`UPDATABLE_TEMPLATE_NAME`](#algokit_utils.applications.app_manager.UPDATABLE_TEMPLATE_NAME) | The name of the TEAL template variable for deploy-time immutability control. | +|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [`DELETABLE_TEMPLATE_NAME`](#algokit_utils.applications.app_manager.DELETABLE_TEMPLATE_NAME) | The name of the TEAL template variable for deploy-time permanence control. | + +## Classes + +| [`AppManager`](#algokit_utils.applications.app_manager.AppManager) | A manager class for interacting with Algorand applications. | +|----------------------------------------------------------------------|---------------------------------------------------------------| + +## Module Contents + +### algokit_utils.applications.app_manager.UPDATABLE_TEMPLATE_NAME *= 'TMPL_UPDATABLE'* + +The name of the TEAL template variable for deploy-time immutability control. + +### algokit_utils.applications.app_manager.DELETABLE_TEMPLATE_NAME *= 'TMPL_DELETABLE'* + +The name of the TEAL template variable for deploy-time permanence control. + +### *class* algokit_utils.applications.app_manager.AppManager(algod_client: algosdk.v2client.algod.AlgodClient) + +A manager class for interacting with Algorand applications. + +Provides functionality for compiling TEAL code, managing application state, +and interacting with application boxes. + +* **Parameters:** + **algod_client** – The Algorand client instance to use for interacting with the network + +#### compile_teal(teal_code: str) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) + +Compile TEAL source code. + +* **Parameters:** + **teal_code** – The TEAL source code to compile +* **Returns:** + The compiled TEAL code and associated metadata + +#### compile_teal_template(teal_template_code: str, template_params: algokit_utils.models.state.TealTemplateParams | None = None, deployment_metadata: collections.abc.Mapping[str, bool | None] | None = None) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) + +Compile a TEAL template with parameters. + +* **Parameters:** + * **teal_template_code** – The TEAL template code to compile + * **template_params** – Parameters to substitute in the template + * **deployment_metadata** – Deployment control parameters +* **Returns:** + The compiled TEAL code and associated metadata + +#### get_compilation_result(teal_code: str) → [algokit_utils.models.application.CompiledTeal](../../models/application/index.md#algokit_utils.models.application.CompiledTeal) | None + +Get cached compilation result for TEAL code if available. + +* **Parameters:** + **teal_code** – The TEAL source code +* **Returns:** + The cached compilation result if available, None otherwise + +#### get_by_id(app_id: int) → [algokit_utils.models.application.AppInformation](../../models/application/index.md#algokit_utils.models.application.AppInformation) + +Get information about an application by ID. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + Information about the application + +#### get_global_state(app_id: int) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the global state of an application. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + The application’s global state + +#### get_local_state(app_id: int, address: str) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Get the local state for an account in an application. + +* **Parameters:** + * **app_id** – The application ID + * **address** – The account address +* **Returns:** + The account’s local state for the application +* **Raises:** + **ValueError** – If local state is not found + +#### get_box_names(app_id: int) → list[[algokit_utils.models.state.BoxName](../../models/state/index.md#algokit_utils.models.state.BoxName)] + +Get names of all boxes for an application. + +* **Parameters:** + **app_id** – The application ID +* **Returns:** + List of box names + +#### get_box_value(app_id: int, box_name: algokit_utils.models.state.BoxIdentifier) → bytes + +Get the value stored in a box. + +* **Parameters:** + * **app_id** – The application ID + * **box_name** – The box identifier +* **Returns:** + The box value as bytes + +#### get_box_values(app_id: int, box_names: list[algokit_utils.models.state.BoxIdentifier]) → list[bytes] + +Get values for multiple boxes. + +* **Parameters:** + * **app_id** – The application ID + * **box_names** – List of box identifiers +* **Returns:** + List of box values as bytes + +#### get_box_value_from_abi_type(app_id: int, box_name: algokit_utils.models.state.BoxIdentifier, abi_type: algokit_utils.applications.abi.ABIType) → algokit_utils.applications.abi.ABIValue + +Get and decode a box value using an ABI type. + +* **Parameters:** + * **app_id** – The application ID + * **box_name** – The box identifier + * **abi_type** – The ABI type to decode with +* **Returns:** + The decoded box value +* **Raises:** + **ValueError** – If decoding fails + +#### get_box_values_from_abi_type(app_id: int, box_names: list[algokit_utils.models.state.BoxIdentifier], abi_type: algokit_utils.applications.abi.ABIType) → list[algokit_utils.applications.abi.ABIValue] + +Get and decode multiple box values using an ABI type. + +* **Parameters:** + * **app_id** – The application ID + * **box_names** – List of box identifiers + * **abi_type** – The ABI type to decode with +* **Returns:** + List of decoded box values + +#### *static* get_box_reference(box_id: algokit_utils.models.state.BoxIdentifier | [algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference)) → tuple[int, bytes] + +Get standardized box reference from various identifier types. + +* **Parameters:** + **box_id** – The box identifier +* **Returns:** + Tuple of (app_id, box_name_bytes) +* **Raises:** + **ValueError** – If box identifier type is invalid + +#### *static* get_abi_return(confirmation: algosdk.v2client.algod.AlgodResponseType, method: algosdk.abi.Method | None = None) → [algokit_utils.applications.abi.ABIReturn](../abi/index.md#algokit_utils.applications.abi.ABIReturn) | None + +Get the ABI return value from a transaction confirmation. + +* **Parameters:** + * **confirmation** – The transaction confirmation + * **method** – The ABI method +* **Returns:** + The parsed ABI return value, or None if not available + +#### *static* decode_app_state(state: list[dict[str, Any]]) → dict[str, [algokit_utils.models.application.AppState](../../models/application/index.md#algokit_utils.models.application.AppState)] + +Decode application state from raw format. + +* **Parameters:** + **state** – The raw application state +* **Returns:** + Decoded application state +* **Raises:** + **ValueError** – If unknown state data type is encountered + +#### *static* replace_template_variables(program: str, template_values: algokit_utils.models.state.TealTemplateParams) → str + +Replace template variables in TEAL code. + +* **Parameters:** + * **program** – The TEAL program code + * **template_values** – Template variable values to substitute +* **Returns:** + TEAL code with substituted values +* **Raises:** + **ValueError** – If template value type is unexpected + +#### *static* replace_teal_template_deploy_time_control_params(teal_template_code: str, params: collections.abc.Mapping[str, bool | None]) → str + +Replace deploy-time control parameters in TEAL template. + +* **Parameters:** + * **teal_template_code** – The TEAL template code + * **params** – The deploy-time control parameters +* **Returns:** + TEAL code with substituted control parameters +* **Raises:** + **ValueError** – If template variables not found in code + +#### *static* strip_teal_comments(teal_code: str) → str diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md new file mode 100644 index 00000000..8ad088c2 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc32/index.md @@ -0,0 +1,157 @@ +# algokit_utils.applications.app_spec.arc32 + +## Attributes + +| [`AppSpecStateDict`](#algokit_utils.applications.app_spec.arc32.AppSpecStateDict) | Type defining Application Specification state entries | +|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| [`OnCompleteActionName`](#algokit_utils.applications.app_spec.arc32.OnCompleteActionName) | String literals representing on completion transaction types | +| [`MethodConfigDict`](#algokit_utils.applications.app_spec.arc32.MethodConfigDict) | Dictionary of dict[OnCompletionActionName, CallConfig] representing allowed actions for each on completion type | +| [`DefaultArgumentType`](#algokit_utils.applications.app_spec.arc32.DefaultArgumentType) | Literal values describing the types of default argument sources | +| [`StateDict`](#algokit_utils.applications.app_spec.arc32.StateDict) | | + +## Classes + +| [`CallConfig`](#algokit_utils.applications.app_spec.arc32.CallConfig) | Describes the type of calls a method can be used for based on {py:class}\`algosdk.transaction.OnComplete\` type | +|-----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| [`StructArgDict`](#algokit_utils.applications.app_spec.arc32.StructArgDict) | dict() -> new empty dictionary | +| [`DefaultArgumentDict`](#algokit_utils.applications.app_spec.arc32.DefaultArgumentDict) | DefaultArgument is a container for any arguments that may | +| [`MethodHints`](#algokit_utils.applications.app_spec.arc32.MethodHints) | MethodHints provides hints to the caller about how to call the method | +| [`Arc32Contract`](#algokit_utils.applications.app_spec.arc32.Arc32Contract) | ARC-0032 application specification | + +## Module Contents + +### algokit_utils.applications.app_spec.arc32.AppSpecStateDict *: TypeAlias* *= dict[str, dict[str, dict]]* + +Type defining Application Specification state entries + +### *class* algokit_utils.applications.app_spec.arc32.CallConfig + +Bases: `enum.IntFlag` + +Describes the type of calls a method can be used for based on {py:class}\`algosdk.transaction.OnComplete\` type + +#### NEVER *= 0* + +Never handle the specified on completion type + +#### CALL *= 1* + +Only handle the specified on completion type for application calls + +#### CREATE *= 2* + +Only handle the specified on completion type for application create calls + +#### ALL *= 3* + +Handle the specified on completion type for both create and normal application calls + +### *class* algokit_utils.applications.app_spec.arc32.StructArgDict + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### name *: str* + +#### elements *: list[list[str]]* + +### algokit_utils.applications.app_spec.arc32.OnCompleteActionName *: TypeAlias* *= Literal['no_op', 'opt_in', 'close_out', 'clear_state', 'update_application', 'delete_application']* + +String literals representing on completion transaction types + +### algokit_utils.applications.app_spec.arc32.MethodConfigDict *: TypeAlias* *= dict[OnCompleteActionName, CallConfig]* + +Dictionary of dict[OnCompletionActionName, CallConfig] representing allowed actions for each on completion type + +### algokit_utils.applications.app_spec.arc32.DefaultArgumentType *: TypeAlias* *= Literal['abi-method', 'local-state', 'global-state', 'constant']* + +Literal values describing the types of default argument sources + +### *class* algokit_utils.applications.app_spec.arc32.DefaultArgumentDict + +Bases: `TypedDict` + +DefaultArgument is a container for any arguments that may +be resolved prior to calling some target method + +#### source *: DefaultArgumentType* + +#### data *: int | str | bytes | algosdk.abi.method.MethodDict* + +### algokit_utils.applications.app_spec.arc32.StateDict + +### *class* algokit_utils.applications.app_spec.arc32.MethodHints + +MethodHints provides hints to the caller about how to call the method + +#### read_only *: bool* *= False* + +#### structs *: dict[str, [StructArgDict](#algokit_utils.applications.app_spec.arc32.StructArgDict)]* + +#### default_arguments *: dict[str, [DefaultArgumentDict](#algokit_utils.applications.app_spec.arc32.DefaultArgumentDict)]* + +#### call_config *: MethodConfigDict* + +#### empty() → bool + +#### dictify() → dict[str, Any] + +#### *static* undictify(data: dict[str, Any]) → [MethodHints](#algokit_utils.applications.app_spec.arc32.MethodHints) + +### *class* algokit_utils.applications.app_spec.arc32.Arc32Contract + +ARC-0032 application specification + +See <[https://github.com/algorandfoundation/ARCs/pull/150](https://github.com/algorandfoundation/ARCs/pull/150)> + +#### approval_program *: str* + +#### clear_program *: str* + +#### contract *: algosdk.abi.Contract* + +#### hints *: dict[str, [MethodHints](#algokit_utils.applications.app_spec.arc32.MethodHints)]* + +#### schema *: StateDict* + +#### global_state_schema *: algosdk.transaction.StateSchema* + +#### local_state_schema *: algosdk.transaction.StateSchema* + +#### bare_call_config *: MethodConfigDict* + +#### dictify() → dict + +#### to_json(indent: int | None = None) → str + +#### *static* from_json(application_spec: str) → [Arc32Contract](#algokit_utils.applications.app_spec.arc32.Arc32Contract) + +#### export(directory: pathlib.Path | str | None = None) → None + +Write out the artifacts generated by the application to disk. + +Writes the approval program, clear program, contract specification and application specification +to files in the specified directory. + +* **Parameters:** + **directory** – Path to the directory where the artifacts should be written. If not specified, + uses the current working directory diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md new file mode 100644 index 00000000..f6262e7a --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/arc56/index.md @@ -0,0 +1,681 @@ +# algokit_utils.applications.app_spec.arc56 + +## Classes + +| [`StructField`](#algokit_utils.applications.app_spec.arc56.StructField) | Represents a field in a struct type. | +|-------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| [`CallEnum`](#algokit_utils.applications.app_spec.arc56.CallEnum) | Enum representing different call types for application transactions. | +| [`CreateEnum`](#algokit_utils.applications.app_spec.arc56.CreateEnum) | Enum representing different create types for application transactions. | +| [`BareActions`](#algokit_utils.applications.app_spec.arc56.BareActions) | Represents bare call and create actions for an application. | +| [`ByteCode`](#algokit_utils.applications.app_spec.arc56.ByteCode) | Represents the approval and clear program bytecode. | +| [`Compiler`](#algokit_utils.applications.app_spec.arc56.Compiler) | Enum representing different compiler types. | +| [`CompilerVersion`](#algokit_utils.applications.app_spec.arc56.CompilerVersion) | Represents compiler version information. | +| [`CompilerInfo`](#algokit_utils.applications.app_spec.arc56.CompilerInfo) | Information about the compiler used. | +| [`Network`](#algokit_utils.applications.app_spec.arc56.Network) | Network-specific application information. | +| [`ScratchVariables`](#algokit_utils.applications.app_spec.arc56.ScratchVariables) | Information about scratch space variables. | +| [`Source`](#algokit_utils.applications.app_spec.arc56.Source) | Source code for approval and clear programs. | +| [`Global`](#algokit_utils.applications.app_spec.arc56.Global) | Global state schema. | +| [`Local`](#algokit_utils.applications.app_spec.arc56.Local) | Local state schema. | +| [`Schema`](#algokit_utils.applications.app_spec.arc56.Schema) | Application state schema. | +| [`TemplateVariables`](#algokit_utils.applications.app_spec.arc56.TemplateVariables) | Template variable information. | +| [`EventArg`](#algokit_utils.applications.app_spec.arc56.EventArg) | Event argument information. | +| [`Event`](#algokit_utils.applications.app_spec.arc56.Event) | Event information. | +| [`Actions`](#algokit_utils.applications.app_spec.arc56.Actions) | Method actions information. | +| [`DefaultValue`](#algokit_utils.applications.app_spec.arc56.DefaultValue) | Default value information for method arguments. | +| [`MethodArg`](#algokit_utils.applications.app_spec.arc56.MethodArg) | Method argument information. | +| [`Boxes`](#algokit_utils.applications.app_spec.arc56.Boxes) | Box storage requirements. | +| [`Recommendations`](#algokit_utils.applications.app_spec.arc56.Recommendations) | Method execution recommendations. | +| [`Returns`](#algokit_utils.applications.app_spec.arc56.Returns) | Method return information. | +| [`Method`](#algokit_utils.applications.app_spec.arc56.Method) | Method information. | +| [`PcOffsetMethod`](#algokit_utils.applications.app_spec.arc56.PcOffsetMethod) | PC offset method types. | +| [`SourceInfo`](#algokit_utils.applications.app_spec.arc56.SourceInfo) | Source code location information. | +| [`StorageKey`](#algokit_utils.applications.app_spec.arc56.StorageKey) | Storage key information. | +| [`StorageMap`](#algokit_utils.applications.app_spec.arc56.StorageMap) | Storage map information. | +| [`Keys`](#algokit_utils.applications.app_spec.arc56.Keys) | Storage keys for different storage types. | +| [`Maps`](#algokit_utils.applications.app_spec.arc56.Maps) | Storage maps for different storage types. | +| [`State`](#algokit_utils.applications.app_spec.arc56.State) | Application state information. | +| [`ProgramSourceInfo`](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo) | Program source information. | +| [`SourceInfoModel`](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) | Source information for approval and clear programs. | +| [`Arc56Contract`](#algokit_utils.applications.app_spec.arc56.Arc56Contract) | ARC-0056 application specification. | + +## Module Contents + +### *class* algokit_utils.applications.app_spec.arc56.StructField + +Represents a field in a struct type. + +* **Variables:** + * **name** – Name of the struct field + * **type** – Type of the struct field, either a string or list of StructFields + +#### name *: str* + +#### type *: list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)] | str* + +#### *static* from_dict(data: dict[str, Any]) → [StructField](#algokit_utils.applications.app_spec.arc56.StructField) + +### *class* algokit_utils.applications.app_spec.arc56.CallEnum + +Bases: `str`, `enum.Enum` + +Enum representing different call types for application transactions. + +#### CLEAR_STATE *= 'ClearState'* + +#### CLOSE_OUT *= 'CloseOut'* + +#### DELETE_APPLICATION *= 'DeleteApplication'* + +#### NO_OP *= 'NoOp'* + +#### OPT_IN *= 'OptIn'* + +#### UPDATE_APPLICATION *= 'UpdateApplication'* + +### *class* algokit_utils.applications.app_spec.arc56.CreateEnum + +Bases: `str`, `enum.Enum` + +Enum representing different create types for application transactions. + +#### DELETE_APPLICATION *= 'DeleteApplication'* + +#### NO_OP *= 'NoOp'* + +#### OPT_IN *= 'OptIn'* + +### *class* algokit_utils.applications.app_spec.arc56.BareActions + +Represents bare call and create actions for an application. + +* **Variables:** + * **call** – List of allowed call actions + * **create** – List of allowed create actions + +#### call *: list[[CallEnum](#algokit_utils.applications.app_spec.arc56.CallEnum)]* + +#### create *: list[[CreateEnum](#algokit_utils.applications.app_spec.arc56.CreateEnum)]* + +#### *static* from_dict(data: dict[str, Any]) → [BareActions](#algokit_utils.applications.app_spec.arc56.BareActions) + +### *class* algokit_utils.applications.app_spec.arc56.ByteCode + +Represents the approval and clear program bytecode. + +* **Variables:** + * **approval** – Base64 encoded approval program bytecode + * **clear** – Base64 encoded clear program bytecode + +#### approval *: str* + +#### clear *: str* + +#### *static* from_dict(data: dict[str, Any]) → [ByteCode](#algokit_utils.applications.app_spec.arc56.ByteCode) + +### *class* algokit_utils.applications.app_spec.arc56.Compiler + +Bases: `str`, `enum.Enum` + +Enum representing different compiler types. + +#### ALGOD *= 'algod'* + +#### PUYA *= 'puya'* + +### *class* algokit_utils.applications.app_spec.arc56.CompilerVersion + +Represents compiler version information. + +* **Variables:** + * **commit_hash** – Git commit hash of the compiler + * **major** – Major version number + * **minor** – Minor version number + * **patch** – Patch version number + +#### commit_hash *: str | None* *= None* + +#### major *: int | None* *= None* + +#### minor *: int | None* *= None* + +#### patch *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [CompilerVersion](#algokit_utils.applications.app_spec.arc56.CompilerVersion) + +### *class* algokit_utils.applications.app_spec.arc56.CompilerInfo + +Information about the compiler used. + +* **Variables:** + * **compiler** – Type of compiler used + * **compiler_version** – Version information for the compiler + +#### compiler *: [Compiler](#algokit_utils.applications.app_spec.arc56.Compiler)* + +#### compiler_version *: [CompilerVersion](#algokit_utils.applications.app_spec.arc56.CompilerVersion)* + +#### *static* from_dict(data: dict[str, Any]) → [CompilerInfo](#algokit_utils.applications.app_spec.arc56.CompilerInfo) + +### *class* algokit_utils.applications.app_spec.arc56.Network + +Network-specific application information. + +* **Variables:** + **app_id** – Application ID on the network + +#### app_id *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Network](#algokit_utils.applications.app_spec.arc56.Network) + +### *class* algokit_utils.applications.app_spec.arc56.ScratchVariables + +Information about scratch space variables. + +* **Variables:** + * **slot** – Scratch slot number + * **type** – Type of the scratch variable + +#### slot *: int* + +#### type *: str* + +#### *static* from_dict(data: dict[str, Any]) → [ScratchVariables](#algokit_utils.applications.app_spec.arc56.ScratchVariables) + +### *class* algokit_utils.applications.app_spec.arc56.Source + +Source code for approval and clear programs. + +* **Variables:** + * **approval** – Base64 encoded approval program source + * **clear** – Base64 encoded clear program source + +#### approval *: str* + +#### clear *: str* + +#### *static* from_dict(data: dict[str, Any]) → [Source](#algokit_utils.applications.app_spec.arc56.Source) + +#### get_decoded_approval() → str + +Get decoded approval program source. + +* **Returns:** + Decoded approval program source code + +#### get_decoded_clear() → str + +Get decoded clear program source. + +* **Returns:** + Decoded clear program source code + +### *class* algokit_utils.applications.app_spec.arc56.Global + +Global state schema. + +* **Variables:** + * **bytes** – Number of byte slices in global state + * **ints** – Number of integers in global state + +#### bytes *: int* + +#### ints *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Global](#algokit_utils.applications.app_spec.arc56.Global) + +### *class* algokit_utils.applications.app_spec.arc56.Local + +Local state schema. + +* **Variables:** + * **bytes** – Number of byte slices in local state + * **ints** – Number of integers in local state + +#### bytes *: int* + +#### ints *: int* + +#### *static* from_dict(data: dict[str, Any]) → [Local](#algokit_utils.applications.app_spec.arc56.Local) + +### *class* algokit_utils.applications.app_spec.arc56.Schema + +Application state schema. + +* **Variables:** + * **global_state** – Global state schema + * **local_state** – Local state schema + +#### global_state *: [Global](#algokit_utils.applications.app_spec.arc56.Global)* + +#### local_state *: [Local](#algokit_utils.applications.app_spec.arc56.Local)* + +#### *static* from_dict(data: dict[str, Any]) → [Schema](#algokit_utils.applications.app_spec.arc56.Schema) + +### *class* algokit_utils.applications.app_spec.arc56.TemplateVariables + +Template variable information. + +* **Variables:** + * **type** – Type of the template variable + * **value** – Optional value of the template variable + +#### type *: str* + +#### value *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [TemplateVariables](#algokit_utils.applications.app_spec.arc56.TemplateVariables) + +### *class* algokit_utils.applications.app_spec.arc56.EventArg + +Event argument information. + +* **Variables:** + * **type** – Type of the event argument + * **desc** – Optional description of the argument + * **name** – Optional name of the argument + * **struct** – Optional struct type name + +#### type *: str* + +#### desc *: str | None* *= None* + +#### name *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [EventArg](#algokit_utils.applications.app_spec.arc56.EventArg) + +### *class* algokit_utils.applications.app_spec.arc56.Event + +Event information. + +* **Variables:** + * **args** – List of event arguments + * **name** – Name of the event + * **desc** – Optional description of the event + +#### args *: list[[EventArg](#algokit_utils.applications.app_spec.arc56.EventArg)]* + +#### name *: str* + +#### desc *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Event](#algokit_utils.applications.app_spec.arc56.Event) + +### *class* algokit_utils.applications.app_spec.arc56.Actions + +Method actions information. + +* **Variables:** + * **call** – Optional list of allowed call actions + * **create** – Optional list of allowed create actions + +#### call *: list[[CallEnum](#algokit_utils.applications.app_spec.arc56.CallEnum)] | None* *= None* + +#### create *: list[[CreateEnum](#algokit_utils.applications.app_spec.arc56.CreateEnum)] | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Actions](#algokit_utils.applications.app_spec.arc56.Actions) + +### *class* algokit_utils.applications.app_spec.arc56.DefaultValue + +Default value information for method arguments. + +* **Variables:** + * **data** – Default value data + * **source** – Source of the default value + * **type** – Optional type of the default value + +#### data *: str* + +#### source *: Literal['box', 'global', 'local', 'literal', 'method']* + +#### type *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [DefaultValue](#algokit_utils.applications.app_spec.arc56.DefaultValue) + +### *class* algokit_utils.applications.app_spec.arc56.MethodArg + +Method argument information. + +* **Variables:** + * **type** – Type of the argument + * **default_value** – Optional default value + * **desc** – Optional description + * **name** – Optional name + * **struct** – Optional struct type name + +#### type *: str* + +#### default_value *: [DefaultValue](#algokit_utils.applications.app_spec.arc56.DefaultValue) | None* *= None* + +#### desc *: str | None* *= None* + +#### name *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [MethodArg](#algokit_utils.applications.app_spec.arc56.MethodArg) + +### *class* algokit_utils.applications.app_spec.arc56.Boxes + +Box storage requirements. + +* **Variables:** + * **key** – Box key + * **read_bytes** – Number of bytes to read + * **write_bytes** – Number of bytes to write + * **app** – Optional application ID + +#### key *: str* + +#### read_bytes *: int* + +#### write_bytes *: int* + +#### app *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Boxes](#algokit_utils.applications.app_spec.arc56.Boxes) + +### *class* algokit_utils.applications.app_spec.arc56.Recommendations + +Method execution recommendations. + +* **Variables:** + * **accounts** – Optional list of accounts + * **apps** – Optional list of applications + * **assets** – Optional list of assets + * **boxes** – Optional box storage requirements + * **inner_transaction_count** – Optional inner transaction count + +#### accounts *: list[str] | None* *= None* + +#### apps *: list[int] | None* *= None* + +#### assets *: list[int] | None* *= None* + +#### boxes *: [Boxes](#algokit_utils.applications.app_spec.arc56.Boxes) | None* *= None* + +#### inner_transaction_count *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Recommendations](#algokit_utils.applications.app_spec.arc56.Recommendations) + +### *class* algokit_utils.applications.app_spec.arc56.Returns + +Method return information. + +* **Variables:** + * **type** – Return type + * **desc** – Optional description + * **struct** – Optional struct type name + +#### type *: str* + +#### desc *: str | None* *= None* + +#### struct *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [Returns](#algokit_utils.applications.app_spec.arc56.Returns) + +### *class* algokit_utils.applications.app_spec.arc56.Method + +Method information. + +* **Variables:** + * **actions** – Allowed actions + * **args** – Method arguments + * **name** – Method name + * **returns** – Return information + * **desc** – Optional description + * **events** – Optional list of events + * **readonly** – Optional readonly flag + * **recommendations** – Optional execution recommendations + +#### actions *: [Actions](#algokit_utils.applications.app_spec.arc56.Actions)* + +#### args *: list[[MethodArg](#algokit_utils.applications.app_spec.arc56.MethodArg)]* + +#### name *: str* + +#### returns *: [Returns](#algokit_utils.applications.app_spec.arc56.Returns)* + +#### desc *: str | None* *= None* + +#### events *: list[[Event](#algokit_utils.applications.app_spec.arc56.Event)] | None* *= None* + +#### readonly *: bool | None* *= None* + +#### recommendations *: [Recommendations](#algokit_utils.applications.app_spec.arc56.Recommendations) | None* *= None* + +#### to_abi_method() → algosdk.abi.Method + +Convert to ABI method. + +* **Raises:** + **ValueError** – If underlying ABI method is not initialized +* **Returns:** + ABI method + +#### *static* from_dict(data: dict[str, Any]) → [Method](#algokit_utils.applications.app_spec.arc56.Method) + +### *class* algokit_utils.applications.app_spec.arc56.PcOffsetMethod + +Bases: `str`, `enum.Enum` + +PC offset method types. + +#### CBLOCKS *= 'cblocks'* + +#### NONE *= 'none'* + +### *class* algokit_utils.applications.app_spec.arc56.SourceInfo + +Source code location information. + +* **Variables:** + * **pc** – List of program counter values + * **error_message** – Optional error message + * **source** – Optional source code + * **teal** – Optional TEAL version + +#### pc *: list[int]* + +#### error_message *: str | None* *= None* + +#### source *: str | None* *= None* + +#### teal *: int | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [SourceInfo](#algokit_utils.applications.app_spec.arc56.SourceInfo) + +### *class* algokit_utils.applications.app_spec.arc56.StorageKey + +Storage key information. + +* **Variables:** + * **key** – Storage key + * **key_type** – Type of the key + * **value_type** – Type of the value + * **desc** – Optional description + +#### key *: str* + +#### key_type *: str* + +#### value_type *: str* + +#### desc *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey) + +### *class* algokit_utils.applications.app_spec.arc56.StorageMap + +Storage map information. + +* **Variables:** + * **key_type** – Type of map keys + * **value_type** – Type of map values + * **desc** – Optional description + * **prefix** – Optional key prefix + +#### key_type *: str* + +#### value_type *: str* + +#### desc *: str | None* *= None* + +#### prefix *: str | None* *= None* + +#### *static* from_dict(data: dict[str, Any]) → [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap) + +### *class* algokit_utils.applications.app_spec.arc56.Keys + +Storage keys for different storage types. + +* **Variables:** + * **box** – Box storage keys + * **global_state** – Global state storage keys + * **local_state** – Local state storage keys + +#### box *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### global_state *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### local_state *: dict[str, [StorageKey](#algokit_utils.applications.app_spec.arc56.StorageKey)]* + +#### *static* from_dict(data: dict[str, Any]) → [Keys](#algokit_utils.applications.app_spec.arc56.Keys) + +### *class* algokit_utils.applications.app_spec.arc56.Maps + +Storage maps for different storage types. + +* **Variables:** + * **box** – Box storage maps + * **global_state** – Global state storage maps + * **local_state** – Local state storage maps + +#### box *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### global_state *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### local_state *: dict[str, [StorageMap](#algokit_utils.applications.app_spec.arc56.StorageMap)]* + +#### *static* from_dict(data: dict[str, Any]) → [Maps](#algokit_utils.applications.app_spec.arc56.Maps) + +### *class* algokit_utils.applications.app_spec.arc56.State + +Application state information. + +* **Variables:** + * **keys** – Storage keys + * **maps** – Storage maps + * **schema** – State schema + +#### keys *: [Keys](#algokit_utils.applications.app_spec.arc56.Keys)* + +#### maps *: [Maps](#algokit_utils.applications.app_spec.arc56.Maps)* + +#### schema *: [Schema](#algokit_utils.applications.app_spec.arc56.Schema)* + +#### *static* from_dict(data: dict[str, Any]) → [State](#algokit_utils.applications.app_spec.arc56.State) + +### *class* algokit_utils.applications.app_spec.arc56.ProgramSourceInfo + +Program source information. + +* **Variables:** + * **pc_offset_method** – PC offset method + * **source_info** – List of source info entries + +#### pc_offset_method *: [PcOffsetMethod](#algokit_utils.applications.app_spec.arc56.PcOffsetMethod)* + +#### source_info *: list[[SourceInfo](#algokit_utils.applications.app_spec.arc56.SourceInfo)]* + +#### *static* from_dict(data: dict[str, Any]) → [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo) + +### *class* algokit_utils.applications.app_spec.arc56.SourceInfoModel + +Source information for approval and clear programs. + +* **Variables:** + * **approval** – Approval program source info + * **clear** – Clear program source info + +#### approval *: [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo)* + +#### clear *: [ProgramSourceInfo](#algokit_utils.applications.app_spec.arc56.ProgramSourceInfo)* + +#### *static* from_dict(data: dict[str, Any]) → [SourceInfoModel](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) + +### *class* algokit_utils.applications.app_spec.arc56.Arc56Contract + +ARC-0056 application specification. + +See [https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) + +* **Variables:** + * **arcs** – List of supported ARC version numbers + * **bare_actions** – Bare call and create actions + * **methods** – List of contract methods + * **name** – Contract name + * **state** – Contract state information + * **structs** – Contract struct definitions + * **byte_code** – Optional bytecode for approval and clear programs + * **compiler_info** – Optional compiler information + * **desc** – Optional contract description + * **events** – Optional list of contract events + * **networks** – Optional network deployment information + * **scratch_variables** – Optional scratch variable information + * **source** – Optional source code + * **source_info** – Optional source code information + * **template_variables** – Optional template variable information + +#### arcs *: list[int]* + +#### bare_actions *: [BareActions](#algokit_utils.applications.app_spec.arc56.BareActions)* + +#### methods *: list[[Method](#algokit_utils.applications.app_spec.arc56.Method)]* + +#### name *: str* + +#### state *: [State](#algokit_utils.applications.app_spec.arc56.State)* + +#### structs *: dict[str, list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)]]* + +#### byte_code *: [ByteCode](#algokit_utils.applications.app_spec.arc56.ByteCode) | None* *= None* + +#### compiler_info *: [CompilerInfo](#algokit_utils.applications.app_spec.arc56.CompilerInfo) | None* *= None* + +#### desc *: str | None* *= None* + +#### events *: list[[Event](#algokit_utils.applications.app_spec.arc56.Event)] | None* *= None* + +#### networks *: dict[str, [Network](#algokit_utils.applications.app_spec.arc56.Network)] | None* *= None* + +#### scratch_variables *: dict[str, [ScratchVariables](#algokit_utils.applications.app_spec.arc56.ScratchVariables)] | None* *= None* + +#### source *: [Source](#algokit_utils.applications.app_spec.arc56.Source) | None* *= None* + +#### source_info *: [SourceInfoModel](#algokit_utils.applications.app_spec.arc56.SourceInfoModel) | None* *= None* + +#### template_variables *: dict[str, [TemplateVariables](#algokit_utils.applications.app_spec.arc56.TemplateVariables)] | None* *= None* + +#### *static* from_dict(application_spec: dict) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +Create Arc56Contract from dictionary. + +* **Parameters:** + **application_spec** – Dictionary containing contract specification +* **Returns:** + Arc56Contract instance + +#### *static* from_json(application_spec: str) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +#### *static* from_arc32(arc32_application_spec: str | [algokit_utils.applications.app_spec.arc32.Arc32Contract](../arc32/index.md#algokit_utils.applications.app_spec.arc32.Arc32Contract)) → [Arc56Contract](#algokit_utils.applications.app_spec.arc56.Arc56Contract) + +#### *static* get_abi_struct_from_abi_tuple(decoded_tuple: Any, struct_fields: list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)], structs: dict[str, list[[StructField](#algokit_utils.applications.app_spec.arc56.StructField)]]) → dict[str, Any] + +#### to_json(indent: int | None = None) → str + +#### dictify() → dict + +#### get_arc56_method(method_name_or_signature: str) → [Method](#algokit_utils.applications.app_spec.arc56.Method) diff --git a/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md b/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md new file mode 100644 index 00000000..7a37b142 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/app_spec/index.md @@ -0,0 +1,6 @@ +# algokit_utils.applications.app_spec + +## Submodules + +* [algokit_utils.applications.app_spec.arc32](arc32/index.md) +* [algokit_utils.applications.app_spec.arc56](arc56/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/applications/enums/index.md b/docs/markdown/autoapi/algokit_utils/applications/enums/index.md new file mode 100644 index 00000000..ac63173b --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/enums/index.md @@ -0,0 +1,72 @@ +# algokit_utils.applications.enums + +## Classes + +| [`OnSchemaBreak`](#algokit_utils.applications.enums.OnSchemaBreak) | Action to take if an Application's schema has breaking changes | +|------------------------------------------------------------------------------|------------------------------------------------------------------| +| [`OnUpdate`](#algokit_utils.applications.enums.OnUpdate) | Action to take if an Application has been updated | +| [`OperationPerformed`](#algokit_utils.applications.enums.OperationPerformed) | Describes the actions taken during deployment | + +## Module Contents + +### *class* algokit_utils.applications.enums.OnSchemaBreak(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Action to take if an Application’s schema has breaking changes + +#### Fail *= 0* + +Fail the deployment + +#### ReplaceApp *= 2* + +Create a new Application and delete the old Application in a single transaction + +#### AppendApp *= 3* + +Create a new Application + +### *class* algokit_utils.applications.enums.OnUpdate(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Action to take if an Application has been updated + +#### Fail *= 0* + +Fail the deployment + +#### UpdateApp *= 1* + +Update the Application with the new approval and clear programs + +#### ReplaceApp *= 2* + +Create a new Application and delete the old Application in a single transaction + +#### AppendApp *= 3* + +Create a new application + +### *class* algokit_utils.applications.enums.OperationPerformed(\*args, \*\*kwds) + +Bases: `enum.Enum` + +Describes the actions taken during deployment + +#### Nothing *= 0* + +An existing Application was found + +#### Create *= 1* + +No existing Application was found, created a new Application + +#### Update *= 2* + +An existing Application was found, but was out of date, updated to latest version + +#### Replace *= 3* + +An existing Application was found, but was out of date, created a new Application and deleted the original diff --git a/docs/markdown/autoapi/algokit_utils/applications/index.md b/docs/markdown/autoapi/algokit_utils/applications/index.md new file mode 100644 index 00000000..8f94c76d --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/applications/index.md @@ -0,0 +1,11 @@ +# algokit_utils.applications + +## Submodules + +* [algokit_utils.applications.abi](abi/index.md) +* [algokit_utils.applications.app_client](app_client/index.md) +* [algokit_utils.applications.app_deployer](app_deployer/index.md) +* [algokit_utils.applications.app_factory](app_factory/index.md) +* [algokit_utils.applications.app_manager](app_manager/index.md) +* [algokit_utils.applications.app_spec](app_spec/index.md) +* [algokit_utils.applications.enums](enums/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md new file mode 100644 index 00000000..7b8afc34 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md @@ -0,0 +1,173 @@ +# algokit_utils.assets.asset_manager + +## Classes + +| [`AccountAssetInformation`](#algokit_utils.assets.asset_manager.AccountAssetInformation) | Information about an account's holding of a particular asset. | +|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [`AssetInformation`](#algokit_utils.assets.asset_manager.AssetInformation) | Information about an Algorand Standard Asset (ASA). | +| [`BulkAssetOptInOutResult`](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult) | Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. | +| [`AssetManager`](#algokit_utils.assets.asset_manager.AssetManager) | A manager for Algorand Standard Assets (ASAs). | + +## Module Contents + +### *class* algokit_utils.assets.asset_manager.AccountAssetInformation + +Information about an account’s holding of a particular asset. + +* **Variables:** + * **asset_id** – The ID of the asset + * **balance** – The amount of the asset held by the account + * **frozen** – Whether the asset is frozen for this account + * **round** – The round this information was retrieved at + +#### asset_id *: int* + +#### balance *: int* + +#### frozen *: bool* + +#### round *: int* + +### *class* algokit_utils.assets.asset_manager.AssetInformation + +Information about an Algorand Standard Asset (ASA). + +* **Variables:** + * **asset_id** – The ID of the asset + * **creator** – The address of the account that created the asset + * **total** – The total amount of the smallest divisible units that were created of the asset + * **decimals** – The amount of decimal places the asset was created with + * **default_frozen** – Whether the asset was frozen by default for all accounts, defaults to None + * **manager** – The address of the optional account that can manage the configuration of the asset and destroy it, + +defaults to None +:ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, +defaults to None +:ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, +defaults to None +:ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, +defaults to None +:ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None +:ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None +:ivar asset_name: The optional name of the asset, defaults to None +:ivar asset_name_b64: The optional name of the asset as bytes, defaults to None +:ivar url: Optional URL where more information about the asset can be retrieved, defaults to None +:ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None +:ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, +defaults to None + +#### asset_id *: int* + +#### creator *: str* + +#### total *: int* + +#### decimals *: int* + +#### default_frozen *: bool | None* *= None* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +#### unit_name *: str | None* *= None* + +#### unit_name_b64 *: bytes | None* *= None* + +#### asset_name *: str | None* *= None* + +#### asset_name_b64 *: bytes | None* *= None* + +#### url *: str | None* *= None* + +#### url_b64 *: bytes | None* *= None* + +#### metadata_hash *: bytes | None* *= None* + +### *class* algokit_utils.assets.asset_manager.BulkAssetOptInOutResult + +Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. + +* **Variables:** + * **asset_id** – The ID of the asset opted into / out of + * **transaction_id** – The transaction ID of the resulting opt in / out + +#### asset_id *: int* + +#### transaction_id *: str* + +### *class* algokit_utils.assets.asset_manager.AssetManager(algod_client: algosdk.v2client.algod.AlgodClient, new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)]) + +A manager for Algorand Standard Assets (ASAs). + +* **Parameters:** + * **algod_client** – An algod client + * **new_group** – A function that creates a new TransactionComposer transaction group + +#### get_by_id(asset_id: int) → [AssetInformation](#algokit_utils.assets.asset_manager.AssetInformation) + +Returns the current asset information for the asset with the given ID. + +* **Parameters:** + **asset_id** – The ID of the asset +* **Returns:** + The asset information + +#### get_account_information(sender: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) | algosdk.atomic_transaction_composer.TransactionSigner, asset_id: int) → [AccountAssetInformation](#algokit_utils.assets.asset_manager.AccountAssetInformation) + +Returns the given sender account’s asset holding for a given asset. + +* **Parameters:** + * **sender** – The address of the sender/account to look up + * **asset_id** – The ID of the asset to return a holding for +* **Returns:** + The account asset holding information + +#### bulk_opt_in(account: str, asset_ids: list[int], signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → list[[BulkAssetOptInOutResult](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult)] + +Opt an account in to a list of Algorand Standard Assets. + +* **Parameters:** + * **account** – The account to opt-in + * **asset_ids** – The list of asset IDs to opt-in to + * **signer** – The signer to use for the transaction, defaults to None + * **rekey_to** – The address to rekey the account to, defaults to None + * **note** – The note to include in the transaction, defaults to None + * **lease** – The lease to include in the transaction, defaults to None + * **static_fee** – The static fee to include in the transaction, defaults to None + * **extra_fee** – The extra fee to include in the transaction, defaults to None + * **max_fee** – The maximum fee to include in the transaction, defaults to None + * **validity_window** – The validity window to include in the transaction, defaults to None + * **first_valid_round** – The first valid round to include in the transaction, defaults to None + * **last_valid_round** – The last valid round to include in the transaction, defaults to None + * **send_params** – The send parameters to use for the transaction, defaults to None +* **Returns:** + An array of records matching asset ID to transaction ID of the opt in + +#### bulk_opt_out(\*, account: str, asset_ids: list[int], ensure_zero_balance: bool = True, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → list[[BulkAssetOptInOutResult](#algokit_utils.assets.asset_manager.BulkAssetOptInOutResult)] + +Opt an account out of a list of Algorand Standard Assets. + +* **Parameters:** + * **account** – The account to opt-out + * **asset_ids** – The list of asset IDs to opt-out of + * **ensure_zero_balance** – Whether to check if the account has a zero balance first, defaults to True + * **signer** – The signer to use for the transaction, defaults to None + * **rekey_to** – The address to rekey the account to, defaults to None + * **note** – The note to include in the transaction, defaults to None + * **lease** – The lease to include in the transaction, defaults to None + * **static_fee** – The static fee to include in the transaction, defaults to None + * **extra_fee** – The extra fee to include in the transaction, defaults to None + * **max_fee** – The maximum fee to include in the transaction, defaults to None + * **validity_window** – The validity window to include in the transaction, defaults to None + * **first_valid_round** – The first valid round to include in the transaction, defaults to None + * **last_valid_round** – The last valid round to include in the transaction, defaults to None + * **send_params** – The send parameters to use for the transaction, defaults to None +* **Raises:** + **ValueError** – If ensure_zero_balance is True and account has non-zero balance or is not opted in +* **Returns:** + An array of records matching asset ID to transaction ID of the opt out diff --git a/docs/markdown/autoapi/algokit_utils/assets/index.md b/docs/markdown/autoapi/algokit_utils/assets/index.md new file mode 100644 index 00000000..5091632c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/assets/index.md @@ -0,0 +1,5 @@ +# algokit_utils.assets + +## Submodules + +* [algokit_utils.assets.asset_manager](asset_manager/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md new file mode 100644 index 00000000..55dfc287 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md @@ -0,0 +1,367 @@ +# algokit_utils.clients.client_manager + +## Classes + +| [`AlgoSdkClients`](#algokit_utils.clients.client_manager.AlgoSdkClients) | Container for Algorand SDK client instances. | +|----------------------------------------------------------------------------|------------------------------------------------| +| [`NetworkDetail`](#algokit_utils.clients.client_manager.NetworkDetail) | Details about an Algorand network. | +| [`ClientManager`](#algokit_utils.clients.client_manager.ClientManager) | Manager for Algorand SDK clients. | + +## Module Contents + +### *class* algokit_utils.clients.client_manager.AlgoSdkClients(algod: algosdk.v2client.algod.AlgodClient, indexer: algosdk.v2client.indexer.IndexerClient | None = None, kmd: algosdk.kmd.KMDClient | None = None) + +Container for Algorand SDK client instances. + +Holds references to Algod, Indexer and KMD clients. + +* **Parameters:** + * **algod** – Algod client instance + * **indexer** – Optional Indexer client instance + * **kmd** – Optional KMD client instance + +#### algod + +#### indexer *= None* + +#### kmd *= None* + +### *class* algokit_utils.clients.client_manager.NetworkDetail + +Details about an Algorand network. + +Contains network type flags and genesis information. + +#### is_testnet *: bool* + +#### is_mainnet *: bool* + +#### is_localnet *: bool* + +#### genesis_id *: str* + +#### genesis_hash *: str* + +### *class* algokit_utils.clients.client_manager.ClientManager(clients_or_configs: [algokit_utils.models.network.AlgoClientConfigs](../../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) | [AlgoSdkClients](#algokit_utils.clients.client_manager.AlgoSdkClients), algorand_client: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) + +Manager for Algorand SDK clients. + +Provides access to Algod, Indexer and KMD clients and helper methods for working with them. + +* **Parameters:** + * **clients_or_configs** – Either client instances or client configurations + * **algorand_client** – AlgorandClient instance + +#### *property* algod *: algosdk.v2client.algod.AlgodClient* + +Returns an algosdk Algod API client. + +* **Returns:** + Algod client instance + +#### *property* indexer *: algosdk.v2client.indexer.IndexerClient* + +Returns an algosdk Indexer API client. + +* **Raises:** + **ValueError** – If no Indexer client is configured +* **Returns:** + Indexer client instance + +#### *property* indexer_if_present *: algosdk.v2client.indexer.IndexerClient | None* + +Returns the Indexer client if configured, otherwise None. + +* **Returns:** + Indexer client instance or None + +#### *property* kmd *: algosdk.kmd.KMDClient* + +Returns an algosdk KMD API client. + +* **Raises:** + **ValueError** – If no KMD client is configured +* **Returns:** + KMD client instance + +#### network() → [NetworkDetail](#algokit_utils.clients.client_manager.NetworkDetail) + +Get details about the connected Algorand network. + +* **Returns:** + Network details including type and genesis information + +#### is_localnet() → bool + +Check if connected to a local network. + +* **Returns:** + True if connected to a local network + +#### is_testnet() → bool + +Check if connected to TestNet. + +* **Returns:** + True if connected to TestNet + +#### is_mainnet() → bool + +Check if connected to MainNet. + +* **Returns:** + True if connected to MainNet + +#### get_testnet_dispenser(auth_token: str | None = None, request_timeout: int | None = None) → [algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient](../dispenser_api_client/index.md#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient) + +Get a TestNet dispenser API client. + +* **Parameters:** + * **auth_token** – Optional authentication token + * **request_timeout** – Optional request timeout in seconds +* **Returns:** + TestNet dispenser client instance + +#### get_app_factory(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, version: str | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → [algokit_utils.applications.app_factory.AppFactory](../../applications/app_factory/index.md#algokit_utils.applications.app_factory.AppFactory) + +Get an application factory for deploying smart contracts. + +* **Parameters:** + * **app_spec** – Application specification + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **version** – Optional version string + * **compilation_params** – Optional compilation parameters +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application factory instance + +#### get_app_client_by_id(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client for an existing application by ID. + +* **Parameters:** + * **app_spec** – Application specification + * **app_id** – Application ID + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application client instance + +#### get_app_client_by_network(app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client for an existing application by network. + +* **Parameters:** + * **app_spec** – Application specification + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Application client instance + +#### get_app_client_by_creator_and_name(creator_address: str, app_name: str, app_spec: [algokit_utils.applications.app_spec.arc56.Arc56Contract](../../applications/app_spec/arc56/index.md#algokit_utils.applications.app_spec.arc56.Arc56Contract) | algokit_utils._legacy_v2.application_specification.ApplicationSpecification | str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → [algokit_utils.applications.app_client.AppClient](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClient) + +Get an application client by creator address and name. + +* **Parameters:** + * **creator_address** – Creator address + * **app_name** – Application name + * **app_spec** – Application specification + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Returns:** + Application client instance + +#### *static* get_algod_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.algod.AlgodClient + +Get an Algod client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + Algod client instance + +#### *static* get_algod_client_from_environment() → algosdk.v2client.algod.AlgodClient + +Get an Algod client from environment variables. + +* **Returns:** + Algod client instance + +#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.kmd.KMDClient + +Get a KMD client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + KMD client instance + +#### *static* get_kmd_client_from_environment() → algosdk.kmd.KMDClient + +Get a KMD client from environment variables. + +* **Returns:** + KMD client instance + +#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.indexer.IndexerClient + +Get an Indexer client from config or environment. + +* **Parameters:** + **config** – Optional client configuration +* **Returns:** + Indexer client instance + +#### *static* get_indexer_client_from_environment() → algosdk.v2client.indexer.IndexerClient + +Get an Indexer client from environment variables. + +* **Returns:** + Indexer client instance + +#### *static* genesis_id_is_localnet(genesis_id: str | None) → bool + +Check if a genesis ID indicates a local network. + +* **Parameters:** + **genesis_id** – Genesis ID to check +* **Returns:** + True if genesis ID indicates a local network + +#### get_typed_app_client_by_creator_and_name(typed_client: type[TypedAppClientT], \*, creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None) → TypedAppClientT + +Get a typed application client by creator address and name. + +* **Parameters:** + * **typed_client** – Typed client class + * **creator_address** – Creator address + * **app_name** – Application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **ignore_cache** – Optional flag to ignore cache + * **app_lookup_cache** – Optional app lookup cache +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application client instance + +#### get_typed_app_client_by_id(typed_client: type[TypedAppClientT], \*, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → TypedAppClientT + +Get a typed application client by ID. + +* **Parameters:** + * **typed_client** – Typed client class + * **app_id** – Application ID + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application client instance + +#### get_typed_app_client_by_network(typed_client: type[TypedAppClientT], \*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) → TypedAppClientT + +Returns a new typed client, resolves the app ID for the current network. + +Uses pre-determined network-specific app IDs specified in the ARC-56 app spec. +If no IDs are in the app spec or the network isn’t recognised, an error is thrown. + +* **Parameters:** + * **typed_client** – The typed client class to instantiate + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **approval_source_map** – Optional approval program source map + * **clear_source_map** – Optional clear program source map +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + The typed client instance + +#### get_typed_app_factory(typed_factory: type[TypedFactoryT], \*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, version: str | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → TypedFactoryT + +Get a typed application factory. + +* **Parameters:** + * **typed_factory** – Typed factory class + * **app_name** – Optional application name + * **default_sender** – Optional default sender address + * **default_signer** – Optional default transaction signer + * **version** – Optional version string + * **compilation_params** – Optional compilation parameters +* **Raises:** + **ValueError** – If no Algorand client is configured +* **Returns:** + Typed application factory instance + +#### *static* get_config_from_environment_or_localnet() → [algokit_utils.models.network.AlgoClientConfigs](../../models/network/index.md#algokit_utils.models.network.AlgoClientConfigs) + +Retrieve client configuration from environment variables or fallback to localnet defaults. + +If ALGOD_SERVER is set in environment variables, it will use environment configuration, +otherwise it will use default localnet configuration. + +* **Returns:** + Configuration for algod, indexer, and optionally kmd + +#### *static* get_default_localnet_config(config_or_port: Literal['algod', 'indexer', 'kmd'] | int) → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Get default configuration for local network services. + +* **Parameters:** + **config_or_port** – Service name or port number +* **Returns:** + Client configuration for local network + +#### *static* get_algod_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the algod configuration from environment variables. +Will raise an error if ALGOD_SERVER environment variable is not set + +* **Returns:** + Algod client configuration + +#### *static* get_indexer_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the indexer configuration from environment variables. +Will raise an error if INDEXER_SERVER environment variable is not set + +* **Returns:** + Indexer client configuration + +#### *static* get_kmd_config_from_environment() → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Retrieve the kmd configuration from environment variables. + +* **Returns:** + KMD client configuration + +#### *static* get_algonode_config(network: Literal['testnet', 'mainnet'], config: Literal['algod', 'indexer']) → [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) + +Returns the Algorand configuration to point to the free tier of the AlgoNode service. + +* **Parameters:** + * **network** – Which network to connect to - TestNet or MainNet + * **config** – Which algod config to return - Algod or Indexer +* **Returns:** + Configuration for the specified network and service diff --git a/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md b/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md new file mode 100644 index 00000000..c1f83d93 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/dispenser_api_client/index.md @@ -0,0 +1,82 @@ +# algokit_utils.clients.dispenser_api_client + +## Attributes + +| [`DISPENSER_ASSETS`](#algokit_utils.clients.dispenser_api_client.DISPENSER_ASSETS) | | +|--------------------------------------------------------------------------------------------------------|----| +| [`DISPENSER_REQUEST_TIMEOUT`](#algokit_utils.clients.dispenser_api_client.DISPENSER_REQUEST_TIMEOUT) | | +| [`DISPENSER_ACCESS_TOKEN_KEY`](#algokit_utils.clients.dispenser_api_client.DISPENSER_ACCESS_TOKEN_KEY) | | + +## Classes + +| [`DispenserApiConfig`](#algokit_utils.clients.dispenser_api_client.DispenserApiConfig) | | +|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`DispenserAssetName`](#algokit_utils.clients.dispenser_api_client.DispenserAssetName) | Enum where members are also (and must be) ints | +| [`DispenserAsset`](#algokit_utils.clients.dispenser_api_client.DispenserAsset) | | +| [`DispenserFundResponse`](#algokit_utils.clients.dispenser_api_client.DispenserFundResponse) | | +| [`DispenserLimitResponse`](#algokit_utils.clients.dispenser_api_client.DispenserLimitResponse) | | +| [`TestNetDispenserApiClient`](#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient) | Client for interacting with the [AlgoKit TestNet Dispenser API]([https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md)). | + +## Module Contents + +### *class* algokit_utils.clients.dispenser_api_client.DispenserApiConfig + +#### BASE_URL *= 'https://api.dispenser.algorandfoundation.tools'* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserAssetName + +Bases: `enum.IntEnum` + +Enum where members are also (and must be) ints + +#### ALGO *= 0* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserAsset + +#### asset_id *: int* + +#### decimals *: int* + +#### description *: str* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserFundResponse + +#### tx_id *: str* + +#### amount *: int* + +### *class* algokit_utils.clients.dispenser_api_client.DispenserLimitResponse + +#### amount *: int* + +### algokit_utils.clients.dispenser_api_client.DISPENSER_ASSETS + +### algokit_utils.clients.dispenser_api_client.DISPENSER_REQUEST_TIMEOUT *= 15* + +### algokit_utils.clients.dispenser_api_client.DISPENSER_ACCESS_TOKEN_KEY *= 'ALGOKIT_DISPENSER_ACCESS_TOKEN'* + +### *class* algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient(auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT) + +Client for interacting with the [AlgoKit TestNet Dispenser API]([https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md)). +To get started create a new access token via algokit dispenser login –ci +and pass it to the client constructor as auth_token. +Alternatively set the access token as environment variable ALGOKIT_DISPENSER_ACCESS_TOKEN, +and it will be auto loaded. If both are set, the constructor argument takes precedence. + +Default request timeout is 15 seconds. Modify by passing request_timeout to the constructor. + +#### auth_token *: str* + +#### request_timeout *= 15* + +#### fund(address: str, amount: int, asset_id: int) → [DispenserFundResponse](#algokit_utils.clients.dispenser_api_client.DispenserFundResponse) + +Fund an account with Algos from the dispenser API + +#### refund(refund_txn_id: str) → None + +Register a refund for a transaction with the dispenser API + +#### get_limit(address: str) → [DispenserLimitResponse](#algokit_utils.clients.dispenser_api_client.DispenserLimitResponse) + +Get current limit for an account with Algos from the dispenser API diff --git a/docs/markdown/autoapi/algokit_utils/clients/index.md b/docs/markdown/autoapi/algokit_utils/clients/index.md new file mode 100644 index 00000000..8ae2dbc7 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/clients/index.md @@ -0,0 +1,6 @@ +# algokit_utils.clients + +## Submodules + +* [algokit_utils.clients.client_manager](client_manager/index.md) +* [algokit_utils.clients.dispenser_api_client](dispenser_api_client/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/config/index.md b/docs/markdown/autoapi/algokit_utils/config/index.md new file mode 100644 index 00000000..fa34f032 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/config/index.md @@ -0,0 +1,102 @@ +# algokit_utils.config + +## Attributes + +| [`ALGOKIT_PROJECT_ROOT`](#algokit_utils.config.ALGOKIT_PROJECT_ROOT) | | +|----------------------------------------------------------------------------|----| +| [`ALGOKIT_CONFIG_FILENAME`](#algokit_utils.config.ALGOKIT_CONFIG_FILENAME) | | +| [`config`](#algokit_utils.config.config) | | + +## Classes + +| [`AlgoKitLogger`](#algokit_utils.config.AlgoKitLogger) | | +|------------------------------------------------------------|----------------------------------------------------------------------------| +| [`UpdatableConfig`](#algokit_utils.config.UpdatableConfig) | Class to manage and update configuration settings for the AlgoKit project. | + +## Module Contents + +### algokit_utils.config.ALGOKIT_PROJECT_ROOT + +### algokit_utils.config.ALGOKIT_CONFIG_FILENAME *= '.algokit.toml'* + +### *class* algokit_utils.config.AlgoKitLogger + +#### error(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an error message, optionally suppressing output + +#### exception(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an exception message, optionally suppressing output + +#### warning(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a warning message, optionally suppressing output + +#### info(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log an info message, optionally suppressing output + +#### debug(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a debug message, optionally suppressing output + +#### verbose(message: str, \*args: Any, suppress_log: bool = False, \*\*kwargs: Any) → None + +Log a verbose message (maps to debug), optionally suppressing output + +### *class* algokit_utils.config.UpdatableConfig + +Class to manage and update configuration settings for the AlgoKit project. + +Attributes: +: debug (bool): Indicates whether debug mode is enabled. + project_root (Path | None): The path to the project root directory. + trace_all (bool): Indicates whether to trace all operations. + trace_buffer_size_mb (int): The size of the trace buffer in megabytes. + max_search_depth (int): The maximum depth to search for a specific file. + populate_app_call_resources (bool): Indicates whether to populate app call resources. + +#### *property* logger *: [AlgoKitLogger](#algokit_utils.config.AlgoKitLogger)* + +#### *property* debug *: bool* + +Returns the debug status. + +#### *property* project_root *: pathlib.Path | None* + +Returns the project root path. + +#### *property* trace_all *: bool* + +Indicates whether to store simulation traces for all operations. + +#### *property* trace_buffer_size_mb *: int | float* + +Returns the size of the trace buffer in megabytes. + +#### *property* populate_app_call_resource *: bool* + +#### with_debug(func: collections.abc.Callable[[], str | None]) → None + +Executes a function with debug mode temporarily enabled. + +#### configure(\*, debug: bool | None = None, project_root: pathlib.Path | None = None, trace_all: bool = False, trace_buffer_size_mb: float = 256, max_search_depth: int = 10, populate_app_call_resources: bool = False) → None + +Configures various settings for the application. +Please note, when project_root is not specified, by default config will attempt to find the algokit.toml by +scanning the parent directories according to the max_search_depth parameter. +Alternatively value can also be set via the ALGOKIT_PROJECT_ROOT environment variable. +If you are executing the config from an algokit compliant project, you can simply call +config.configure(debug=True). + +* **Parameters:** + * **debug** – Indicates whether debug mode is enabled. + * **project_root** – The path to the project root directory. Defaults to None. + * **trace_all** – Indicates whether to trace all operations. Defaults to False. Which implies that + only the operations that are failed will be traced by default. + * **trace_buffer_size_mb** – The size of the trace buffer in megabytes. Defaults to 256 + * **max_search_depth** – The maximum depth to search for a specific file. Defaults to 10 + * **populate_app_call_resources** – Indicates whether to populate app call resources. Defaults to False + +### algokit_utils.config.config diff --git a/docs/markdown/autoapi/algokit_utils/errors/index.md b/docs/markdown/autoapi/algokit_utils/errors/index.md new file mode 100644 index 00000000..47a58848 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/errors/index.md @@ -0,0 +1,5 @@ +# algokit_utils.errors + +## Submodules + +* [algokit_utils.errors.logic_error](logic_error/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md b/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md new file mode 100644 index 00000000..f5039daf --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/errors/logic_error/index.md @@ -0,0 +1,76 @@ +# algokit_utils.errors.logic_error + +## Exceptions + +| [`LogicError`](#algokit_utils.errors.logic_error.LogicError) | Common base class for all non-exit exceptions. | +|----------------------------------------------------------------|--------------------------------------------------| + +## Classes + +| [`LogicErrorData`](#algokit_utils.errors.logic_error.LogicErrorData) | dict() -> new empty dictionary | +|------------------------------------------------------------------------|----------------------------------| + +## Functions + +| [`parse_logic_error`](#algokit_utils.errors.logic_error.parse_logic_error)(→ LogicErrorData | None) | | +|-------------------------------------------------------------------------------------------------------|----| + +## Module Contents + +### *class* algokit_utils.errors.logic_error.LogicErrorData + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### transaction_id *: str* + +#### message *: str* + +#### pc *: int* + +### algokit_utils.errors.logic_error.parse_logic_error(error_str: str) → [LogicErrorData](#algokit_utils.errors.logic_error.LogicErrorData) | None + +### *exception* algokit_utils.errors.logic_error.LogicError(\*, logic_error_str: str, program: str, source_map: AlgoSourceMap | None, transaction_id: str, message: str, pc: int, logic_error: Exception | None = None, traces: list[[algokit_utils.models.simulate.SimulationTrace](../../models/simulate/index.md#algokit_utils.models.simulate.SimulationTrace)] | None = None, get_line_for_pc: collections.abc.Callable[[int], int | None] | None = None) + +Bases: `Exception` + +Common base class for all non-exit exceptions. + +#### logic_error *= None* + +#### logic_error_str + +#### source_map + +#### lines + +#### transaction_id + +#### message + +#### pc + +#### traces *= None* + +#### line_no + +#### trace(lines: int = 5) → str diff --git a/docs/markdown/autoapi/algokit_utils/index.md b/docs/markdown/autoapi/algokit_utils/index.md new file mode 100644 index 00000000..1b2f3707 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/index.md @@ -0,0 +1,24 @@ +# algokit_utils + +AlgoKit Python Utilities - a set of utilities for building solutions on Algorand + +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + +> from algokit_utils.accounts import KmdAccountManager +> from algokit_utils.applications import AppClient +> from algokit_utils.applications.app_spec import Arc52Contract +> etc. + +## Submodules + +* [algokit_utils.accounts](accounts/index.md) +* [algokit_utils.algorand](algorand/index.md) +* [algokit_utils.applications](applications/index.md) +* [algokit_utils.assets](assets/index.md) +* [algokit_utils.clients](clients/index.md) +* [algokit_utils.config](config/index.md) +* [algokit_utils.errors](errors/index.md) +* [algokit_utils.models](models/index.md) +* [algokit_utils.protocols](protocols/index.md) +* [algokit_utils.transactions](transactions/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/models/account/index.md b/docs/markdown/autoapi/algokit_utils/models/account/index.md new file mode 100644 index 00000000..6f0eca22 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/account/index.md @@ -0,0 +1,120 @@ +# algokit_utils.models.account + +## Attributes + +| [`DISPENSER_ACCOUNT_NAME`](#algokit_utils.models.account.DISPENSER_ACCOUNT_NAME) | | +|------------------------------------------------------------------------------------|----| + +## Classes + +| [`TransactionSignerAccount`](#algokit_utils.models.account.TransactionSignerAccount) | A basic transaction signer account. | +|----------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| [`SigningAccount`](#algokit_utils.models.account.SigningAccount) | Holds the private key and address for an account. | +| [`MultisigMetadata`](#algokit_utils.models.account.MultisigMetadata) | Metadata for a multisig account. | +| [`MultiSigAccount`](#algokit_utils.models.account.MultiSigAccount) | Account wrapper that supports partial or full multisig signing. | + +## Module Contents + +### algokit_utils.models.account.DISPENSER_ACCOUNT_NAME *= 'DISPENSER'* + +### *class* algokit_utils.models.account.TransactionSignerAccount + +A basic transaction signer account. + +#### address *: str* + +#### signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +### *class* algokit_utils.models.account.SigningAccount + +Holds the private key and address for an account. + +Provides access to the account’s private key, address, public key and transaction signer. + +#### private_key *: str* + +Base64 encoded private key + +#### address *: str* *= ''* + +Address for this account + +#### *property* public_key *: bytes* + +The public key for this account. + +* **Returns:** + The public key as bytes + +#### *property* signer *: algosdk.atomic_transaction_composer.AccountTransactionSigner* + +Get an AccountTransactionSigner for this account. + +* **Returns:** + A transaction signer for this account + +#### *static* new_account() → [SigningAccount](#algokit_utils.models.account.SigningAccount) + +Create a new random account. + +* **Returns:** + A new Account instance + +### *class* algokit_utils.models.account.MultisigMetadata + +Metadata for a multisig account. + +Contains the version, threshold and addresses for a multisig account. + +#### version *: int* + +#### threshold *: int* + +#### addresses *: list[str]* + +### *class* algokit_utils.models.account.MultiSigAccount(multisig_params: [MultisigMetadata](#algokit_utils.models.account.MultisigMetadata), signing_accounts: list[[SigningAccount](#algokit_utils.models.account.SigningAccount)]) + +Account wrapper that supports partial or full multisig signing. + +Provides functionality to manage and sign transactions for a multisig account. + +* **Parameters:** + * **multisig_params** – The parameters for the multisig account + * **signing_accounts** – The list of accounts that can sign + +#### *property* params *: [MultisigMetadata](#algokit_utils.models.account.MultisigMetadata)* + +Get the parameters for the multisig account. + +* **Returns:** + The multisig account parameters + +#### *property* signing_accounts *: list[[SigningAccount](#algokit_utils.models.account.SigningAccount)]* + +Get the list of accounts that are present to sign. + +* **Returns:** + The list of signing accounts + +#### *property* address *: str* + +Get the address of the multisig account. + +* **Returns:** + The multisig account address + +#### *property* signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +Get the transaction signer for this multisig account. + +* **Returns:** + The multisig transaction signer + +#### sign(transaction: algosdk.transaction.Transaction) → algosdk.transaction.MultisigTransaction + +Sign the given transaction with all present signers. + +* **Parameters:** + **transaction** – Either a transaction object or a raw, partially signed transaction +* **Returns:** + The transaction signed by the present signers diff --git a/docs/markdown/autoapi/algokit_utils/models/amount/index.md b/docs/markdown/autoapi/algokit_utils/models/amount/index.md new file mode 100644 index 00000000..89160023 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/amount/index.md @@ -0,0 +1,108 @@ +# algokit_utils.models.amount + +## Classes + +| [`AlgoAmount`](#algokit_utils.models.amount.AlgoAmount) | Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. | +|-----------------------------------------------------------|--------------------------------------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.models.amount.AlgoAmount(amount: dict[str, int | decimal.Decimal]) + +Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. + +* **Parameters:** + **amount** – A dictionary containing either algos, algo, microAlgos, or microAlgo as key + and their corresponding value as an integer or Decimal. +* **Raises:** + **ValueError** – If an invalid amount format is provided. +* **Example:** + +```pycon +>>> amount = AlgoAmount({"algos": 1}) +>>> amount = AlgoAmount({"microAlgos": 1_000_000}) +``` + +#### *property* micro_algos *: int* + +Return the amount as a number in µAlgo. + +* **Returns:** + The amount in µAlgo. + +#### *property* micro_algo *: int* + +Return the amount as a number in µAlgo. + +* **Returns:** + The amount in µAlgo. + +#### *property* algos *: decimal.Decimal* + +Return the amount as a number in Algo. + +* **Returns:** + The amount in Algo. + +#### *property* algo *: decimal.Decimal* + +Return the amount as a number in Algo. + +* **Returns:** + The amount in Algo. + +#### *static* from_algos(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of Algo. + +* **Parameters:** + **amount** – The amount in Algo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_algos(1) +``` + +#### *static* from_algo(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of Algo. + +* **Parameters:** + **amount** – The amount in Algo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_algo(1) +``` + +#### *static* from_micro_algos(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of µAlgo. + +* **Parameters:** + **amount** – The amount in µAlgo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_micro_algos(1_000_000) +``` + +#### *static* from_micro_algo(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) + +Create an AlgoAmount object representing the given number of µAlgo. + +* **Parameters:** + **amount** – The amount in µAlgo. +* **Returns:** + An AlgoAmount instance. +* **Example:** + +```pycon +>>> amount = AlgoAmount.from_micro_algo(1_000_000) +``` diff --git a/docs/markdown/autoapi/algokit_utils/models/application/index.md b/docs/markdown/autoapi/algokit_utils/models/application/index.md new file mode 100644 index 00000000..1211df33 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/application/index.md @@ -0,0 +1,72 @@ +# algokit_utils.models.application + +## Classes + +| [`AppState`](#algokit_utils.models.application.AppState) | | +|----------------------------------------------------------------------------------|----| +| [`AppInformation`](#algokit_utils.models.application.AppInformation) | | +| [`CompiledTeal`](#algokit_utils.models.application.CompiledTeal) | | +| [`AppCompilationResult`](#algokit_utils.models.application.AppCompilationResult) | | +| [`AppSourceMaps`](#algokit_utils.models.application.AppSourceMaps) | | + +## Module Contents + +### *class* algokit_utils.models.application.AppState + +#### key_raw *: bytes* + +#### key_base64 *: str* + +#### value_raw *: bytes | None* + +#### value_base64 *: str | None* + +#### value *: str | int* + +### *class* algokit_utils.models.application.AppInformation + +#### app_id *: int* + +#### app_address *: str* + +#### approval_program *: bytes* + +#### clear_state_program *: bytes* + +#### creator *: str* + +#### global_state *: dict[str, [AppState](#algokit_utils.models.application.AppState)]* + +#### local_ints *: int* + +#### local_byte_slices *: int* + +#### global_ints *: int* + +#### global_byte_slices *: int* + +#### extra_program_pages *: int | None* + +### *class* algokit_utils.models.application.CompiledTeal + +#### teal *: str* + +#### compiled *: str* + +#### compiled_hash *: str* + +#### compiled_base64_to_bytes *: bytes* + +#### source_map *: algosdk.source_map.SourceMap | None* + +### *class* algokit_utils.models.application.AppCompilationResult + +#### compiled_approval *: [CompiledTeal](#algokit_utils.models.application.CompiledTeal)* + +#### compiled_clear *: [CompiledTeal](#algokit_utils.models.application.CompiledTeal)* + +### *class* algokit_utils.models.application.AppSourceMaps + +#### approval_source_map *: algosdk.source_map.SourceMap | None* *= None* + +#### clear_source_map *: algosdk.source_map.SourceMap | None* *= None* diff --git a/docs/markdown/autoapi/algokit_utils/models/index.md b/docs/markdown/autoapi/algokit_utils/models/index.md new file mode 100644 index 00000000..e0f53185 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/index.md @@ -0,0 +1,11 @@ +# algokit_utils.models + +## Submodules + +* [algokit_utils.models.account](account/index.md) +* [algokit_utils.models.amount](amount/index.md) +* [algokit_utils.models.application](application/index.md) +* [algokit_utils.models.network](network/index.md) +* [algokit_utils.models.simulate](simulate/index.md) +* [algokit_utils.models.state](state/index.md) +* [algokit_utils.models.transaction](transaction/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/models/network/index.md b/docs/markdown/autoapi/algokit_utils/models/network/index.md new file mode 100644 index 00000000..4d94f90c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/network/index.md @@ -0,0 +1,32 @@ +# algokit_utils.models.network + +## Classes + +| [`AlgoClientNetworkConfig`](#algokit_utils.models.network.AlgoClientNetworkConfig) | Connection details for connecting to an {py:class}\`algosdk.v2client.algod.AlgodClient\` or | +|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [`AlgoClientConfigs`](#algokit_utils.models.network.AlgoClientConfigs) | | + +## Module Contents + +### *class* algokit_utils.models.network.AlgoClientNetworkConfig + +Connection details for connecting to an {py:class}\`algosdk.v2client.algod.AlgodClient\` or +{py:class}\`algosdk.v2client.indexer.IndexerClient\` + +#### server *: str* + +URL for the service e.g. http://localhost:4001 or https://testnet-api.algonode.cloud + +#### token *: str | None* *= None* + +API Token to authenticate with the service + +#### port *: str | int | None* *= None* + +### *class* algokit_utils.models.network.AlgoClientConfigs + +#### algod_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig)* + +#### indexer_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig) | None* + +#### kmd_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig) | None* diff --git a/docs/markdown/autoapi/algokit_utils/models/simulate/index.md b/docs/markdown/autoapi/algokit_utils/models/simulate/index.md new file mode 100644 index 00000000..7c1fec4c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/simulate/index.md @@ -0,0 +1,18 @@ +# algokit_utils.models.simulate + +## Classes + +| [`SimulationTrace`](#algokit_utils.models.simulate.SimulationTrace) | | +|-----------------------------------------------------------------------|----| + +## Module Contents + +### *class* algokit_utils.models.simulate.SimulationTrace + +#### app_budget_added *: int | None* + +#### app_budget_consumed *: int | None* + +#### failure_message *: str | None* + +#### exec_trace *: dict[str, object]* diff --git a/docs/markdown/autoapi/algokit_utils/models/state/index.md b/docs/markdown/autoapi/algokit_utils/models/state/index.md new file mode 100644 index 00000000..61e3c040 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/state/index.md @@ -0,0 +1,55 @@ +# algokit_utils.models.state + +## Attributes + +| [`TealTemplateParams`](#algokit_utils.models.state.TealTemplateParams) | | +|--------------------------------------------------------------------------|----| +| [`BoxIdentifier`](#algokit_utils.models.state.BoxIdentifier) | | + +## Classes + +| [`BoxName`](#algokit_utils.models.state.BoxName) | | +|------------------------------------------------------------|-----------------------------------------------------------------------| +| [`BoxValue`](#algokit_utils.models.state.BoxValue) | | +| [`DataTypeFlag`](#algokit_utils.models.state.DataTypeFlag) | Enum where members are also (and must be) ints | +| [`BoxReference`](#algokit_utils.models.state.BoxReference) | Represents a box reference with a foreign app index and the box name. | + +## Module Contents + +### *class* algokit_utils.models.state.BoxName + +#### name *: str* + +#### name_raw *: bytes* + +#### name_base64 *: str* + +### *class* algokit_utils.models.state.BoxValue + +#### name *: [BoxName](#algokit_utils.models.state.BoxName)* + +#### value *: bytes* + +### *class* algokit_utils.models.state.DataTypeFlag + +Bases: `enum.IntEnum` + +Enum where members are also (and must be) ints + +#### BYTES *= 1* + +#### UINT *= 2* + +### algokit_utils.models.state.TealTemplateParams *: TypeAlias* *= Mapping[str, str | int | bytes] | dict[str, str | int | bytes]* + +### algokit_utils.models.state.BoxIdentifier *: TypeAlias* *= str | bytes | AccountTransactionSigner* + +### *class* algokit_utils.models.state.BoxReference(app_id: int, name: bytes | str) + +Bases: `algosdk.box_reference.BoxReference` + +Represents a box reference with a foreign app index and the box name. + +Args: +: app_index (int): index of the application in the foreign app array + name (bytes): key for the box in bytes diff --git a/docs/markdown/autoapi/algokit_utils/models/transaction/index.md b/docs/markdown/autoapi/algokit_utils/models/transaction/index.md new file mode 100644 index 00000000..ad9cbd9d --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/models/transaction/index.md @@ -0,0 +1,89 @@ +# algokit_utils.models.transaction + +## Attributes + +| [`Arc2TransactionNote`](#algokit_utils.models.transaction.Arc2TransactionNote) | | +|----------------------------------------------------------------------------------|----| +| [`TransactionNoteData`](#algokit_utils.models.transaction.TransactionNoteData) | | +| [`TransactionNote`](#algokit_utils.models.transaction.TransactionNote) | | + +## Classes + +| [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) | Base ARC-0002 transaction note structure | +|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| [`StringFormatArc2Note`](#algokit_utils.models.transaction.StringFormatArc2Note) | ARC-0002 note for string-based formats (m/b/u) | +| [`JsonFormatArc2Note`](#algokit_utils.models.transaction.JsonFormatArc2Note) | ARC-0002 note for JSON format | +| [`TransactionWrapper`](#algokit_utils.models.transaction.TransactionWrapper) | Wrapper around algosdk.transaction.Transaction with optional property validators | +| [`SendParams`](#algokit_utils.models.transaction.SendParams) | Parameters for sending a transaction | + +## Module Contents + +### *class* algokit_utils.models.transaction.BaseArc2Note + +Bases: `TypedDict` + +Base ARC-0002 transaction note structure + +#### dapp_name *: str* + +### *class* algokit_utils.models.transaction.StringFormatArc2Note + +Bases: [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) + +ARC-0002 note for string-based formats (m/b/u) + +#### format *: Literal['m', 'b', 'u']* + +#### data *: str* + +### *class* algokit_utils.models.transaction.JsonFormatArc2Note + +Bases: [`BaseArc2Note`](#algokit_utils.models.transaction.BaseArc2Note) + +ARC-0002 note for JSON format + +#### format *: Literal['j']* + +#### data *: str | dict[str, Any] | list[Any] | int | None* + +### algokit_utils.models.transaction.Arc2TransactionNote + +### algokit_utils.models.transaction.TransactionNoteData + +### algokit_utils.models.transaction.TransactionNote + +### *class* algokit_utils.models.transaction.TransactionWrapper(transaction: algosdk.transaction.Transaction) + +Bases: `algosdk.transaction.Transaction` + +Wrapper around algosdk.transaction.Transaction with optional property validators + +#### *property* raw *: algosdk.transaction.Transaction* + +#### *property* payment *: algosdk.transaction.PaymentTxn* + +#### *property* keyreg *: algosdk.transaction.KeyregTxn* + +#### *property* asset_config *: algosdk.transaction.AssetConfigTxn* + +#### *property* asset_transfer *: algosdk.transaction.AssetTransferTxn* + +#### *property* asset_freeze *: algosdk.transaction.AssetFreezeTxn* + +#### *property* application_call *: algosdk.transaction.ApplicationCallTxn* + +#### *property* state_proof *: algosdk.transaction.StateProofTxn* + +### *class* algokit_utils.models.transaction.SendParams + +Bases: `TypedDict` + +Parameters for sending a transaction + +#### max_rounds_to_wait *: int | None* + +#### suppress_log *: bool | None* + +#### populate_app_call_resources *: bool | None* + +#### cover_app_call_inner_transaction_fees *: bool | None* diff --git a/docs/markdown/autoapi/algokit_utils/protocols/account/index.md b/docs/markdown/autoapi/algokit_utils/protocols/account/index.md new file mode 100644 index 00000000..190f37ab --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/account/index.md @@ -0,0 +1,23 @@ +# algokit_utils.protocols.account + +## Classes + +| [`TransactionSignerAccountProtocol`](#algokit_utils.protocols.account.TransactionSignerAccountProtocol) | An account that has a transaction signer. | +|-----------------------------------------------------------------------------------------------------------|---------------------------------------------| + +## Module Contents + +### *class* algokit_utils.protocols.account.TransactionSignerAccountProtocol + +Bases: `Protocol` + +An account that has a transaction signer. +Implemented by SigningAccount, LogicSigAccount, MultiSigAccount and TransactionSignerAccount abstractions. + +#### *property* address *: str* + +The address of the account. + +#### *property* signer *: algosdk.atomic_transaction_composer.TransactionSigner* + +The transaction signer for the account. diff --git a/docs/markdown/autoapi/algokit_utils/protocols/index.md b/docs/markdown/autoapi/algokit_utils/protocols/index.md new file mode 100644 index 00000000..8796fff2 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/index.md @@ -0,0 +1,6 @@ +# algokit_utils.protocols + +## Submodules + +* [algokit_utils.protocols.account](account/index.md) +* [algokit_utils.protocols.typed_clients](typed_clients/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md b/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md new file mode 100644 index 00000000..751ea934 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/protocols/typed_clients/index.md @@ -0,0 +1,97 @@ +# algokit_utils.protocols.typed_clients + +## Classes + +| [`TypedAppClientProtocol`](#algokit_utils.protocols.typed_clients.TypedAppClientProtocol) | Base class for protocol classes. | +|---------------------------------------------------------------------------------------------|------------------------------------| +| [`TypedAppFactoryProtocol`](#algokit_utils.protocols.typed_clients.TypedAppFactoryProtocol) | Base class for protocol classes. | + +## Module Contents + +### *class* algokit_utils.protocols.typed_clients.TypedAppClientProtocol(\*, app_id: int, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None) + +Bases: `Protocol` + +Base class for protocol classes. + +Protocol classes are defined as: + +```default +class Proto(Protocol): + def meth(self) -> int: + ... +``` + +Such classes are primarily used with static type checkers that recognize +structural subtyping (static duck-typing). + +For example: + +```default +class C: + def meth(self) -> int: + return 0 + +def func(x: Proto) -> int: + return x.meth() + +func(C()) # Passes static type check +``` + +See PEP 544 for details. Protocol classes decorated with +@typing.runtime_checkable act as simple-minded runtime protocols that check +only the presence of given attributes, ignoring their type signatures. +Protocol classes can be generic, they are defined as: + +```default +class GenProto[T](Protocol): + def meth(self) -> T: + ... +``` + +#### *classmethod* from_creator_and_name(\*, creator_address: str, app_name: str, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, ignore_cache: bool | None = None, app_lookup_cache: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) → typing_extensions.Self + +#### *classmethod* from_network(\*, app_name: str | None = None, default_sender: str | None = None, default_signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, approval_source_map: algosdk.source_map.SourceMap | None = None, clear_source_map: algosdk.source_map.SourceMap | None = None, algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient)) → typing_extensions.Self + +### *class* algokit_utils.protocols.typed_clients.TypedAppFactoryProtocol(algorand: [algokit_utils.algorand.AlgorandClient](../../algorand/index.md#algokit_utils.algorand.AlgorandClient), \*\*kwargs: Any) + +Bases: `Protocol`, `Generic`[`CreateParamsT`, `UpdateParamsT`, `DeleteParamsT`] + +Base class for protocol classes. + +Protocol classes are defined as: + +```default +class Proto(Protocol): + def meth(self) -> int: + ... +``` + +Such classes are primarily used with static type checkers that recognize +structural subtyping (static duck-typing). + +For example: + +```default +class C: + def meth(self) -> int: + return 0 + +def func(x: Proto) -> int: + return x.meth() + +func(C()) # Passes static type check +``` + +See PEP 544 for details. Protocol classes decorated with +@typing.runtime_checkable act as simple-minded runtime protocols that check +only the presence of given attributes, ignoring their type signatures. +Protocol classes can be generic, they are defined as: + +```default +class GenProto[T](Protocol): + def meth(self) -> T: + ... +``` + +#### deploy(\*, on_update: algokit_utils.applications.app_deployer.OnUpdate | None = None, on_schema_break: algokit_utils.applications.app_deployer.OnSchemaBreak | None = None, create_params: CreateParamsT | None = None, update_params: UpdateParamsT | None = None, delete_params: DeleteParamsT | None = None, existing_deployments: [algokit_utils.applications.app_deployer.ApplicationLookup](../../applications/app_deployer/index.md#algokit_utils.applications.app_deployer.ApplicationLookup) | None = None, ignore_cache: bool = False, app_name: str | None = None, send_params: algokit_utils.models.SendParams | None = None, compilation_params: [algokit_utils.applications.app_client.AppClientCompilationParams](../../applications/app_client/index.md#algokit_utils.applications.app_client.AppClientCompilationParams) | None = None) → tuple[[TypedAppClientProtocol](#algokit_utils.protocols.typed_clients.TypedAppClientProtocol), [algokit_utils.applications.app_factory.AppFactoryDeployResult](../../applications/app_factory/index.md#algokit_utils.applications.app_factory.AppFactoryDeployResult)] diff --git a/docs/markdown/autoapi/algokit_utils/transactions/index.md b/docs/markdown/autoapi/algokit_utils/transactions/index.md new file mode 100644 index 00000000..7455d34c --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/index.md @@ -0,0 +1,7 @@ +# algokit_utils.transactions + +## Submodules + +* [algokit_utils.transactions.transaction_composer](transaction_composer/index.md) +* [algokit_utils.transactions.transaction_creator](transaction_creator/index.md) +* [algokit_utils.transactions.transaction_sender](transaction_sender/index.md) diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md new file mode 100644 index 00000000..6000abcd --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md @@ -0,0 +1,841 @@ +# algokit_utils.transactions.transaction_composer + +## Attributes + +| [`MethodCallParams`](#algokit_utils.transactions.transaction_composer.MethodCallParams) | | +|-------------------------------------------------------------------------------------------------------------------------|----| +| [`AppMethodCallTransactionArgument`](#algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument) | | +| [`TxnParams`](#algokit_utils.transactions.transaction_composer.TxnParams) | | + +## Classes + +| [`PaymentParams`](#algokit_utils.transactions.transaction_composer.PaymentParams) | Parameters for a payment transaction. | +|---------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [`AssetCreateParams`](#algokit_utils.transactions.transaction_composer.AssetCreateParams) | Parameters for creating a new asset. | +| [`AssetConfigParams`](#algokit_utils.transactions.transaction_composer.AssetConfigParams) | Parameters for configuring an existing asset. | +| [`AssetFreezeParams`](#algokit_utils.transactions.transaction_composer.AssetFreezeParams) | Parameters for freezing an asset. | +| [`AssetDestroyParams`](#algokit_utils.transactions.transaction_composer.AssetDestroyParams) | Parameters for destroying an asset. | +| [`OnlineKeyRegistrationParams`](#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams) | Parameters for online key registration. | +| [`OfflineKeyRegistrationParams`](#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams) | Parameters for offline key registration. | +| [`AssetTransferParams`](#algokit_utils.transactions.transaction_composer.AssetTransferParams) | Parameters for transferring an asset. | +| [`AssetOptInParams`](#algokit_utils.transactions.transaction_composer.AssetOptInParams) | Parameters for opting into an asset. | +| [`AssetOptOutParams`](#algokit_utils.transactions.transaction_composer.AssetOptOutParams) | Parameters for opting out of an asset. | +| [`AppCallParams`](#algokit_utils.transactions.transaction_composer.AppCallParams) | Parameters for calling an application. | +| [`AppCreateSchema`](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | dict() -> new empty dictionary | +| [`AppCreateParams`](#algokit_utils.transactions.transaction_composer.AppCreateParams) | Parameters for creating an application. | +| [`AppUpdateParams`](#algokit_utils.transactions.transaction_composer.AppUpdateParams) | Parameters for updating an application. | +| [`AppDeleteParams`](#algokit_utils.transactions.transaction_composer.AppDeleteParams) | Parameters for deleting an application. | +| [`AppCallMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams) | Parameters for a regular ABI method call. | +| [`AppCreateMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams) | Parameters for an ABI method call that creates an application. | +| [`AppUpdateMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams) | Parameters for an ABI method call that updates an application. | +| [`AppDeleteMethodCallParams`](#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams) | Parameters for an ABI method call that deletes an application. | +| [`BuiltTransactions`](#algokit_utils.transactions.transaction_composer.BuiltTransactions) | Set of transactions built by TransactionComposer. | +| [`TransactionComposerBuildResult`](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) | Result of building transactions with TransactionComposer. | +| [`SendAtomicTransactionComposerResults`](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) | Results from sending an AtomicTransactionComposer transaction group. | +| [`TransactionComposer`](#algokit_utils.transactions.transaction_composer.TransactionComposer) | A class for composing and managing Algorand transactions. | + +## Functions + +| [`send_atomic_transaction_composer`](#algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer)(...) | Send an AtomicTransactionComposer transaction group. | +|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| + +## Module Contents + +### *class* algokit_utils.transactions.transaction_composer.PaymentParams + +Bases: `_CommonTxnParams` + +Parameters for a payment transaction. + +* **Variables:** + * **receiver** – The account that will receive the ALGO + * **amount** – Amount to send + * **close_remainder_to** – If given, close the sender account and send the remaining balance to this address, + +defaults to None + +#### receiver *: str* + +#### amount *: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount)* + +#### close_remainder_to *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetCreateParams + +Bases: `_CommonTxnParams` + +Parameters for creating a new asset. + +* **Variables:** + * **total** – The total amount of the smallest divisible unit to create + * **decimals** – The amount of decimal places the asset should have, defaults to None + * **default_frozen** – Whether the asset is frozen by default in the creator address, defaults to None + * **manager** – The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + * **reserve** – The address that holds the uncirculated supply, defaults to None + * **freeze** – The address that can freeze the asset in any account, defaults to None + * **clawback** – The address that can clawback the asset from any account, defaults to None + * **unit_name** – The short ticker name for the asset, defaults to None + * **asset_name** – The full name of the asset, defaults to None + * **url** – The metadata URL for the asset, defaults to None + * **metadata_hash** – Hash of the metadata contained in the metadata URL, defaults to None + +#### total *: int* + +#### asset_name *: str | None* *= None* + +#### unit_name *: str | None* *= None* + +#### url *: str | None* *= None* + +#### decimals *: int | None* *= None* + +#### default_frozen *: bool | None* *= None* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +#### metadata_hash *: bytes | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetConfigParams + +Bases: `_CommonTxnParams` + +Parameters for configuring an existing asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **manager** – The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + * **reserve** – The address that holds the uncirculated supply, defaults to None + * **freeze** – The address that can freeze the asset in any account, defaults to None + * **clawback** – The address that can clawback the asset from any account, defaults to None + +#### asset_id *: int* + +#### manager *: str | None* *= None* + +#### reserve *: str | None* *= None* + +#### freeze *: str | None* *= None* + +#### clawback *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetFreezeParams + +Bases: `_CommonTxnParams` + +Parameters for freezing an asset. + +* **Variables:** + * **asset_id** – The ID of the asset + * **account** – The account to freeze or unfreeze + * **frozen** – Whether the assets in the account should be frozen + +#### asset_id *: int* + +#### account *: str* + +#### frozen *: bool* + +### *class* algokit_utils.transactions.transaction_composer.AssetDestroyParams + +Bases: `_CommonTxnParams` + +Parameters for destroying an asset. + +* **Variables:** + **asset_id** – ID of the asset + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams + +Bases: `_CommonTxnParams` + +Parameters for online key registration. + +* **Variables:** + * **vote_key** – The root participation public key + * **selection_key** – The VRF public key + * **vote_first** – The first round that the participation key is valid + * **vote_last** – The last round that the participation key is valid + * **vote_key_dilution** – The dilution for the 2-level participation key + * **state_proof_key** – The 64 byte state proof public key commitment, defaults to None + +#### vote_key *: str* + +#### selection_key *: str* + +#### vote_first *: int* + +#### vote_last *: int* + +#### vote_key_dilution *: int* + +#### state_proof_key *: bytes | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams + +Bases: `_CommonTxnParams` + +Parameters for offline key registration. + +* **Variables:** + **prevent_account_from_ever_participating_again** – Whether to prevent the account from ever participating again + +#### prevent_account_from_ever_participating_again *: bool* + +### *class* algokit_utils.transactions.transaction_composer.AssetTransferParams + +Bases: `_CommonTxnParams` + +Parameters for transferring an asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **amount** – Amount of the asset to transfer (smallest divisible unit) + * **receiver** – The account to send the asset to + * **clawback_target** – The account to take the asset from, defaults to None + * **close_asset_to** – The account to close the asset to, defaults to None + +#### asset_id *: int* + +#### amount *: int* + +#### receiver *: str* + +#### clawback_target *: str | None* *= None* + +#### close_asset_to *: str | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AssetOptInParams + +Bases: `_CommonTxnParams` + +Parameters for opting into an asset. + +* **Variables:** + **asset_id** – ID of the asset + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_composer.AssetOptOutParams + +Bases: `_CommonTxnParams` + +Parameters for opting out of an asset. + +* **Variables:** + * **asset_id** – ID of the asset + * **creator** – The creator address of the asset + +#### asset_id *: int* + +#### creator *: str* + +### *class* algokit_utils.transactions.transaction_composer.AppCallParams + +Bases: `_CommonTxnParams` + +Parameters for calling an application. + +* **Variables:** + * **on_complete** – The OnComplete action + * **app_id** – ID of the application, defaults to None + * **approval_program** – The program to execute for all OnCompletes other than ClearState, defaults to None + * **clear_state_program** – The program to execute for ClearState OnComplete, defaults to None + * **schema** – The state schema for the app. This is immutable, defaults to None + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **extra_pages** – Number of extra pages required for the programs, defaults to None + * **box_references** – Box references, defaults to None + +#### on_complete *: algosdk.transaction.OnComplete* + +#### app_id *: int | None* *= None* + +#### approval_program *: str | bytes | None* *= None* + +#### clear_state_program *: str | bytes | None* *= None* + +#### schema *: dict[str, int] | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### extra_pages *: int | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateSchema + +Bases: `TypedDict` + +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object’s + +> (key, value) pairs + +dict(iterable) -> new dictionary initialized as if via: +: d = {} + for k, v in iterable: +
+ > d[k] = v + +dict( + +``` +** +``` + +kwargs) -> new dictionary initialized with the name=value pairs +: in the keyword argument list. For example: dict(one=1, two=2) + +#### global_ints *: int* + +#### global_byte_slices *: int* + +#### local_ints *: int* + +#### local_byte_slices *: int* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateParams + +Bases: `_CommonTxnParams` + +Parameters for creating an application. + +* **Variables:** + **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) + +or compiled teal (bytes) +:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) +or compiled teal (bytes) +:ivar schema: The state schema for the app. This is immutable, defaults to None +:ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None +:ivar args: Application arguments, defaults to None +:ivar account_references: Account references, defaults to None +:ivar app_references: App references, defaults to None +:ivar asset_references: Asset references, defaults to None +:ivar box_references: Box references, defaults to None +:ivar extra_program_pages: Number of extra pages required for the programs, defaults to None + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### schema *: [AppCreateSchema](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### extra_program_pages *: int | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppUpdateParams + +Bases: `_CommonTxnParams` + +Parameters for updating an application. + +* **Variables:** + * **app_id** – ID of the application + * **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) + +or compiled teal (bytes) +:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) +or compiled teal (bytes) +:ivar args: Application arguments, defaults to None +:ivar account_references: Account references, defaults to None +:ivar app_references: App references, defaults to None +:ivar asset_references: Asset references, defaults to None +:ivar box_references: Box references, defaults to None +:ivar on_complete: The OnComplete action, defaults to None + +#### app_id *: int* + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppDeleteParams + +Bases: `_CommonTxnParams` + +Parameters for deleting an application. + +* **Variables:** + * **app_id** – ID of the application + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **box_references** – Box references, defaults to None + * **on_complete** – The OnComplete action, defaults to DeleteApplicationOC + +#### app_id *: int* + +#### args *: list[bytes] | None* *= None* + +#### account_references *: list[str] | None* *= None* + +#### app_references *: list[int] | None* *= None* + +#### asset_references *: list[int] | None* *= None* + +#### box_references *: list[[algokit_utils.models.state.BoxReference](../../models/state/index.md#algokit_utils.models.state.BoxReference) | algokit_utils.models.state.BoxIdentifier] | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete* + +### *class* algokit_utils.transactions.transaction_composer.AppCallMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for a regular ABI method call. + +* **Variables:** + * **app_id** – ID of the application + * **method** – The ABI method to call + * **args** – Arguments to the ABI method, either an ABI value, transaction with explicit signer, + +transaction, another method call, or None +:ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + +#### app_id *: int* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that creates an application. + +* **Variables:** + * **approval_program** – The program to execute for all OnCompletes other than ClearState + * **clear_state_program** – The program to execute for ClearState OnComplete + * **schema** – The state schema for the app, defaults to None + * **on_complete** – The OnComplete action (cannot be ClearState), defaults to None + * **extra_program_pages** – Number of extra pages required for the programs, defaults to None + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### schema *: [AppCreateSchema](#algokit_utils.transactions.transaction_composer.AppCreateSchema) | None* *= None* + +#### on_complete *: algosdk.transaction.OnComplete | None* *= None* + +#### extra_program_pages *: int | None* *= None* + +### *class* algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that updates an application. + +* **Variables:** + * **app_id** – ID of the application + * **approval_program** – The program to execute for all OnCompletes other than ClearState + * **clear_state_program** – The program to execute for ClearState OnComplete + * **on_complete** – The OnComplete action, defaults to UpdateApplicationOC + +#### app_id *: int* + +#### approval_program *: str | bytes* + +#### clear_state_program *: str | bytes* + +#### on_complete *: algosdk.transaction.OnComplete* + +### *class* algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams + +Bases: `_BaseAppMethodCall` + +Parameters for an ABI method call that deletes an application. + +* **Variables:** + * **app_id** – ID of the application + * **on_complete** – The OnComplete action, defaults to DeleteApplicationOC + +#### app_id *: int* + +#### on_complete *: algosdk.transaction.OnComplete* + +### algokit_utils.transactions.transaction_composer.MethodCallParams + +### algokit_utils.transactions.transaction_composer.AppMethodCallTransactionArgument + +### algokit_utils.transactions.transaction_composer.TxnParams + +### *class* algokit_utils.transactions.transaction_composer.BuiltTransactions + +Set of transactions built by TransactionComposer. + +* **Variables:** + * **transactions** – The built transactions + * **method_calls** – Any ABIMethod objects associated with any of the transactions in a map keyed by txn id + * **signers** – Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id + +#### transactions *: list[algosdk.transaction.Transaction]* + +#### method_calls *: dict[int, algosdk.abi.Method]* + +#### signers *: dict[int, algosdk.atomic_transaction_composer.TransactionSigner]* + +### *class* algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult + +Result of building transactions with TransactionComposer. + +* **Variables:** + * **atc** – The AtomicTransactionComposer instance + * **transactions** – The list of transactions with signers + * **method_calls** – Map of transaction index to ABI method + +#### atc *: algosdk.atomic_transaction_composer.AtomicTransactionComposer* + +#### transactions *: list[algosdk.atomic_transaction_composer.TransactionWithSigner]* + +#### method_calls *: dict[int, algosdk.abi.Method]* + +### *class* algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults + +Results from sending an AtomicTransactionComposer transaction group. + +* **Variables:** + * **group_id** – The group ID if this was a transaction group + * **confirmations** – The confirmation info for each transaction + * **tx_ids** – The transaction IDs that were sent + * **transactions** – The transactions that were sent + * **returns** – The ABI return values from any ABI method calls + * **simulate_response** – The simulation response if simulation was performed, defaults to None + +#### group_id *: str* + +#### confirmations *: list[algosdk.v2client.algod.AlgodResponseType]* + +#### tx_ids *: list[str]* + +#### transactions *: list[[algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)]* + +#### returns *: list[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)]* + +#### simulate_response *: dict[str, Any] | None* *= None* + +### algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer, algod: algosdk.v2client.algod.AlgodClient, \*, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Send an AtomicTransactionComposer transaction group. + +Executes a group of transactions atomically using the AtomicTransactionComposer. + +* **Parameters:** + * **atc** – The AtomicTransactionComposer instance containing the transaction group to send + * **algod** – The Algod client to use for sending the transactions + * **max_rounds_to_wait** – Maximum number of rounds to wait for confirmation, defaults to 5 + * **skip_waiting** – If True, don’t wait for transaction confirmation, defaults to False + * **suppress_log** – If True, suppress logging, defaults to None + * **populate_app_call_resources** – If True, populate app call resources, defaults to None + * **cover_app_call_inner_transaction_fees** – If True, cover app call inner transaction fees, defaults to None + * **additional_atc_context** – Additional context for the AtomicTransactionComposer +* **Returns:** + Results from sending the transaction group +* **Raises:** + * **Exception** – If there is an error sending the transactions + * **error** – If there is an error from the Algorand node + +### *class* algokit_utils.transactions.transaction_composer.TransactionComposer(algod: algosdk.v2client.algod.AlgodClient, get_signer: collections.abc.Callable[[str], algosdk.atomic_transaction_composer.TransactionSigner], get_suggested_params: collections.abc.Callable[[], algosdk.transaction.SuggestedParams] | None = None, default_validity_window: int | None = None, app_manager: [algokit_utils.applications.app_manager.AppManager](../../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager) | None = None) + +A class for composing and managing Algorand transactions. + +Provides a high-level interface for building and executing transaction groups using the Algosdk library. +Supports various transaction types including payments, asset operations, application calls, and key registrations. + +* **Parameters:** + * **algod** – An instance of AlgodClient used to get suggested params and send transactions + * **get_signer** – A function that takes an address and returns a TransactionSigner for that address + * **get_suggested_params** – Optional function to get suggested transaction parameters, + +defaults to using algod.suggested_params() +:param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 +:param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None + +#### add_transaction(transaction: algosdk.transaction.Transaction, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add a raw transaction to the composer. + +* **Parameters:** + * **transaction** – The transaction to add + * **signer** – Optional transaction signer, defaults to getting signer from transaction sender +* **Returns:** + The transaction composer instance for chaining + +#### add_payment(params: [PaymentParams](#algokit_utils.transactions.transaction_composer.PaymentParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add a payment transaction. + +* **Parameters:** + **params** – The payment transaction parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_create(params: [AssetCreateParams](#algokit_utils.transactions.transaction_composer.AssetCreateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset creation transaction. + +* **Parameters:** + **params** – The asset creation parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_config(params: [AssetConfigParams](#algokit_utils.transactions.transaction_composer.AssetConfigParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset configuration transaction. + +* **Parameters:** + **params** – The asset configuration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_freeze(params: [AssetFreezeParams](#algokit_utils.transactions.transaction_composer.AssetFreezeParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset freeze transaction. + +* **Parameters:** + **params** – The asset freeze parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_destroy(params: [AssetDestroyParams](#algokit_utils.transactions.transaction_composer.AssetDestroyParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset destruction transaction. + +* **Parameters:** + **params** – The asset destruction parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_transfer(params: [AssetTransferParams](#algokit_utils.transactions.transaction_composer.AssetTransferParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset transfer transaction. + +* **Parameters:** + **params** – The asset transfer parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_opt_in(params: [AssetOptInParams](#algokit_utils.transactions.transaction_composer.AssetOptInParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset opt-in transaction. + +* **Parameters:** + **params** – The asset opt-in parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_asset_opt_out(params: [AssetOptOutParams](#algokit_utils.transactions.transaction_composer.AssetOptOutParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an asset opt-out transaction. + +* **Parameters:** + **params** – The asset opt-out parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_create(params: [AppCreateParams](#algokit_utils.transactions.transaction_composer.AppCreateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application creation transaction. + +* **Parameters:** + **params** – The application creation parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_update(params: [AppUpdateParams](#algokit_utils.transactions.transaction_composer.AppUpdateParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application update transaction. + +* **Parameters:** + **params** – The application update parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_delete(params: [AppDeleteParams](#algokit_utils.transactions.transaction_composer.AppDeleteParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application deletion transaction. + +* **Parameters:** + **params** – The application deletion parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_call(params: [AppCallParams](#algokit_utils.transactions.transaction_composer.AppCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application call transaction. + +* **Parameters:** + **params** – The application call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_create_method_call(params: [AppCreateMethodCallParams](#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application creation method call transaction. + +* **Parameters:** + **params** – The application creation method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_update_method_call(params: [AppUpdateMethodCallParams](#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application update method call transaction. + +* **Parameters:** + **params** – The application update method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_delete_method_call(params: [AppDeleteMethodCallParams](#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application deletion method call transaction. + +* **Parameters:** + **params** – The application deletion method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_app_call_method_call(params: [AppCallMethodCallParams](#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an application call method call transaction. + +* **Parameters:** + **params** – The application call method call parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_online_key_registration(params: [OnlineKeyRegistrationParams](#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an online key registration transaction. + +* **Parameters:** + **params** – The online key registration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_offline_key_registration(params: [OfflineKeyRegistrationParams](#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams)) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an offline key registration transaction. + +* **Parameters:** + **params** – The offline key registration parameters +* **Returns:** + The transaction composer instance for chaining + +#### add_atc(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Add an existing AtomicTransactionComposer’s transactions. + +* **Parameters:** + **atc** – The AtomicTransactionComposer to add +* **Returns:** + The transaction composer instance for chaining + +#### count() → int + +Get the total number of transactions. + +* **Returns:** + The number of transactions + +#### build() → [TransactionComposerBuildResult](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) + +Build the transaction group. + +* **Returns:** + The built transaction group result + +#### rebuild() → [TransactionComposerBuildResult](#algokit_utils.transactions.transaction_composer.TransactionComposerBuildResult) + +Rebuild the transaction group from scratch. + +* **Returns:** + The rebuilt transaction group result + +#### build_transactions() → [BuiltTransactions](#algokit_utils.transactions.transaction_composer.BuiltTransactions) + +Build and return the transactions without executing them. + +* **Returns:** + The built transactions result + +#### execute(\*, max_rounds_to_wait: int | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +#### send(params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Send the transaction group to the network. + +* **Parameters:** + **params** – Parameters for the send operation +* **Returns:** + The transaction send results +* **Raises:** + **Exception** – If the transaction fails + +#### simulate(allow_more_logs: bool | None = None, allow_empty_signatures: bool | None = None, allow_unnamed_resources: bool | None = None, extra_opcode_budget: int | None = None, exec_trace_config: algosdk.v2client.models.SimulateTraceConfig | None = None, simulation_round: int | None = None, skip_signatures: bool | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) + +Simulate transaction group execution with configurable validation rules. + +* **Parameters:** + * **allow_more_logs** – Whether to allow more logs than the standard limit + * **allow_empty_signatures** – Whether to allow transactions with empty signatures + * **allow_unnamed_resources** – Whether to allow unnamed resources + * **extra_opcode_budget** – Additional opcode budget to allocate + * **exec_trace_config** – Configuration for execution tracing + * **simulation_round** – Round number to simulate at + * **skip_signatures** – Whether to skip signature validation +* **Returns:** + The simulation results + +#### *static* arc2_note(note: algokit_utils.models.transaction.Arc2TransactionNote) → bytes + +Create an encoded transaction note that follows the ARC-2 spec. + +[https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) + +* **Parameters:** + **note** – The ARC-2 note to encode +* **Returns:** + The encoded note bytes +* **Raises:** + **ValueError** – If the dapp_name is invalid diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md new file mode 100644 index 00000000..34ebfb70 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_creator/index.md @@ -0,0 +1,90 @@ +# algokit_utils.transactions.transaction_creator + +## Classes + +| [`AlgorandClientTransactionCreator`](#algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator) | A creator for Algorand transactions. | +|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------| + +## Module Contents + +### *class* algokit_utils.transactions.transaction_creator.AlgorandClientTransactionCreator(new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)]) + +A creator for Algorand transactions. + +Provides methods to create various types of Algorand transactions including payments, +asset operations, application calls and key registrations. + +* **Parameters:** + **new_group** – A lambda that starts a new TransactionComposer transaction group + +#### *property* payment *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.PaymentParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.PaymentParams)], algosdk.transaction.Transaction]* + +Create a payment transaction to transfer Algo between accounts. + +#### *property* asset_create *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetCreateParams)], algosdk.transaction.Transaction]* + +Create a create Algorand Standard Asset transaction. + +#### *property* asset_config *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetConfigParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetConfigParams)], algosdk.transaction.Transaction]* + +Create an asset config transaction to reconfigure an existing Algorand Standard Asset. + +#### *property* asset_freeze *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetFreezeParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetFreezeParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset freeze transaction. + +#### *property* asset_destroy *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetDestroyParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetDestroyParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset destroy transaction. + +#### *property* asset_transfer *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetTransferParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetTransferParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset transfer transaction. + +#### *property* asset_opt_in *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetOptInParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptInParams)], algosdk.transaction.Transaction]* + +Create an Algorand Standard Asset opt-in transaction. + +#### *property* asset_opt_out *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AssetOptOutParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptOutParams)], algosdk.transaction.Transaction]* + +Create an asset opt-out transaction. + +#### *property* app_create *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams)], algosdk.transaction.Transaction]* + +Create an application create transaction. + +#### *property* app_update *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppUpdateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams)], algosdk.transaction.Transaction]* + +Create an application update transaction. + +#### *property* app_delete *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppDeleteParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams)], algosdk.transaction.Transaction]* + +Create an application delete transaction. + +#### *property* app_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallParams)], algosdk.transaction.Transaction]* + +Create an application call transaction. + +#### *property* app_create_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application create call with ABI method call transaction. + +#### *property* app_update_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application update call with ABI method call transaction. + +#### *property* app_delete_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application delete call with ABI method call transaction. + +#### *property* app_call_method_call *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.AppCallMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams)], [algokit_utils.transactions.transaction_composer.BuiltTransactions](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.BuiltTransactions)]* + +Create an application call with ABI method call transaction. + +#### *property* online_key_registration *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams)], algosdk.transaction.Transaction]* + +Create an online key registration transaction. + +#### *property* offline_key_registration *: collections.abc.Callable[[[algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams)], algosdk.transaction.Transaction]* + +Create an offline key registration transaction. diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md new file mode 100644 index 00000000..c8b886d6 --- /dev/null +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_sender/index.md @@ -0,0 +1,278 @@ +# algokit_utils.transactions.transaction_sender + +## Classes + +| [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) | Base class for transaction results. | +|-----------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------| +| [`SendSingleAssetCreateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult) | Result of creating a new ASA (Algorand Standard Asset). | +| [`SendAppTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult) | Result of an application transaction. | +| [`SendAppUpdateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult) | Result of updating an application. | +| [`SendAppCreateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult) | Result of creating a new application. | +| [`AlgorandClientTransactionSender`](#algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender) | Orchestrates sending transactions for AlgorandClient. | + +## Module Contents + +### *class* algokit_utils.transactions.transaction_sender.SendSingleTransactionResult + +Base class for transaction results. + +Represents the result of sending a single transaction. + +#### transaction *: [algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)* + +#### confirmation *: algosdk.v2client.algod.AlgodResponseType* + +#### group_id *: str* + +#### tx_id *: str | None* *= None* + +#### tx_ids *: list[str]* + +#### transactions *: list[[algokit_utils.models.transaction.TransactionWrapper](../../models/transaction/index.md#algokit_utils.models.transaction.TransactionWrapper)]* + +#### confirmations *: list[algosdk.v2client.algod.AlgodResponseType]* + +#### returns *: list[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] | None* *= None* + +#### *classmethod* from_composer_result(result: [algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults), index: int = -1) → typing_extensions.Self + +### *class* algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult + +Bases: [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Result of creating a new ASA (Algorand Standard Asset). + +Contains the asset ID of the newly created asset. + +#### asset_id *: int* + +### *class* algokit_utils.transactions.transaction_sender.SendAppTransactionResult + +Bases: [`SendSingleTransactionResult`](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult), `Generic`[`ABIReturnT`] + +Result of an application transaction. + +Contains the ABI return value if applicable. + +#### abi_return *: ABIReturnT | None* *= None* + +### *class* algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult + +Bases: [`SendAppTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[`ABIReturnT`] + +Result of updating an application. + +Contains the compiled approval and clear programs. + +#### compiled_approval *: Any | None* *= None* + +#### compiled_clear *: Any | None* *= None* + +### *class* algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult + +Bases: [`SendAppUpdateTransactionResult`](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[`ABIReturnT`] + +Result of creating a new application. + +Contains the app ID and address of the newly created application. + +#### app_id *: int* + +#### app_address *: str* + +### *class* algokit_utils.transactions.transaction_sender.AlgorandClientTransactionSender(new_group: collections.abc.Callable[[], [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer)], asset_manager: [algokit_utils.assets.asset_manager.AssetManager](../../assets/asset_manager/index.md#algokit_utils.assets.asset_manager.AssetManager), app_manager: [algokit_utils.applications.app_manager.AppManager](../../applications/app_manager/index.md#algokit_utils.applications.app_manager.AppManager), algod_client: algosdk.v2client.algod.AlgodClient) + +Orchestrates sending transactions for AlgorandClient. + +Provides methods to send various types of transactions including payments, +asset operations, and application calls. + +#### new_group() → [algokit_utils.transactions.transaction_composer.TransactionComposer](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.TransactionComposer) + +Create a new transaction group. + +* **Returns:** + A new TransactionComposer instance + +#### payment(params: [algokit_utils.transactions.transaction_composer.PaymentParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.PaymentParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Send a payment transaction to transfer Algo between accounts. + +* **Parameters:** + * **params** – Payment transaction parameters + * **send_params** – Send parameters +* **Returns:** + Result of the payment transaction + +#### asset_create(params: [algokit_utils.transactions.transaction_composer.AssetCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetCreateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleAssetCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleAssetCreateTransactionResult) + +Create a new Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset creation parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the new asset ID + +#### asset_config(params: [algokit_utils.transactions.transaction_composer.AssetConfigParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetConfigParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Configure an existing Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset configuration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the configuration transaction + +#### asset_freeze(params: [algokit_utils.transactions.transaction_composer.AssetFreezeParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetFreezeParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Freeze or unfreeze an Algorand Standard Asset for an account. + +* **Parameters:** + * **params** – Asset freeze parameters + * **send_params** – Send parameters +* **Returns:** + Result of the freeze transaction + +#### asset_destroy(params: [algokit_utils.transactions.transaction_composer.AssetDestroyParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetDestroyParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Destroys an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset destruction parameters + * **send_params** – Send parameters +* **Returns:** + Result of the destroy transaction + +#### asset_transfer(params: [algokit_utils.transactions.transaction_composer.AssetTransferParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetTransferParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Transfer an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset transfer parameters + * **send_params** – Send parameters +* **Returns:** + Result of the transfer transaction + +#### asset_opt_in(params: [algokit_utils.transactions.transaction_composer.AssetOptInParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptInParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Opt an account into an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset opt-in parameters + * **send_params** – Send parameters +* **Returns:** + Result of the opt-in transaction + +#### asset_opt_out(\*, params: [algokit_utils.transactions.transaction_composer.AssetOptOutParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AssetOptOutParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, ensure_zero_balance: bool = True) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Opt an account out of an Algorand Standard Asset. + +* **Parameters:** + * **params** – Asset opt-out parameters + * **send_params** – Send parameters + * **ensure_zero_balance** – Check if account has zero balance before opt-out, defaults to True +* **Raises:** + **ValueError** – If account has non-zero balance or is not opted in +* **Returns:** + Result of the opt-out transaction + +#### app_create(params: [algokit_utils.transactions.transaction_composer.AppCreateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Create a new application. + +* **Parameters:** + * **params** – Application creation parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the new application ID and address + +#### app_update(params: [algokit_utils.transactions.transaction_composer.AppUpdateParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppUpdateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Update an application. + +* **Parameters:** + * **params** – Application update parameters + * **send_params** – Send parameters +* **Returns:** + Result containing the compiled programs + +#### app_delete(params: [algokit_utils.transactions.transaction_composer.AppDeleteParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Delete an application. + +* **Parameters:** + * **params** – Application deletion parameters + * **send_params** – Send parameters +* **Returns:** + Result of the deletion transaction + +#### app_call(params: [algokit_utils.transactions.transaction_composer.AppCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application. + +* **Parameters:** + * **params** – Application call parameters + * **send_params** – Send parameters +* **Returns:** + Result containing any ABI return value + +#### app_create_method_call(params: [algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCreateMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppCreateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppCreateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s create method. + +* **Parameters:** + * **params** – Method call parameters for application creation + * **send_params** – Send parameters +* **Returns:** + Result containing the new application ID and address + +#### app_update_method_call(params: [algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppUpdateMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppUpdateTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppUpdateTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s update method. + +* **Parameters:** + * **params** – Method call parameters for application update + * **send_params** – Send parameters +* **Returns:** + Result containing the compiled programs + +#### app_delete_method_call(params: [algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppDeleteMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s delete method. + +* **Parameters:** + * **params** – Method call parameters for application deletion + * **send_params** – Send parameters +* **Returns:** + Result of the deletion transaction + +#### app_call_method_call(params: [algokit_utils.transactions.transaction_composer.AppCallMethodCallParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.AppCallMethodCallParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendAppTransactionResult](#algokit_utils.transactions.transaction_sender.SendAppTransactionResult)[[algokit_utils.applications.abi.ABIReturn](../../applications/abi/index.md#algokit_utils.applications.abi.ABIReturn)] + +Call an application’s call method. + +* **Parameters:** + * **params** – Method call parameters + * **send_params** – Send parameters +* **Returns:** + Result containing any ABI return value + +#### online_key_registration(params: [algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OnlineKeyRegistrationParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Register an online key. + +* **Parameters:** + * **params** – Key registration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the registration transaction + +#### offline_key_registration(params: [algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams](../transaction_composer/index.md#algokit_utils.transactions.transaction_composer.OfflineKeyRegistrationParams), send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendSingleTransactionResult](#algokit_utils.transactions.transaction_sender.SendSingleTransactionResult) + +Register an offline key. + +* **Parameters:** + * **params** – Key registration parameters + * **send_params** – Send parameters +* **Returns:** + Result of the registration transaction diff --git a/docs/markdown/autoapi/algorand_client/index.md b/docs/markdown/autoapi/algorand_client/index.md new file mode 100644 index 00000000..e2034ce0 --- /dev/null +++ b/docs/markdown/autoapi/algorand_client/index.md @@ -0,0 +1 @@ +# algorand_client diff --git a/docs/markdown/autoapi/client_manager/index.md b/docs/markdown/autoapi/client_manager/index.md new file mode 100644 index 00000000..e73efa8f --- /dev/null +++ b/docs/markdown/autoapi/client_manager/index.md @@ -0,0 +1 @@ +# client_manager diff --git a/docs/markdown/autoapi/composer/index.md b/docs/markdown/autoapi/composer/index.md new file mode 100644 index 00000000..4cb259e0 --- /dev/null +++ b/docs/markdown/autoapi/composer/index.md @@ -0,0 +1 @@ +# composer diff --git a/docs/markdown/autoapi/index.md b/docs/markdown/autoapi/index.md new file mode 100644 index 00000000..32153723 --- /dev/null +++ b/docs/markdown/autoapi/index.md @@ -0,0 +1,48 @@ +# API Reference + +This page contains auto-generated API reference documentation [1](#f1). + +* [composer](composer/index.md) +* [algokit_utils](algokit_utils/index.md) + * [algokit_utils.accounts](algokit_utils/accounts/index.md) + * [algokit_utils.accounts.account_manager](algokit_utils/accounts/account_manager/index.md) + * [algokit_utils.accounts.kmd_account_manager](algokit_utils/accounts/kmd_account_manager/index.md) + * [algokit_utils.algorand](algokit_utils/algorand/index.md) + * [algokit_utils.applications](algokit_utils/applications/index.md) + * [algokit_utils.applications.abi](algokit_utils/applications/abi/index.md) + * [algokit_utils.applications.app_client](algokit_utils/applications/app_client/index.md) + * [algokit_utils.applications.app_deployer](algokit_utils/applications/app_deployer/index.md) + * [algokit_utils.applications.app_factory](algokit_utils/applications/app_factory/index.md) + * [algokit_utils.applications.app_manager](algokit_utils/applications/app_manager/index.md) + * [algokit_utils.applications.app_spec](algokit_utils/applications/app_spec/index.md) + * [algokit_utils.applications.app_spec.arc32](algokit_utils/applications/app_spec/arc32/index.md) + * [algokit_utils.applications.app_spec.arc56](algokit_utils/applications/app_spec/arc56/index.md) + * [algokit_utils.applications.enums](algokit_utils/applications/enums/index.md) + * [algokit_utils.assets](algokit_utils/assets/index.md) + * [algokit_utils.assets.asset_manager](algokit_utils/assets/asset_manager/index.md) + * [algokit_utils.clients](algokit_utils/clients/index.md) + * [algokit_utils.clients.client_manager](algokit_utils/clients/client_manager/index.md) + * [algokit_utils.clients.dispenser_api_client](algokit_utils/clients/dispenser_api_client/index.md) + * [algokit_utils.config](algokit_utils/config/index.md) + * [algokit_utils.errors](algokit_utils/errors/index.md) + * [algokit_utils.errors.logic_error](algokit_utils/errors/logic_error/index.md) + * [algokit_utils.models](algokit_utils/models/index.md) + * [algokit_utils.models.account](algokit_utils/models/account/index.md) + * [algokit_utils.models.amount](algokit_utils/models/amount/index.md) + * [algokit_utils.models.application](algokit_utils/models/application/index.md) + * [algokit_utils.models.network](algokit_utils/models/network/index.md) + * [algokit_utils.models.simulate](algokit_utils/models/simulate/index.md) + * [algokit_utils.models.state](algokit_utils/models/state/index.md) + * [algokit_utils.models.transaction](algokit_utils/models/transaction/index.md) + * [algokit_utils.protocols](algokit_utils/protocols/index.md) + * [algokit_utils.protocols.account](algokit_utils/protocols/account/index.md) + * [algokit_utils.protocols.typed_clients](algokit_utils/protocols/typed_clients/index.md) + * [algokit_utils.transactions](algokit_utils/transactions/index.md) + * [algokit_utils.transactions.transaction_composer](algokit_utils/transactions/transaction_composer/index.md) + * [algokit_utils.transactions.transaction_creator](algokit_utils/transactions/transaction_creator/index.md) + * [algokit_utils.transactions.transaction_sender](algokit_utils/transactions/transaction_sender/index.md) +* [client_manager](client_manager/index.md) +* [algorand_client](algorand_client/index.md) +* [account_manager](account_manager/index.md) + +* **[1]** Created with [sphinx-autoapi](https://github.com/readthedocs/sphinx-autoapi) diff --git a/docs/markdown/capabilities/account.md b/docs/markdown/capabilities/account.md index 54bd929e..cfbc2c71 100644 --- a/docs/markdown/capabilities/account.md +++ b/docs/markdown/capabilities/account.md @@ -1,32 +1,213 @@ # Account management -Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, idempotent KMD and environment variable injected accounts -that can be used to sign transactions as well as representing a sender address at the same time. +Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, rekeyed, multisig, transaction signer, idempotent KMD and environment variable injected accounts that can be used to sign transactions as well as representing a sender address at the same time. This significantly simplifies management of transaction signing. - +## `AccountManager` -## `Account` +The [`AccountManager`]() is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! -Encapsulates a private key with convenience properties for `address`, `signer` and `public_key`. +To get an instance of `AccountManager`, you can use either [`AlgorandClient`](algorand-client.md) via `algorand.account` or instantiate it directly: -There are various methods of obtaining an `Account` instance +```python +from algokit_utils import AccountManager -* `get_account`: Returns an `Account` instance with the private key loaded by convention based on the given name identifier: - * from an environment variable containing a mnemonic `{NAME}_MNEMONIC` OR - * loading the account from KMD ny name if it exists (LocalNet only) OR - * creating the account in KMD with associated name (LocalNet only) +account_manager = AccountManager(client_manager) +``` - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against - TestNet/MainNet will automatically resolve from environment variables -* `Account.new_account`: Returns a new `Account` using `algosdk.account.generate_account()` -* `Account(private_key)`: Load an existing account from a private key -* `Account(private_key, address)`: Load an existing account from a private key and address, useful for re-keyed accounts -* `get_account_from_mnemonic`: Load an existing account from a mnemonic -* `get_dispenser_account`: Gets a dispenser account that is funded by either: - * Using the LocalNet default account (LocalNet only) OR - * Loading an account from `DISPENSER_MNEMONIC` +## `TransactionSignerAccountProtocol` -If working with a LocalNet instance, there are some additional functions that rely on a KMD service being exposed: +The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. -* `create_kmd_wallet_account`, `get_kmd_wallet_account` or `get_or_create_kmd_wallet_account`: These functions allow retrieving a KMD wallet account by name, -* `get_localnet_default_account`: Gets default localnet account that is funded with algos +The following conform to `TransactionSignerAccountProtocol`: + +- [`TransactionSignerAccount`]() - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- [`SigningAccount`]() - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- [`LogicSigAccount`]() - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- [`MultisigAccount`]() - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` + +## Registering a signer + +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by [`AlgorandClient`](algorand-client.md) to automatically sign transactions by that sender. Any of the [methods]() within `AccountManager` that return an account will automatically register the signer with the sender. + +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of [account based objects]() that combine signer and sender (`TransactionSignerAccount` | `SigningAccount` | `LogicSigAccount` | `MultisigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: + +```python +algorand.account + .set_signer_from_account(TransactionSignerAccount(your_address, your_signer)) + .set_signer_from_account(SigningAccount.new_account()) + .set_signer_from_account( + LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)) + ) + .set_signer_from_account( + MultisigAccount( + MultisigMetadata( + version = 1, + threshold = 1, + addresses = ["ADDRESS1...", "ADDRESS2..."] + ), + [account1, account2] + ) + ) + .set_signer("SENDERADDRESS", transaction_signer) +``` + +## Default signer + +If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can [register a default signer](): + +```python +algorand.account.set_default_signer(my_default_signer) +``` + +## Get a signer + +[`AlgorandClient`](algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer]() for a given sender address: + +```python +signer = algorand.account.get_signer("SENDER_ADDRESS") +``` + +If there is no signer registered for that sender address it will either return the default signer ([if registered]()) or throw an exception. + +## Accounts + +In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`]() (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](algorand-client.md)): + +- [`algorand.account.from_environment(name, fund_with)`]() - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) + - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code + - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD +- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`]() - Registers and returns an account with secret key loaded by taking the mnemonic secret +- [`algorand.account.multisig(multisig_params, signing_accounts)`]() - Registers and returns a multisig account with one or more signing keys loaded +- [`algorand.account.rekeyed(sender, signer)`]() - Registers and returns an account representing the given rekeyed sender/signer combination +- [`algorand.account.random()`]() - Returns a new, cryptographically randomly generated account with private key loaded +- [`algorand.account.from_kmd()`]() - Returns an account with private key loaded from the given KMD wallet (identified by name) +- [`algorand.account.logicsig(program, args?)`]() - Returns an account that represents a logic signature + +### Underlying account classes + +While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. + +- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created +- [`SigningAccount`]() - An abstraction around `algosdk.Account` that supports rekeyed accounts +- `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object +- [`MultisigAccount`]() - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present + +### Dispenser + +- [`algorand.account.dispenserFromEnvironment()`]() - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- [`algorand.account.localNetDispenser()`]() - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account + +## Rekey account + +One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +> [!WARNING] +> Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. + +You can issue a transaction to rekey an account by using the [`algorand.account.rekeyAccount(account, rekeyTo, options)`]() function: + +- `account: string | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekeyTo: string | TransactionSignerAccount` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- An `options` object, which has: + - [Common transaction parameters](algorand-client.md#transaction-parameters) + - [Execution parameters](algorand-client.md#sending-a-single-transaction) + +You can also pass in `rekeyTo` as a [common transaction parameter](algorand-client.md#transaction-parameters) to any transaction. + +### Examples + +```python +# Basic example (with string addresses) + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", +}) + +# Basic example (with signer accounts) + +algorand.account.rekey_account({ + account: account1, + rekey_to: new_signer_account, +}) + +# Advanced example + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", + lease: "lease", + note: "note", + first_valid_round: 1000, + validity_window: 10, + extra_fee: AlgoAmount.from_micro_algos(1000), + static_fee: AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee: AlgoAmount.from_micro_algos(3000), + max_rounds_to_wait_for_confirmation: 5, + suppress_log: True, +}) + + +# Using a rekeyed account + +Note: if a signing account is passed into `algorand.account.rekey_account` then you don't need to call `rekeyed_account` to register the new signer + +rekeyed_account = algorand.account.rekey_account(account, new_account) +# rekeyed_account can be used to sign transactions on behalf of account... +``` + +## KMD account management + +When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for: + +- Accessing the private key of the default accounts that are pre-seeded with Algo so that other accounts can be funded and it’s possible to use LocalNet +- Idempotently creating new accounts against a name that will stay intact while the LocalNet instance is running without you needing to store private keys anywhere (i.e. completely automated) + +The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that’s needed. This code has been abstracted away into the `KmdAccountManager` class. + +To get an instance of the `KmdAccountManager` class you can access it from [`AlgorandClient`](algorand-client.md) via `algorand.account.kmd` or instantiate it directly (passing in a [`ClientManager`](client.md)): + +```python +from algokit_utils import KmdAccountManager + +kmd_account_manager = KmdAccountManager(client_manager) +``` + +The methods that are available are: + +- [`get_wallet_account(wallet_name, predicate?, sender?)`]()\` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- [`get_or_create_wallet_account(name, fund_with?)`]()\` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- [`get_localnet_dispenser_account()`]()\` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) + +```python +# Get a wallet account that seeded the LocalNet network +default_dispenser_account = kmd_account_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 +) +# Same as above, but dedicated method call for convenience +localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +# Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD +# if creating it then fund it with 2 ALGO from the default dispenser account +new_account = kmd_account_manager.get_or_create_wallet_account( + "account1", + AlgoAmount.from_algos(2) +) +# This will return the same account as above since the name matches +existing_account = kmd_account_manager.get_or_create_wallet_account( + "account1" +) +``` + +Some of this functionality is directly exposed from [`AccountManager`](), which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via [`AlgorandClient`](algorand-client.md): + +```python +# Get and register LocalNet dispenser +localnet_dispenser = algorand.account.localnet_dispenser() +# Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD +dispenser = algorand.account.dispenser_from_environment() +# Get / create and register account from KMD idempotently by name +account1 = algorand.account.from_kmd("account1", AlgoAmount.from_algos(2)) +``` diff --git a/docs/markdown/capabilities/algorand-client.md b/docs/markdown/capabilities/algorand-client.md new file mode 100644 index 00000000..9404653e --- /dev/null +++ b/docs/markdown/capabilities/algorand-client.md @@ -0,0 +1,191 @@ +# Algorand client + +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It’s the [default entrypoint](../index.md#id3) into AlgoKit Utils functionality. + +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](), e.g.: + +```python +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet +# configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=algod) +# Point to pre-created algod, indexer and kmd clients +algorand = AlgorandClient.from_clients(algod=algod, indexer=indexer, kmd=kmd) +# Point to custom configuration for algod +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod, indexer and kmd +algorand = AlgorandClient.from_config( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config +) +``` + +## Accessing SDK clients + +Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. + +```py +algorand = AlgorandClient.default_localnet() + +algod_client = algorand.client.algod +indexer_client = algorand.client.indexer +kmd_client = algorand.client.kmd +``` + +## Accessing manager class instances + +The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. + +- [`AccountManager`](account.md) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.setDefaultSigner(signer)` - + - `algorand.setSignerFromAccount(account)` - + - `algorand.setSigner(sender, signer)` +- [`AssetManager`](asset.md) via `algorand.asset` +- [`ClientManager`](client.md) via `algorand.client` + +## Creating and issuing transactions + +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](transaction-composer.md)). + +### Creating transactions + +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`]() class. Intellisense will guide you on the different options. + +The signature for the calls to send a single transaction usually look like: + +```python +algorand.create_transaction.{method}(params=TxnParams(...), send_params=SendParams(...)) -> Transaction: +``` + +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils` and consist of: + - `AppCallParams`, + - `AppCreateParams`, + - `AppDeleteParams`, + - `AppUpdateParams`, + - `AssetConfigParams`, + - `AssetCreateParams`, + - `AssetDestroyParams`, + - `AssetFreezeParams`, + - `AssetOptInParams`, + - `AssetOptOutParams`, + - `AssetTransferParams`, + - `OfflineKeyRegistrationParams`, + - `OnlineKeyRegistrationParams`, + - `PaymentParams`, +- `SendParams` is a typed dictionary exposing setting to apply during send operation: + - `max_rounds_to_wait_for_confirmation: int | None` - The number of rounds to wait for confirmation. By default until the latest lastValid has past. + - `suppress_log: bool | None` - Whether to suppress log messages from transaction send, default: do not suppress. + - `populate_app_call_resources: bool | None` - Whether to use simulate to automatically populate app call resources in the txn objects. Defaults to `Config.populateAppCallResources`. + - `cover_app_call_inner_transaction_fees: bool | None` - Whether to use simulate to automatically calculate required app call inner transaction fees and cover them in the parent app call transaction fee + +The return type for the ABI method call methods are slightly different: + +```python +algorand.createTransaction.app{call_type}_method_call(params=MethodCallParams(...), send_params=SendParams(...)) -> BuiltTransactions +``` + +MethodCallParams is a union type that can be any of the Algorand method call types, exact dataclasses can be imported from `algokit_utils` and consist of: + +- `AppCreateMethodCallParams`, +- `AppCallMethodCallParams`, +- `AppDeleteMethodCallParams`, +- `AppUpdateMethodCallParams`, + +Where `BuiltTransactions` looks like this: + +```python +@dataclass(frozen=True) +class BuiltTransactions: + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] + signers: dict[int, TransactionSigner] +``` + +This signifies the fact that an ABI method call can actually result in multiple transactions (which in turn may have different signers), that you need ABI metadata to be able to extract the return value from the transaction result. + +### Sending a single transaction + +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`]() class. Intellisense will guide you on the different options. + +Further documentation is present in the related capabilities: + +- [App management](app.md) +- [Asset management](asset.md) +- [Algo transfers](transfer.md) + +The signature for the calls to send a single transaction usually look like: + +`algorand.send.{method}(params=TxnParams, send_params=SendParams) -> SingleSendTransactionResult` + +- To get intellisense on the params, use your IDE’s intellisense keyboard shortcut (e.g. ctrl+space). +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils`. +- [`SendParams`]() a typed dictionary exposing setting to apply during send operation. +- [`SendSingleTransactionResult`]() is all of the information that is relevant when [sending a single transaction to the network](transaction.md#sending-a-transaction) + +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppressLog: true`. + +### Composing a group of transactions + +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{Type}()` methods on [`TransactionComposer`](transaction-composer.md) to add a series of transactions. + +```typescript +result = (algorand + .new_group() + .add_payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=1_000_000 # 1 Algo in microAlgos + ) + ) + .add_asset_opt_in( + AssetOptInParams( + sender="SENDERADDRESS", + asset_id=12345 + ) + ) + .send()) +``` + +`new_group()` returns a new [`TransactionComposer`](transaction-composer.md) instance, which can also return the group of transactions, simulate them and other things. + +### Transaction parameters + +To create a transaction you instantiate a relevant Transaction parameters dataclass from `algokit_utils.transactions import *` or `from algokit_utils import PaymentParams, AssetOptInParams, etc`. + +All transaction parameters share the following common base parameters: + +- [`CommonTransactionParams`]() + - `sender: str` - The address of the account sending the transaction. + - `signer: algosdk.TransactionSigner | TransactionSignerAccount | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured). + - `rekey_to: string | None` - Change the signing key of the sender to the given address. **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + - `note: bytes | str | None` - Note to attach to the transaction. Max of 1000 bytes. + - `lease: bytes | str | None` - Prevent multiple transactions with the same lease being included within the validity window. A [lease](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). + - Fee management + - `static_fee: AlgoAmount | None` - The static transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. + - `extra_fee: AlgoAmount | None` - The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. + - `max_fee: AlgoAmount | None` - Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. + - Round validity management + - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. + - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. + - `last_valid_round: int | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. + +Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. + +### Transaction configuration + +AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it’s set to `1000`. +- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) +- `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/markdown/capabilities/amount.md b/docs/markdown/capabilities/amount.md new file mode 100644 index 00000000..5770ef43 --- /dev/null +++ b/docs/markdown/capabilities/amount.md @@ -0,0 +1,55 @@ +# Algo amount handling + +Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. + +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the modularity principle) you can safely and explicitly convert to microAlgo or Algo. + +To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. + +## `AlgoAmount` + +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or existing the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it’s easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn’t be!). + +To import the AlgoAmount class you can access it via: + +```python +from algokit_utils import AlgoAmount +``` + +### Creating an `AlgoAmount` + +There are a few ways to create an `AlgoAmount`: + +- Algo + - Constructor: `AlgoAmount({"algo": 10})` or `AlgoAmount({"algos": 10})` + - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` +- microAlgo + - Constructor: `AlgoAmount({"microAlgo": 10_000})` or `AlgoAmount({"microAlgos": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` + +### Extracting a value from `AlgoAmount` + +The `AlgoAmount` class has properties to return Algo and microAlgo: + +- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer + +`AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. + +You can also call `str(amount)` or use an `AlgoAmount` directly in string interpolation to convert it to a nice user-facing formatted amount expressed in microAlgo. + +### Additional Features + +The `AlgoAmount` class supports arithmetic operations: + +- Addition: `amount1 + amount2` +- Subtraction: `amount1 - amount2` +- Comparison operations: `<`, `<=`, `>`, `>=`, `==`, `!=` + +Example: + +```python +amount1 = AlgoAmount({"algo": 1}) +amount2 = AlgoAmount({"microAlgo": 500_000}) +total = amount1 + amount2 # Results in 1.5 Algo +``` diff --git a/docs/markdown/capabilities/app-client.md b/docs/markdown/capabilities/app-client.md index abf57d3b..5953e5d2 100644 --- a/docs/markdown/capabilities/app-client.md +++ b/docs/markdown/capabilities/app-client.md @@ -1,154 +1,347 @@ -# App client +# App client and App factory -Application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker). +> [!NOTE] +> This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client is a high productivity application client that works with ARC-0032 application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](app-deploy.md) and [App management](app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_app_client_call.py). +> [!NOTE] +> If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don’t know the app ID (deferred knowledge or the instance doesn’t exist yet on the blockchain) or you have multiple app IDs -## Design +## `AppFactory` -The design for the app client is based on a wrapper for parsing an [ARC-0032](https://github.com/algorandfoundation/ARCs/pull/150) application spec and wrapping the [App deployment](app-deploy.md) functionality and corresponding [design](app-deploy.md#id1). +The `AppFactory` is a class that, for a given app spec, allows you to create and deploy one or more app instances and to create one or more app clients to interact with those (or other) app instances. -## Creating an application client +To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: -There are two key ways of instantiating an ApplicationClient: +```python +# Minimal example +factory = algorand.get_app_factory( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", +) + +# Advanced example +factory = algorand.get_app_factory( + app_spec=parsed_arc32_or_arc56_app_spec, + default_sender="SENDERADDRESS", + app_name="OverriddenAppName", + version="2.0.0", + compilation_params={ + "updatable": True, + "deletable": False, + "deploy_time_params": { "ONE": 1, "TWO": "value" }, + } +) +``` + +## `AppClient` -1. By app ID - When needing to call an existing app by app ID or unconditionally create a new app. - The signature `ApplicationClient(algod_client, app_spec, app_id=..., ...)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `app_id`: The app_id of an existing application, or 0 if creating a new app -2. By creator and app name - When needing to deploy or find an app associated with a specific creator account and app name. - The signature `ApplicationClient(algod_client, app_spec, creator=..., indexer=..., app_lookup)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `creator`: The address or `Account` of the creator of the app for which to search for the deployed app under - * `indexer`: - * `app_lookup`: Optional if an indexer is provided, - * `app_name`: An overridden name to identify the contract with, otherwise `contract.name` is used from the app spec +The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). -Both approaches also allow specifying the following parameters that will be used as defaults for all application calls: +To get an instance of `AppClient` you can use either `AlgorandClient` or instantiate it directly: -* `signer`: `TransactionSigner` to sign transactions with. -* `sender`: Address to use for transaction signing, will be derived from the signer if not provided. -* `suggested_params`: Default `SuggestedParams` to use, will use current network suggested params by default +```python +# Minimal examples +app_client = AppClient.from_creator_and_name( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + creator_address="CREATORADDRESS", + algorand=algorand, +) + +app_client = AppClient( + AppClientParams( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + app_id=12345, + algorand=algorand, + ) +) + +app_client = AppClient.from_network( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + algorand=algorand, +) + +# Advanced example +app_client = AppClient( + AppClientParams( + app_spec=parsed_app_spec, + app_id=12345, + algorand=algorand, + app_name="OverriddenAppName", + default_sender="SENDERADDRESS", + approval_source_map=approval_teal_source_map, + clear_source_map=clear_teal_source_map, + ) +) +``` -Both approaches also allow specifying a mapping of template values via the `template_values` parameter, this will be used before compiling the application to replace any -`TMPL_` variables that may be in the TEAL. The `TMPL_UPDATABLE` and `TMPL_DELETABLE` variables used in some AlgoKit templates are handled by the `deploy` method, but should be included if -using `create` or `update` directly. +You can access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. -## Calling methods on the app +## Dynamically creating clients for a given app spec -There are various methods available on `ApplicationClient` that can be used to call an app: +The `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. -* `call`: Used to call methods with an on complete action of `no_op` -* `create`: Used to create an instance of the app, by using an `app_id` of 0, includes the approval and clear programs in the call -* `update`: Used to update an existing app, includes the approval and clear programs in the call, and is called with an on complete action of `update_application` -* `delete`: Used to remove an existing app, is called with an on complete action of `delete_application` -* `opt_in`: Used to opt in to an existing app, is called with an on complete action of `opt_in` -* `close_out`: Used to close out of an existing app, is called with an on complete action of `opt_in` -* `clear_state`: Used to unconditionally close out from an app, calls the clear program of an app +This is possible via two methods on the app factory: -### Specifying which method +- `factory.get_app_client_by_id(app_id, ...)` - Returns a new `AppClient` for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified. +- `factory.get_app_client_by_creator_and_name(creator_address, app_name, ...)` - Returns a new `AppClient`, resolving the app by creator address and name using AlgoKit app deployment semantics. Automatically populates app_name, default_sender and source maps from the factory if not specified. -All methods for calling an app that support ABI methods (everything except `clear_state`) take a parameter `call_abi_method` which can be used to specify which method to call. -The method selected can be specified explicitly, or allow the client to infer the method where possible, supported values are: +```python +app_client1 = factory.get_app_client_by_id(app_id=12345) +app_client2 = factory.get_app_client_by_id(app_id=12346) +app_client3 = factory.get_app_client_by_id( + app_id=12345, + default_sender="SENDER2ADDRESS" +) + +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" +) +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" +) +app_client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName", + ignore_cache=True, # Perform fresh indexer lookups + default_sender="SENDER2ADDRESS" +) +``` -* `None`: The default value, when `None` is passed the client will attempt to find any ABI method or bare method that is compatible with the provided arguments -* `False`: Indicates that an ABI method should not be used, and instead a bare method call is made -* `True`: Indicates that an ABI method should be used, and the client will attempt to find an ABI method that is compatible with the provided arguments -* `str`: If a string is provided, it will be interpreted as either an ABI signature specifying a method, or as an ABI method name -* `algosdk.abi.Method`: The specified ABI method will be called -* `ABIReturnSubroutine`: Any type that has a `method_spec` function that returns an `algosd.abi.Method` +## Creating and deploying an app -### ABI arguments +Once you have an app factory you can perform the following actions: -ABI arguments are passed as python keyword arguments e.g. to pass the ABI parameter `name` for the ABI method `hello` the following syntax is used `client.call("hello", name="world")` +- `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it’s an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. -### Transaction Parameters +> See [API docs]() for details on parameter signatures. -All methods for calling an app take an optional `transaction_parameters` argument, with the following supported parameters: +### Create -* `signer`: The `TransactionSigner` to use on the call. This overrides any signer specified on the client -* `sender`: The address of the sender to use on the call, must be able to be signed for by the `signer`. This overrides any sender specified on the client -* `suggested_params`: `SuggestedParams` to use on the call. This overrides any suggested_params specified on the client -* `note`: Note to include in the transaction -* `lease`: Lease parameter for the transaction -* `boxes`: A sequence of boxes to use in the transaction, this is a list of (app_index, box_name) tuples `[(0, "box_name"), (0, ...)]` -* `accounts`: Account references to include in the transaction -* `foreign_apps`: Foreign apps to include in the transaction -* `foreign_assets`: Foreign assets to include in the transaction -* `on_complete`: The on complete action to use for the transaction, only available when using `call` or `create` -* `extra_pages`: Additional pages to allocate when calling `create`, by default a sufficient amount will be calculated based on the current approval and clear. This can be overridden, if more is required - for a future update +The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: -Parameters can be passed as one of the dataclasses `CommonCallParameters`, `OnCompleteCallParameters`, `CreateCallParameters` (exact type depends on method used) +- You don’t need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs]() for details. ```python -client.call("hello", transaction_parameters=algokit_utils.OnCompleteCallParameters(signer=...)) +# Use no-argument bare-call +result, app_client = factory.send.bare.create() + +# Specify parameters for bare-call and override other parameters +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( + args=[bytes([1, 2, 3, 4])], + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { + "ONE": 1, + "TWO": "two", + }, + "updatable": True, + "deletable": False, + } +) + +# Specify parameters for ABI method call +result, app_client = factory.send.create( + AppClientMethodCallParams( + method="create_application", + args=[1, "something"] + ) +) ``` -Alternatively, parameters can be passed as a dictionary e.g. +## Updating and deleting an app + +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app created via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. + +## Calling the app + +You can construct a params object, transaction(s) and sign and send a transaction to call the app that a given `AppClient` instance is pointing to. + +This is done via the following properties: + +- `app_client.params.{method}(params)` - Params for an ABI method call +- `app_client.params.bare.{method}(params)` - Params for a bare call +- `app_client.create_transaction.{method}(params)` - Transaction(s) for an ABI method call +- `app_client.create_transaction.bare.{method}(params)` - Transaction for a bare call +- `app_client.send.{method}(params)` - Sign and send an ABI method call +- `app_client.send.bare.{method}(params)` - Sign and send a bare call + +Where `{method}` is one of: + +- `update` - An update call +- `opt_in` - An opt-in call +- `delete` - A delete application call +- `clear_state` - A clear state call (note: calls the clear program and only applies to bare calls) +- `close_out` - A close-out call +- `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) ```python -client.call("hello", transaction_parameters={"signer":...}) +call1 = app_client.send.update( + AppClientMethodCallParams( + method="update_abi", + args=["string_io"], + ), + compilation_params={"deploy_time_params": deploy_time_params} +) + +call2 = app_client.send.delete( + AppClientMethodCallParams( + method="delete_abi", + args=["string_io"] + ) +) + +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") +) + +call4 = app_client.send.bare.clear_state() + +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) +) + +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") +) ``` -## Composing calls +## Funding the app account -If multiple calls need to be made in a single transaction, the `compose_` method variants can be used. All these methods take an `AtomicTransactionComposer` as their first argument. -Once all the calls have been added to the ATC, it can then be executed. For example: +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you via `fund_app_account(params)`. -```python -from algokit_utils import ApplicationClient -from algosdk.atomic_transaction_composer import AtomicTransactionComposer +The input parameters are: -client = ApplicationClient(...) -atc = AtomicTransactionComposer() -client.compose_call(atc, "hello", name="world") -... # additional compose calls +- A `FundAppAccountParams` object, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client’s default sender if configured). -response = client.execute_atc(atc) +Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you’ll want to get the funding call as a transaction, e.g.: + +```python +result = app_client.send.call( + AppClientMethodCallParams( + method="bootstrap", + args=[ + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) + ) + ) + ], + box_references=["Box1"] + ) +) ``` +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. + ## Reading state +`AppClient` has a number of mechanisms to read state (global, local and box storage) from the app instance. + +### App spec methods + +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the generic methods give you. + +You can access this functionality via: + +- `app_client.state.global_state.{method}()` - Global state +- `app_client.state.local_state(address).{method}()` - Local state +- `app_client.state.box.{method}()` - Box storage + +Where `{method}` is one of: + +- `get_all()` - Returns all single-key state values in a dict keyed by the key name and the value a decoded ABI value. +- `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be bytes with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value dict. It’s recommended that this is only done when you have a unique `prefix` for the map otherwise there’s a high risk that incorrect values will be included in the map. + +```python +values = app_client.state.global_state.get_all() +value = app_client.state.local_state("ADDRESS").get_value("value1") +map_value = app_client.state.box.get_map_value("map1", "mapKey") +map_dict = app_client.state.global_state.get_map("myMap") +``` + +### Generic methods + There are various methods defined that let you read state from the smart contract app: -* `get_global_state` - Gets the current global state of the app -* `get_local_state` - Gets the current local state for the given account address +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`]() +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](). + +```python +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box") +box_name2: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box2") + +box_names = app_client.get_box_names() +box_value = app_client.get_box_value(box_name) +box_values = app_client.get_box_values([box_name, box_name2]) +box_abi_value = app_client.get_box_value_from_abi_type( + box_name, + algosdk.ABIStringType +) +box_abi_values = app_client.get_box_values_from_abi_type( + [box_name, box_name2], + algosdk.ABIStringType +) +``` ## Handling logic errors and diagnosing errors -Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, -exhaustion of opcode budget, or any number of other reasons. +Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons. When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](app-deploy.md#id2) you can expose debugging -information that makes it much easier to understand what’s happening. +The information in that error message can be parsed and when combined with the [source map from compilation]() you can expose debugging information that makes it much easier to understand what’s happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. + +The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -When an error is thrown then the resulting error that is re-thrown will be a `LogicError`, which has the following fields: +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](), which has the following fields: -* `logic_error`: Original exception -* `program`: Program source (if available) -* `source_map`: Source map used (if available) -* `transaction_id`: Transaction ID of failing transaction -* `message`: The error message -* `line_no`: The line number in the TEAL program that -* `traces`: A list of Trace objects providing additional insights on simulation when debug mode is active. +- `logic_error: Exception` - The original logic error exception +- `logic_error_str: str` - The string representation of the logic error +- `program: str` - The TEAL program source code +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - Combined error message with debugging information +- `pc: int` - The program counter value where error occurred +- `traces: list[SimulationTrace] | None` - Simulation traces if debug enabled +- `line_no: int | None` - The line number in the TEAL source code +- `lines: list[str]` - The TEAL program split into individual lines -The function `trace()` will provide a formatted output of the surrounding TEAL where the error occurred. +Note: This information will only show if the app client / app factory has a source map. This will occur if: -#### NOTE -The extended information will only show if the Application Client has a source map. This will occur if: +- You have called `create`, `update` or `deploy` +- You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) +- You had source maps present in an app factory and then used it to [create an app client]() (they are automatically passed through) + +If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: + +```python +config.configure(debug=True) +``` -1.) The ApplicationClient instance has already called, `create, `update`or`deploy`OR 2.)`template_values`are provided when creating the ApplicationClient, so a SourceMap can be obtained automatically OR 3.)`approval_source_map`on`ApplicationClient`has been set from a previously compiled approval program OR 4.) A source map has been exported/imported using`export_source_map`/`import_source_map\`””” +If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. -### Debug Mode and traces Field +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](debugging.md). -When debug mode is active, the LogicError will contain a field named traces. This field will include raw simulate execution traces, providing a detailed account of the transaction simulation. These traces are crucial for diagnosing complex issues and are automatically included in all application client calls when debug mode is active. +## Default arguments -#### NOTE -Remember to enable debug mode (`config.debug = True`) to include raw simulate execution traces in the `LogicError`. +If an ABI method call specifies default argument values for any of its arguments you can pass in `None` for the value of that argument for the default value to be automatically populated. diff --git a/docs/markdown/capabilities/app-deploy.md b/docs/markdown/capabilities/app-deploy.md index 2fb69175..ff7cb202 100644 --- a/docs/markdown/capabilities/app-deploy.md +++ b/docs/markdown/capabilities/app-deploy.md @@ -1,19 +1,16 @@ # App deployment -Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution +AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet). -App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, -particularly [App management](app-client.md). It allows you to idempotently (with safe retryability) deploy an app, including deploy-time immutability and permanence control and -TEAL template substitution. +It’s optional to use this functionality, since you can construct your own deployment logic using create / update / delete calls and your own mechanism to maintaining app metadata (like app IDs etc.), but this capability is an opinionated out-of-the-box solution that takes care of the heavy lifting for you. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). +App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App management](app.md). - +To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -## Design +## Smart contract development lifecycle -The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). -While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. +The design behind the deployment capability is unique. The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. Namely, it described the concept of a smart contract development lifecycle: @@ -36,86 +33,182 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) and - [ARC-0004](https://arc.algorand.foundation/ARCs/arc-0004) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be - different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance -- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier - development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) -- Contracts are resolvable by a string “name” for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID - instead +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) and [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) +- Contracts are resolvable by a string “name” for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead + +This design allows you to have the same deployment code across environments without having to specify an ID for each environment. This makes it really easy to apply [continuous delivery](https://continuousdelivery.com/) practices to your smart contract deployment and make the deployment process completely automated. + +## `AppDeployer` -## Finding apps by creator +The [`AppDeployer`]() is a class that is used to manage app deployments and deployment metadata. + +To get an instance of `AppDeployer` you can use either [`AlgorandClient`](algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](app.md#appmanager), [`AlgorandClientTransactionSender`](algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): + +```python +from algokit_utils.app_deployer import AppDeployer + +app_deployer = AppDeployer(app_manager, transaction_sender, indexer) +``` -There is a method `algokit.get_creator_apps(creatorAccount, indexer)`, which performs a series of indexer lookups that return all apps created by the given creator. These are indexed by the name it -was deployed under if the creation transaction contained the following payload in the transaction note field: +## Deployment metadata + +When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. + +The deployment metadata is defined in [`AppDeployMetadata`](), which is an object with: + +- `name: str` - The unique name identifier of the app within the creator account +- `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) +- `deletable: bool | None` - Whether or not the app is deletable (`true`) / permanent (`false`) / unspecified (`None`) +- `updatable: bool | None` - Whether or not the app is updatable (`true`) / immutable (`false`) / unspecified (`None`) + +An example of the ARC-2 transaction note that is attached as an app creation / update transaction note to specify this metadata is: ```default -ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} +ALGOKIT_DEPLOYER:j{name:"MyApp",version:"1.0",updatable:true,deletable:false} ``` -Any creation transactions or update transactions are then retrieved and processed in chronological order to result in an `AppLookup` object +## Lookup deployed apps by name -Given there are a number of indexer calls to retrieve this data it’s a non-trivial object to create, and it’s recommended that for the duration you are performing a single deployment -you hold a value of it rather than recalculating it. Most AlgoKit Utils functions that need it will also take an optional value of it that will be used in preference to retrieving a -fresh version. +In order to resolve what apps have been previously deployed and their metadata, AlgoKit provides a method that does a series of indexer lookups and returns a map of name to app metadata via `get_creator_apps_by_name(creator_address)`. -## Deploying an application +```python +app_lookup = algorand.app_deployer.get_creator_apps_by_name("CREATORADDRESS") +app1_metadata = app_lookup.apps["app1"] +``` -The method that performs the deployment logic is the instance method `ApplicationClient.deploy`. It performs an idempotent (safely retryable) deployment. It will detect if the app already -exists and if it doesn’t it will create it. If the app does already exist then it will: +This method caches the result of the lookup, since it’s a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -- Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the - deployment configuration. +The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](): -It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. -This metadata works in concert with `get_creator_apps` to allow the app to be reliably retrieved against that creator in it’s currently deployed state. +```python +@dataclasses.dataclass +class ApplicationLookup: + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) +``` -`deploy` automatically executes [template substitution]() including deploy-time control of permanence and immutability. +The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](). + +> Refer to the [API docs]() for latest information on exact types. + +## Performing a deployment + +In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. + +For example: + +```python +deployment_result = algorand.app_deployer.deploy( + AppDeployParams( + metadata=AppDeploymentMetaData( + name="MyApp", + version="1.0.0", + deletable=False, + updatable=False, + ), + create_params=AppCreateParams( + sender="CREATORADDRESS", + approval_program=approval_teal_template_or_byte_code, + clear_state_program=clear_state_teal_template_or_byte_code, + schema=StateSchema( + global_ints=1, + global_byte_slices=2, + local_ints=3, + local_byte_slices=4, + ), + # Other parameters if a create call is made... + ), + update_params=AppUpdateParams( + sender="SENDERADDRESS", + # Other parameters if an update call is made... + ), + delete_params=AppDeleteParams( + sender="SENDERADDRESS", + # Other parameters if a delete call is made... + ), + deploy_time_params={ + "VALUE": 1, # TEAL template variables to replace + }, + on_schema_break=OnSchemaBreak.Append, + on_update=OnUpdate.Update, + send_params=SendParams( + populate_app_call_resources=True, + # Other execution control parameters + ), + ) +) +``` + +This method performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn’t it will create it. If the app does already exist then it will: + +- Detect if the app has been updated (i.e. the program logic has changed) and either fail, perform an update, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than were originally requested) and either fail, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. + +It will automatically [add metadata to the transaction note of the create or update transactions]() that indicates the name, version, updatability and deletability of the contract. This metadata works in concert with [`appDeployer.get_creator_apps_by_name`]() to allow the app to be reliably retrieved against that creator in it’s currently deployed state. It will automatically update it’s lookup cache so subsequent calls to `get_creator_apps_by_name` or `deploy` will use the latest metadata without needing to call indexer again. + +`deploy` also automatically executes [template substitution]() including deploy-time control of permanence and immutability if the requisite template parameters are specified in the provided TEAL template. ### Input parameters -The following inputs are used when deploying an App +The first parameter `deployment` is an [`AppDeployParams`](), which is an object with: -- `version`: The version string for the app defined in app_spec, if not specified the version will automatically increment for existing apps that are updated, and set to 1.0 for new apps -- `signer`, `sender`: Optional signer and sender for deployment operations, sender must be the same as the creator specified -- `allow_update`, `allow_delete`: Control the updatability and deletability of the app, used to populate `TMPL_UPDATABLE` and `TMPL_DELETABLE` template values -- `on_update`: Determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment) -- `on_schema_break`: Determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created) -- `create_args`: Args to use if a create operation is performed -- `update_args`: Args to use if an update operation is performed -- `delete_args`: Args to use if a delete operation is performed -- `template_values`: Values to use for automatic substitution of [deploy-time parameter values]() is mapping of `key: value` that will result in `TMPL_{key}` being replaced with `value` +- `metadata: AppDeployMetadata` - determines the [deployment metadata]() of the deployment +- `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](app.md#creation) (raw parameters or ABI method call) +- `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic +- `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter +- `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution]() + - [`TealTemplateParams`]() is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens]() if schema requirements increase (values: ‘replace’, ‘fail’, ‘append’) +- `on_update: OnUpdate | str | None` - determines [what happens]() if contract logic changes (values: ‘update’, ‘replace’, ‘fail’, ‘append’) +- `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries +- `ignore_cache: bool | None` - if True, bypasses cached deployment metadata +- Additional fields from [`SendParams`]() - transaction execution parameters ### Idempotency -`deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will -do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. - - +`deploy` is idempotent which means you can safely call it again multiple times and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. ### Compilation and template substitution -When compiling TEAL template code, the capabilities described in the [design above]() are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. +When compiling TEAL template code, the capabilities described in the [above design]() are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. -In order for a smart contract to be able to use this functionality, it must have a TEAL Template that contains the following: +In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which will be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn’t (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn’t (permanent) -If you are building a smart contract using the [beaker_production AlgoKit template](https://github.com/algorandfoundation/algokit-beaker-default-template) if provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. + +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result]() of substituting then compiling the TEAL template(s) in the following properties of the return value: + +- `compiled_approval: CompiledTeal | None` +- `compiled_clear: CompiledTeal | None` + +Template substitution is done by executing `algorand.app.compile_teal_template(teal_template_code, template_params, deployment_metadata)`, which in turn calls the following in order and returns the compilation result per above (all of which can also be invoked directly): + +- `AppManager.strip_teal_comments(teal_code)` - Strips out any TEAL comments to reduce the payload that is sent to algod and reduce the likelihood of hitting the max payload limit +- `AppManager.replace_template_variables(teal_template_code, template_values)` - Replaces the template variables by looking for `TMPL_{key}` +- `AppManager.replace_teal_template_deploy_time_control_params(teal_template_code, params)` - If `params` is provided, it allows for deploy-time immutability and permanence control by replacing `TMPL_UPDATABLE` with `params.get("updatable")` if not `None` and replacing `TMPL_DELETABLE` with `params.get("deletable")` if not `None` +- `algorand.app.compile_teal(teal_code)` - Sends the final TEAL to algod for compilation and returns the result including the source map and caches the compilation result within the `AppManager` instance ### Return value -`deploy` returns a `DeployResponse` object, that describes the action taken. - -- `action_taken`: Describes what happened during deployment - - `Create` - The smart contract app is created. - - `Update` - The smart contract app is updated - - `Replace` - The smart contract app was deleted and created again (in an atomic transaction) - - `Nothing` - Nothing was done since an existing up-to-date app was found -- `create_response`: If action taken was `Create` or `Replace`, the result of the create transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `update_response`: If action taken was `Update`, the result of the update transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `delete_response`: If action taken was `Replace`, the result of the delete transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `app`: An `AppMetaData` object, describing the final app state +When `deploy` executes it will return a [comprehensive result]() object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. + +The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): + +- `OperationPerformed.CREATE` - The smart contract app was created +- `OperationPerformed.UPDATE` - The smart contract app was updated +- `OperationPerformed.REPLACE` - The smart contract app was deleted and created again (in an atomic transaction) +- `OperationPerformed.NOTHING` - Nothing was done since it was detected the existing smart contract app deployment was up to date + +As well as the `operation_performed` parameter and the [optional compilation result](), the return value will have the [`ApplicationMetaData`]() [fields]() present. + +Based on the value of `operation_performed`, there will be other data available in the return value: + +- If `CREATE`, `UPDATE` or `REPLACE` then it will have the relevant [`SendAppTransactionResult`](app.md#calling-an-app) values: + - `create_result` for create operations + - `update_result` for update operations +- If `REPLACE` then it will also have `delete_result` to capture the result of deleting the existing app diff --git a/docs/markdown/capabilities/app.md b/docs/markdown/capabilities/app.md new file mode 100644 index 00000000..1cb907c3 --- /dev/null +++ b/docs/markdown/capabilities/app.md @@ -0,0 +1,163 @@ +# App management + +App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes). + +## `AppManager` + +The `AppManager` is a class that is used to manage app information. To get an instance of `AppManager` you can use either [`AlgorandClient`](algorand-client.md) via `algorand.app` or instantiate it directly (passing in an algod client instance): + +```python +from algokit_utils import AppManager + +app_manager = AppManager(algod_client) +``` + +## Calling apps + +### App Clients + +The recommended way of interacting with apps is via [App clients](app-client.md) and [App factory](app-client.md#appfactory). The methods shown on this page are the underlying mechanisms that app clients use and are for advanced use cases when you want more control. + +### Compilation + +The `AppManager` class allows you to compile TEAL code with caching semantics that allows you to avoid duplicate compilation and keep track of source maps from compiled code. + +```python +# Basic compilation +teal_code = "return 1" +compilation_result = app_manager.compile_teal(teal_code) + +# Get cached compilation result +cached_result = app_manager.get_compilation_result(teal_code) + +# Compile with template substitution +template_code = "int TMPL_VALUE" +template_params = {"VALUE": 1} +compilation_result = app_manager.compile_teal_template( + template_code, + template_params=template_params +) + +# Compile with deployment control (updatable/deletable) +control_template = f"""#pragma version 8 +int {UPDATABLE_TEMPLATE_NAME} +int {DELETABLE_TEMPLATE_NAME}""" +deployment_metadata = {"updatable": True, "deletable": True} +compilation_result = app_manager.compile_teal_template( + control_template, + deployment_metadata=deployment_metadata +) +``` + +The compilation result contains: + +- `teal` - Original TEAL code +- `compiled` - Base64 encoded compiled bytecode +- `compiled_hash` - Hash of compiled bytecode +- `compiled_base64_to_bytes` - Raw bytes of compiled bytecode +- `source_map` - Source map for debugging + +## Accessing state + +### Global state + +To access global state you can use: + +```python +# Get global state for app +global_state = app_manager.get_global_state(app_id) + +# Parse raw state from algod +decoded_state = AppManager.decode_app_state(raw_state) + +# Access state values +key_raw = decoded_state["value1"].key_raw # Raw bytes +key_base64 = decoded_state["value1"].key_base64 # Base64 encoded +value = decoded_state["value1"].value # Parsed value (str or int) +value_raw = decoded_state["value1"].value_raw # Raw bytes if bytes value +value_base64 = decoded_state["value1"].value_base64 # Base64 if bytes value +``` + +### Local state + +To access local state you can use: + +```python +local_state = app_manager.get_local_state(app_id, "ACCOUNT_ADDRESS") +``` + +### Boxes + +To access box storage: + +```python +# Get box names +box_names = app_manager.get_box_names(app_id) + +# Get box values +box_value = app_manager.get_box_value(app_id, box_name) +box_values = app_manager.get_box_values(app_id, [box_name1, box_name2]) + +# Get decoded ABI values +abi_value = app_manager.get_box_value_from_abi_type( + app_id, box_name, algosdk.abi.StringType() +) +abi_values = app_manager.get_box_values_from_abi_type( + app_id, [box_name1, box_name2], algosdk.abi.StringType() +) + +# Get box reference for transaction +box_ref = AppManager.get_box_reference(box_id) +``` + +## Getting app information + +To get app information: + +```python +# Get app info by ID +app_info = app_manager.get_by_id(app_id) + +# Get ABI return value from transaction +abi_return = AppManager.get_abi_return(confirmation, abi_method) +``` + +## Box references + +Box references can be specified in several ways: + +```python +# String name (encoded to bytes) +box_ref = "my_box" + +# Raw bytes +box_ref = b"my_box" + +# Account signer (uses address as name) +box_ref = account_signer + +# Box reference with app ID +box_ref = BoxReference(app_id=123, name=b"my_box") +``` + +## Common app parameters + +When interacting with apps (creating, updating, deleting, calling), there are common parameters that can be passed: + +- `app_id` - ID of the application +- `sender` - Address of transaction sender +- `signer` - Transaction signer (optional) +- `args` - Arguments to pass to the smart contract +- `account_references` - Account addresses to reference +- `app_references` - App IDs to reference +- `asset_references` - Asset IDs to reference +- `box_references` - Box references to load +- `on_complete` - On complete action +- Other common transaction parameters like `note`, `lease`, etc. + +For ABI method calls, additional parameters: + +- `method` - The ABI method to call +- `args` - ABI typed arguments to pass + +See [App client](app-client.md) for more details on constructing app calls. diff --git a/docs/markdown/capabilities/asset.md b/docs/markdown/capabilities/asset.md new file mode 100644 index 00000000..63a574b5 --- /dev/null +++ b/docs/markdown/capabilities/asset.md @@ -0,0 +1,134 @@ +# Assets + +The Algorand Standard Asset (ASA) management functions include creating, opting in and transferring assets, which are fundamental to asset interaction in a blockchain environment. + +## `AssetManager` + +The `AssetManager` class provides functionality for managing Algorand Standard Assets (ASAs). It can be accessed through the `AlgorandClient` via `algorand.asset` or instantiated directly: + +```python +from algokit_utils import AssetManager, TransactionComposer +from algosdk.v2client import algod + +asset_manager = AssetManager( + algod_client=algod_client, + new_group=lambda: TransactionComposer() +) +``` + +## Asset Information + +The `AssetManager` provides two key data classes for asset information: + +### `AssetInformation` + +Contains details about an Algorand Standard Asset (ASA): + +```python +@dataclass +class AssetInformation: + asset_id: int # The ID of the asset + creator: str # Address of the creator account + total: int # Total units created + decimals: int # Number of decimal places + default_frozen: bool | None = None # Whether asset is frozen by default + manager: str | None = None # Optional manager address + reserve: str | None = None # Optional reserve address + freeze: str | None = None # Optional freeze address + clawback: str | None = None # Optional clawback address + unit_name: str | None = None # Optional unit name (e.g. ticker) + asset_name: str | None = None # Optional asset name + url: str | None = None # Optional URL for more info + metadata_hash: bytes | None = None # Optional 32-byte metadata hash +``` + +### `AccountAssetInformation` + +Contains information about an account’s holding of a particular asset: + +```python +@dataclass +class AccountAssetInformation: + asset_id: int # The ID of the asset + balance: int # Amount held by the account + frozen: bool # Whether frozen for this account + round: int # Round this info was retrieved at +``` + +## Bulk Operations + +The `AssetManager` provides methods for bulk opt-in/opt-out operations: + +### Bulk Opt-In + +```python +# Basic example +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + signer=transaction_signer, + note=b"opt-in note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +### Bulk Opt-Out + +```python +# Basic example +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + ensure_zero_balance=True, + signer=transaction_signer, + note=b"opt-out note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +The bulk operations return a list of `BulkAssetOptInOutResult` objects containing: + +- `asset_id`: The ID of the asset opted into/out of +- `transaction_id`: The transaction ID of the opt-in/out + +## Get Asset Information + +### Getting Asset Parameters + +You can get the current parameters of an asset from algod using `get_by_id()`: + +```python +asset_info = asset_manager.get_by_id(12345) +``` + +### Getting Account Holdings + +You can get an account’s current holdings of an asset using `get_account_information()`: + +```python +address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +asset_id = 12345 +account_info = asset_manager.get_account_information(address, asset_id) +``` diff --git a/docs/markdown/capabilities/client.md b/docs/markdown/capabilities/client.md index 09973c11..1bc30811 100644 --- a/docs/markdown/capabilities/client.md +++ b/docs/markdown/capabilities/client.md @@ -1,29 +1,109 @@ # Client management -Client management is one of the core capabilities provided by AlgoKit Utils. -It allows you to create [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) -and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. +Client management is one of the core capabilities provided by AlgoKit Utils. It allows you to create (auto-retry) [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. -Any AlgoKit Utils function that needs one of these clients will take the underlying `algosdk` classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, -`algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#id1) principle you can use existing logic to get instances of these clients without needing to use the -Client management capability if you prefer. +Any AlgoKit Utils function that needs one of these clients will take the underlying algosdk classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, `algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#id1) principle you can use existing logic to get instances of these clients without needing to use the Client management capability if you prefer. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_network_clients.py). +## `ClientManager` + +The `ClientManager` is a class that is used to manage client instances. + +To get an instance of `ClientManager` you can instantiate it directly: + +```python +from algokit_utils import ClientManager, AlgoSdkClients, AlgoClientConfigs +from algosdk.v2client.algod import AlgodClient + +# Using AlgoSdkClients +algod_client = AlgodClient(...) +algorand_client = ... # Get AlgorandClient instance from somewhere +clients = AlgoSdkClients(algod=algod_client, indexer=indexer_client, kmd=kmd_client) +client_manager = ClientManager(clients, algorand_client) + +# Using AlgoClientConfigs +algod_config = AlgoClientNetworkConfig(server="https://...", token="") +configs = AlgoClientConfigs(algod_config=algod_config) +client_manager = ClientManager(configs, algorand_client) +``` + ## Network configuration -The network configuration is specified using the `AlgoClientConfig` class. This same interface is used to specify the config for algod, indexer and kmd clients. +The network configuration is specified using the `AlgoClientConfig` type. This same type is used to specify the config for [algod](https://developer.algorand.org/docs/sdks/python/), [indexer](https://developer.algorand.org/docs/sdks/python/) and [kmd](https://developer.algorand.org/docs/sdks/python/) SDK clients. There are a number of ways to produce one of these configuration objects: -- Manually creating the object, e.g. `AlgoClientConfig(server="https://myalgodnode.com", token="SECRET_TOKEN")` -- `algokit_utils.get_algonode_config(network, config, token)`: Loads an Algod or indexer config against [Nodely](https://nodely.io/docs/free/start) to either MainNet or TestNet -- `algokit_utils.get_default_localnet_config(configOrPort)`: Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration +- Manually specifying a dataclass, e.g. + ```python + from algokit_utils import AlgoClientNetworkConfig + + config = AlgoClientNetworkConfig( + server="https://myalgodnode.com", + token="SECRET_TOKEN" # optional + ) + ``` +- `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables +- `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algonode_config(network)` - Loads an Algod or indexer config against [AlgoNode free tier](https://nodely.io/docs/free/start) to either MainNet or TestNet +- `ClientManager.get_default_localnet_config()` - Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration ## Clients -Once you have the configuration for a client, to get the client you can use the following functions: +### Creating an SDK client instance + +Once you have the configuration for a client, to get a new client you can use the following functions: + +- `ClientManager.get_algod_client(config)` - Returns an Algod client for the given configuration; the client automatically retries on transient HTTP errors +- `ClientManager.get_indexer_client(config)` - Returns an Indexer client for given configuration +- `ClientManager.get_kmd_client(config)` - Returns a Kmd client for the given configuration + +You can also shortcut needing to write the likes of `ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())` with environment shortcut methods: + +- `ClientManager.get_algod_client_from_environment()` - Returns an Algod client by loading the config from environment variables +- `ClientManager.get_indexer_client_from_environment()` - Returns an indexer client by loading the config from environment variables +- `ClientManager.get_kmd_client_from_environment()` - Returns a kmd client by loading the config from environment variables + +### Accessing SDK clients via ClientManager instance + +Once you have a `ClientManager` instance, you can access the SDK clients: + +```python +client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) + +algod_client = client_manager.algod +indexer_client = client_manager.indexer +kmd_client = client_manager.kmd +``` + +If the method to create the `ClientManager` doesn’t configure indexer or kmd (both of which are optional), then accessing those clients will trigger an error. + +### Creating a TestNet dispenser API client instance + +You can also create a [TestNet dispenser API client instance](dispenser-client.md) from `ClientManager` too. + +## Automatic retry + +When receiving an Algod or Indexer client from AlgoKit Utils, it will be a special wrapper client that handles retrying transient failures. + +## Network information + +You can get information about the current network you are connected to: + +```python +# Get network information +network = client_manager.network() +print(f"Is mainnet: {network.is_mainnet}") +print(f"Is testnet: {network.is_testnet}") +print(f"Is localnet: {network.is_localnet}") +print(f"Genesis ID: {network.genesis_id}") +print(f"Genesis hash: {network.genesis_hash}") + +# Convenience methods +is_mainnet = client_manager.is_mainnet() +is_testnet = client_manager.is_testnet() +is_localnet = client_manager.is_localnet() +``` -- `algokit_utils.get_algod_client(config)`: Returns an Algod client for the given configuration or if none is provided retrieves a configuration from the environment using `ALGOD_SERVER`, `ALGOD_TOKEN` and optionally `ALGOD_PORT`. -- `algokit_utils.get_indexer_client(config)`: Returns an Indexer client for given configuration or if none is provided retrieves a configuration from the environment using `INDEXER_SERVER`, `INDEXER_TOKEN` and optionally `INDEXER_PORT` -- `algokit_utils.get_kmd_client_from_algod_client(config)`: - Returns a Kmd client based on the provided algod client configuration, with the assumption the KMD services is running on the same host but a different port (either `KMD_PORT` environment variable or `4002` by default) +The first time `network()` is called it will make a HTTP call to algod to get the network parameters, but from then on it will be cached within that `ClientManager` instance for subsequent calls. diff --git a/docs/markdown/capabilities/debugger.md b/docs/markdown/capabilities/debugging.md similarity index 63% rename from docs/markdown/capabilities/debugger.md rename to docs/markdown/capabilities/debugging.md index ac23a73e..3230212b 100644 --- a/docs/markdown/capabilities/debugger.md +++ b/docs/markdown/capabilities/debugging.md @@ -4,36 +4,64 @@ The AlgoKit Python Utilities package provides a set of debugging tools that can ## Configuration -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: +The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. - `debug`: Indicates whether debug mode is enabled. - `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. - `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. - `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. - `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. +- `populate_app_call_resources`: Indicates whether to populate app call resources. Defaults to false, which means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will not populate app call resources. The `configure` method can be used to set these attributes. To enable debug mode in your project you can configure it as follows: -```py +```python from algokit_utils.config import config -config.configure(debug=True) +config.configure( + debug=True, + project_root=Path("./my-project"), + trace_all=True, + trace_buffer_size_mb=512, + max_search_depth=15, + populate_app_call_resources=True, +) ``` ## Debugging Utilities -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: - -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. +When debug mode is enabled, AlgoKit Utils will automatically: + +- Generate transaction traces compatible with the AVM Debugger +- Manage trace file storage with automatic cleanup +- Provide source map generation for TEAL contracts + +The following methods are provided for manual debugging operations: + +- `persist_sourcemaps`: Persists sourcemaps for given TEAL contracts as AVM Debugger-compliant artifacts. Parameters: + - `sources`: List of TEAL sources to generate sourcemaps for + - `project_root`: Project root directory for storage + - `client`: AlgodClient instance + - `with_sources`: Whether to include TEAL source files (default: True) +- `simulate_and_persist_response`: Simulates transactions and persists debug traces. Parameters: + - `atc`: AtomicTransactionComposer containing transactions + - `project_root`: Project root directory for storage + - `algod_client`: AlgodClient instance + - `buffer_size_mb`: Maximum trace storage in MB (default: 256) + - `allow_empty_signatures`: Allow unsigned transactions (default: True) + - `allow_unnamed_resources`: Allow unnamed resources (default: True) + - `extra_opcode_budget`: Additional opcode budget + - `exec_trace_config`: Custom trace configuration + - `simulation_round`: Specific round to simulate ### Trace filename format The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; +```default +${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json ``` Where: diff --git a/docs/markdown/capabilities/dispenser-client.md b/docs/markdown/capabilities/dispenser-client.md index 9a8d4893..b04f8ef6 100644 --- a/docs/markdown/capabilities/dispenser-client.md +++ b/docs/markdown/capabilities/dispenser-client.md @@ -7,54 +7,85 @@ The TestNet Dispenser Client is a utility for interacting with the AlgoKit TestN To create a Dispenser Client, you need to provide an authorization token. This can be done in two ways: 1. Pass the token directly to the client constructor as `auth_token`. -2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) on how to obtain the token). +2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) on how to obtain the token). If both methods are used, the constructor argument takes precedence. -```py +```python +import algokit_utils + +# With auth token +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", +) + +# With auth token and timeout +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", + request_timeout=2, # seconds +) + +# From environment variables +# i.e. os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' +dispenser = algorand.client.get_testnet_dispenser_from_environment() + +# Alternatively, you can construct it directly from algokit_utils import TestNetDispenserApiClient # Using constructor argument - client = TestNetDispenserApiClient(auth_token="your_auth_token") # Using environment variable - import os -os.environ["ALGOKIT_DISPENSER_ACCESS_TOKEN"] = "your_auth_token" +os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' client = TestNetDispenserApiClient() ``` ## Funding an Account -To fund an account with Algos from the dispenser API, use the `fund` method. This method requires the receiver’s address, the amount to be funded, and the asset ID. +To fund an account with Algo from the dispenser API, use the `fund` method. This method requires the receiver’s address and the amount to be funded. -```py -response = client.fund(address="receiver_address", amount=1000, asset_id=0) +```python +response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, # Amount in microAlgos +) ``` -The `fund` method returns a `FundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. +The `fund` method returns a `DispenserFundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. ## Registering a Refund To register a refund for a transaction with the dispenser API, use the `refund` method. This method requires the transaction ID of the refund transaction. -```py -client.refund(refund_txn_id="transaction_id") +```python +dispenser.refund("transaction_id") ``` -> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by send funds back to TestNet Dispenser, then you can invoke this `refund` endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the `sender` field of any issued `fund` transaction initiated via [`fund`](). +> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by sending funds back to TestNet Dispenser, then you can invoke this refund endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the sender field of any issued fund transaction initiated via [fund](). ## Getting Current Limit -To get the current limit for an account with Algos from the dispenser API, use the `get_limit` method. This method requires the account address. +To get the current limit for an account with Algo from the dispenser API, use the `get_limit` method. -```py -response = client.get_limit(address="account_address") +```python +response = dispenser.get_limit() ``` -The `get_limit` method returns a `LimitResponse` object, which contains the current limit amount. +The `get_limit` method returns a `DispenserLimitResponse` object, which contains the current limit amount. ## Error Handling If an error occurs while making a request to the dispenser API, an exception will be raised with a message indicating the type of error. Refer to [Error Handling docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) for details on how you can handle each individual error `code`. + +Here’s an example of handling errors: + +```python +try: + response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, + ) +except Exception as e: + print(f"Error occurred: {str(e)}") +``` diff --git a/docs/markdown/capabilities/testing.md b/docs/markdown/capabilities/testing.md new file mode 100644 index 00000000..857c7ad8 --- /dev/null +++ b/docs/markdown/capabilities/testing.md @@ -0,0 +1,204 @@ +# Testing + +The following is a collection of useful snippets that can help you get started with testing your Algorand applications using AlgoKit utils. For the sake of simplicity, we’ll use [pytest](https://docs.pytest.org/en/latest/) in the examples below. + +## Basic Test Setup + +Here’s a basic test setup using pytest fixtures that provides common testing utilities: + +```python +import pytest +from algokit_utils import Account, SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.amount import AlgoAmount + +@pytest.fixture +def algorand() -> AlgorandClient: + """Get an AlgorandClient instance configured for LocalNet""" + return AlgorandClient.default_localnet() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + """Create and fund a test account with ALGOs""" + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_algos(100), + min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account +``` + +Refer to [pytest fixture scopes](https://docs.pytest.org/en/latest/how-to/fixtures.html#fixture-scopes) for more information on how to control lifecycle of fixtures. + +## Creating Test Assets + +Here’s a helper function to create test ASAs (Algorand Standard Assets): + +```python +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None = None) -> int: + """Create a test asset and return its ID""" + if total is None: + total = random.randint(20, 120) + + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TST", + asset_name=f"Test Asset {random.randint(1,100)}", + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) + ) + + return int(create_result.confirmation["asset-index"]) +``` + +## Testing Application Deployments + +Here’s how one can test smart contract application deployments: + +```python +def test_app_deployment(algorand: AlgorandClient, funded_account: SigningAccount): + """Test deploying a smart contract application""" + + # Load the application spec + app_spec = Path("artifacts/application.json").read_text() + + # Create app factory + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + + # Deploy the app + app_client, deploy_response = factory.deploy( + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, + ) + + # Verify deployment + assert deploy_response.app.app_id > 0 + assert deploy_response.app.app_address +``` + +## Testing Asset Transfers + +Here’s how one can test ASA transfers between accounts: + +```python +def test_asset_transfer(algorand: AlgorandClient, funded_account: SigningAccount): + """Test ASA transfers between accounts""" + + # Create receiver account + receiver = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=receiver, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1) + ) + + # Create test asset + asset_id = generate_test_asset(algorand, funded_account, 100) + + # Opt receiver into asset + algorand.send.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer + ) + ) + + # Transfer asset + transfer_amount = 5 + result = algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=receiver.address, + asset_id=asset_id, + amount=transfer_amount + ) + ) + + # Verify transfer + receiver_balance = algorand.asset.get_account_information(receiver, asset_id) + assert receiver_balance.balance == transfer_amount +``` + +## Testing Application Calls + +Here’s how to test application method calls: + +```python +def test_app_method_call(algorand: AlgorandClient, funded_account: SigningAccount): + """Test calling ABI methods on an application""" + + # Deploy application first + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Call application method + result = app_client.send.call( + AppClientMethodCallParams( + method="hello", + args=["world"] + ) + ) + + # Verify result + assert result.abi_return == "Hello, world" +``` + +## Testing Box Storage + +Here’s how to test application box storage: + +```python +def test_box_storage(algorand: AlgorandClient, funded_account: SigningAccount): + """Test application box storage""" + + # Deploy application + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Fund app account for box storage MBR + app_client.fund_app_account( + FundAppAccountParams(amount=AlgoAmount.from_algos(1)) + ) + + # Store value in box + box_name = b"test_box" + box_value = "test_value" + app_client.send.call( + AppClientMethodCallParams( + method="set_box", + args=[box_name, box_value], + box_references=[box_name] + ) + ) + + # Verify box value + stored_value = app_client.get_box_value(box_name) + assert stored_value == box_value.encode() +``` diff --git a/docs/markdown/capabilities/transaction-composer.md b/docs/markdown/capabilities/transaction-composer.md new file mode 100644 index 00000000..330b757b --- /dev/null +++ b/docs/markdown/capabilities/transaction-composer.md @@ -0,0 +1,228 @@ +# Transaction composer + +The `TransactionComposer` class allows you to easily compose one or more compliant Algorand transactions and execute and/or simulate them. + +It’s the core of how the `AlgorandClient` class composes and sends transactions. + +```python +from algokit_utils import TransactionComposer, AppManager +from algokit_utils.transactions import ( + PaymentParams, + AppCallMethodCallParams, + AssetCreateParams, + AppCreateParams, + # ... other transaction parameter types +) +``` + +To get an instance of `TransactionComposer` you can either get it from an app client, from an `AlgorandClient`, or by instantiating via the constructor. + +```python +# From AlgorandClient +composer_from_algorand = algorand.new_group() + +# From AppClient +composer_from_app_client = app_client.algorand.new_group() + +# From constructor +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer +) + +# From constructor with optional params +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer, + # Custom function to get suggested params + get_suggested_params=lambda: algod.suggested_params(), + # Number of rounds the transaction should be valid for + default_validity_window=1000, + # Optional AppManager instance for TEAL compilation + app_manager=AppManager(algod) +) +``` + +## Constructing a transaction + +To construct a transaction you need to add it to the composer, passing in the relevant params object for that transaction. Params are Python dataclasses aavailable for import from `algokit_utils.transactions`. + +Parameter types include: + +- `PaymentParams` - For ALGO transfers +- `AssetCreateParams` - For creating ASAs +- `AssetConfigParams` - For reconfiguring ASAs +- `AssetTransferParams` - For ASA transfers +- `AssetOptInParams` - For opting in to ASAs +- `AssetOptOutParams` - For opting out of ASAs +- `AssetDestroyParams` - For destroying ASAs +- `AssetFreezeParams` - For freezing ASA balances +- `AppCreateParams` - For creating applications +- `AppCreateMethodCallParams` - For creating applications with ABI method calls +- `AppCallParams` - For calling applications +- `AppCallMethodCallParams` - For calling ABI methods on applications +- `AppUpdateParams` - For updating applications +- `AppUpdateMethodCallParams` - For updating applications with ABI method calls +- `AppDeleteParams` - For deleting applications +- `AppDeleteMethodCallParams` - For deleting applications with ABI method calls +- `OnlineKeyRegistrationParams` - For online key registration transactions +- `OfflineKeyRegistrationParams` - For offline key registration transactions + +The methods to construct a transaction are all named `add_{transaction_type}` and return an instance of the composer so they can be chained together fluently to construct a transaction group. + +For example: + +```python +from algokit_utils import AlgoAmount +from algokit_utils.transactions import AppCallMethodCallParams, PaymentParams + +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100), + note=b"Payment note" + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + boxes=[box_reference] # Optional box references + )) +) +``` + +## Simulating a transaction + +Transactions can be simulated using the simulate endpoint in algod, which enables evaluating the transaction on the network without it actually being committed to a block. +This is a powerful feature, which has a number of options which are detailed in the [simulate API docs](https://developer.algorand.org/docs/rest-apis/algod/#post-v2transactionssimulate). + +The `simulate()` method accepts several optional parameters that are passed through to the algod simulate endpoint: + +- `allow_more_logs: bool | None` - Allow more logs than standard +- `allow_empty_signatures: bool | None` - Allow transactions without signatures +- `allow_unnamed_resources: bool | None` - Allow unnamed resources in app calls +- `extra_opcode_budget: int | None` - Additional opcode budget +- `exec_trace_config: SimulateTraceConfig | None` - Execution trace configuration +- `simulation_round: int | None` - Round to simulate at +- `skip_signatures: int | None` - Skip signature verification + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate() +) + +# Access simulation results +simulate_response = result.simulate_response +confirmations = result.confirmations +transactions = result.transactions +returns = result.returns # ABI returns if any +``` + +### Simulate without signing + +There are situations where you may not be able to (or want to) sign the transactions when executing simulate. +In these instances you should set `skip_signatures=True` which automatically builds empty transaction signers and sets both `fix-signers` and `allow-empty-signatures` to `True` when sending the algod API call. + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate( + skip_signatures=True, + allow_more_logs=True, # Optional: allow more logs + extra_opcode_budget=700 # Optional: increase opcode budget + ) +) +``` + +### Resource Population + +The `TransactionComposer` includes automatic resource population capabilities for application calls. When sending or simulating transactions, it can automatically detect and populate required references for: + +- Account references +- Application references +- Asset references +- Box references + +This happens automatically when either: + +1. The global `algokit_utils.config` instance is set to `populate_app_call_resources=True` (default is `False`) +2. The `populate_app_call_resources` parameter is explicitly passed as `True` when sending transactions + +```python +# Automatic resource population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + # Resources will be automatically populated! + )) + .send(params=SendParams(populate_app_call_resources=True)) +) + +# Or disable automatic population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + # Explicitly specify required resources + account_references=["ACCOUNT"], + app_references=[456], + asset_references=[789], + box_references=[box_reference] + )) + .send(params=SendParams(populate_app_call_resources=False)) +) +``` + +The resource population: + +- Respects the maximum limits (4 for accounts, 8 for foreign references) +- Handles cross-reference resources efficiently (e.g., asset holdings and local state) +- Automatically distributes resources across multiple transactions in a group when needed +- Raises descriptive errors if resource limits are exceeded + +This feature is particularly useful when: + +- Working with complex smart contracts that access various resources +- Building transaction groups where resources need to be coordinated +- Developing applications where resource requirements may change dynamically + +Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. diff --git a/docs/markdown/capabilities/transaction.md b/docs/markdown/capabilities/transaction.md new file mode 100644 index 00000000..c0f692e2 --- /dev/null +++ b/docs/markdown/capabilities/transaction.md @@ -0,0 +1,135 @@ +# Transaction management + +Transaction management is one of the core capabilities provided by AlgoKit Utils. It allows you to construct, simulate and send single or grouped transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, multiple sender account types, and sending behavior. + +## Transaction Results + +All AlgoKit Utils functions that send transactions return either a `SendSingleTransactionResult` or `SendAtomicTransactionComposerResults`, providing consistent mechanisms to interpret transaction outcomes. + +### SendSingleTransactionResult + +The base `SendSingleTransactionResult` class is used for single transactions: + +```python +@dataclass(frozen=True, kw_only=True) +class SendSingleTransactionResult: + transaction: TransactionWrapper # Last transaction + confirmation: AlgodResponseType # Last confirmation + group_id: str + tx_id: str | None = None # Transaction ID of the last transaction + tx_ids: list[str] # All transaction IDs in the group + transactions: list[TransactionWrapper] + confirmations: list[AlgodResponseType] + returns: list[ABIReturn] | None = None # ABI returns if applicable +``` + +Common variations include: + +- `SendSingleAssetCreateTransactionResult` - Adds `asset_id` +- `SendAppTransactionResult` - Adds `abi_return` +- `SendAppUpdateTransactionResult` - Adds compilation results +- `SendAppCreateTransactionResult` - Adds `app_id` and `app_address` + +### SendAtomicTransactionComposerResults + +When using the atomic transaction composer directly via `TransactionComposer.send()` or `TransactionComposer.simulate()`, you’ll receive a `SendAtomicTransactionComposerResults`: + +```python +@dataclass +class SendAtomicTransactionComposerResults: + group_id: str # The group ID if this was a transaction group + confirmations: list[AlgodResponseType] # The confirmation info for each transaction + tx_ids: list[str] # The transaction IDs that were sent + transactions: list[TransactionWrapper] # The transactions that were sent + returns: list[ABIReturn] # The ABI return values from any ABI method calls + simulate_response: dict[str, Any] | None = None # Simulation response if simulated +``` + +### Application-specific Result Types + +When working with applications via `AppClient` or `AppFactory`, you’ll get enhanced result types that provide direct access to parsed ABI values: + +- `SendAppFactoryTransactionResult` +- `SendAppUpdateFactoryTransactionResult` +- `SendAppCreateFactoryTransactionResult` + +These types extend the base transaction results to add an `abi_value` field that contains the parsed ABI return value according to the ARC-56 specification. The `Arc56ReturnValueType` can be: + +- A primitive ABI value (bool, int, str, bytes) +- An ABI struct (as a Python dict) +- None (for void returns) + +### Where You’ll Encounter Each Result Type + +Different interfaces return different result types: + +1. **Direct Transaction Composer** + - `TransactionComposer.send()` → `SendAtomicTransactionComposerResults` + - `TransactionComposer.simulate()` → `SendAtomicTransactionComposerResults` +2. **AlgorandClient Methods** + - `.send.payment()` → `SendSingleTransactionResult` + - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` (contains raw ABI return) + - `.send.app_create()` → `SendAppCreateTransactionResult` (with app ID/address) + - `.send.app_update()` → `SendAppUpdateTransactionResult` (with compilation info) +3. **AppClient Methods** + - `.call()` → `SendAppTransactionResult` + - `.create()` → `SendAppCreateTransactionResult` + - `.update()` → `SendAppUpdateTransactionResult` +4. **AppFactory Methods** + - `.create()` → `SendAppCreateFactoryTransactionResult` + - `.call()` → `SendAppFactoryTransactionResult` + - `.update()` → `SendAppUpdateFactoryTransactionResult` + +Example usage with AppFactory for easy access to ABI returns: + +```python +# Using AppFactory +result = app_factory.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Access the parsed ABI return value directly +parsed_value = result.abi_value # Already decoded per ARC-56 spec + +# Compared to base AppClient where you need to parse manually +base_result = app_client.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Need to manually handle ABI return parsing +if base_result.abi_return: + parsed_value = base_result.abi_return.value +``` + +Key differences between result types: + +1. **Base Transaction Results** (`SendSingleTransactionResult`) + - Focus on transaction confirmation details + - Include group support but optimized for single transactions + - No direct ABI value parsing +2. **Atomic Transaction Results** (`SendAtomicTransactionComposerResults`) + - Built for transaction groups + - Include simulation support + - Raw ABI returns via `.returns` + - No single transaction convenience fields +3. **Application Results** (`SendAppTransactionResult` family) + - Add application-specific fields (`app_id`, compilation results) + - Include raw ABI returns via `.abi_return` + - Base application transaction support +4. **Factory Results** (`SendAppFactoryTransactionResult` family) + - Highest level of abstraction + - Direct access to parsed ABI values via `.abi_value` + - Automatic ARC-56 compliant value parsing + - Combines app-specific fields with parsed ABI returns + +## Further reading + +To understand how to create, simulate and send transactions consult: + +- The [`TransactionComposer`](transaction-composer.md) documentation for composing transaction groups +- The [`AlgorandClient`](algorand-client.md) documentation for a high-level interface to send transactions + +The transaction composer documentation covers the details of constructing transactions and transaction groups, while the Algorand client documentation covers the high-level interface for sending transactions. diff --git a/docs/markdown/capabilities/transfer.md b/docs/markdown/capabilities/transfer.md index 0462a051..088f50a5 100644 --- a/docs/markdown/capabilities/transfer.md +++ b/docs/markdown/capabilities/transfer.md @@ -1,58 +1,151 @@ -# Algo transfers - -Algo transfers is a higher-order use case capability provided by AlgoKit Utils allows you to easily initiate algo transfers between accounts, including dispenser management and -idempotent account funding. - -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_transfer.py). - -## Transferring Algos - -The key function to facilitate Algo transfers is `algokit.transfer(algod_client, transfer_parameters)`, which returns the underlying `EnsureFundedResponse` and takes a `TransferParameters` - -The following fields on `TransferParameters` are required to transfer ALGOs: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `micro_algos`: The amount of micro ALGOs to send - -## Ensuring minimum Algos - -The ability to automatically fund an account to have a minimum amount of disposable ALGOs to spend is incredibly useful for automation and deployment scripts. -The function to facilitate this is `ensure_funded(client, parameters)`, which takes an `EnsureBalanceParameters` instance and returns the underlying `EnsureFundedResponse` if a payment was made, a string if the dispenser API was used, or None otherwise. - -The following fields on `EnsureBalanceParameters` are required to ensure minimum ALGOs: - -- `account_to_fund`: The account address that will receive the ALGOs. This can be an `Account` instance, an `AccountTransactionSigner` instance, or a string. -- `min_spending_balance_micro_algos`: The minimum balance of micro ALGOs that the account should have available to spend (i.e. on top of minimum balance requirement). -- `min_funding_increment_micro_algos`: When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets called often on an active account). Default is 0. -- `funding_source`: The account (with private key) or signer that will send the ALGOs. If not set, it will use `get_dispenser_account`. This can be an `Account` instance, an `AccountTransactionSigner` instance, [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) instance, or None. -- `suggested_params`: (optional) Transaction parameters, an instance of `SuggestedParams`. -- `note`: (optional) The transaction note, default is “Funding account to meet minimum requirement”. -- `fee_micro_algos`: (optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call. -- `max_fee_micro_algos`: (optional) The maximum fee that you are happy to pay (default: unbounded). If this is set it’s possible the transaction could get rejected during network congestion. - -The function calls Algod to find the current balance and minimum balance requirement, gets the difference between those two numbers and checks to see if it’s more than the `min_spending_balance_micro_algos`. If so, it will send the difference, or the `min_funding_increment_micro_algos` if that is specified. If the account is on TestNet and `use_dispenser_api` is True, the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md) will be used to fund the account. - -> Please note, if you are attempting to fund via Dispenser API, make sure to set `ALGOKIT_DISPENSER_ACCESS_TOKEN` environment variable prior to invoking `ensure_funded`. To generate the token refer to [AlgoKit CLI documentation](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) - -## Transfering Assets - -The key function to facilitate asset transfers is `transfer_asset(algod_client, transfer_parameters)`, which returns a `AssetTransferTxn` and takes a `TransferAssetParameters`: - -The following fields on `TransferAssetParameters` are required to transfer assets: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `asset_id`: The asset id that will be transfered -- `amount`: The amount to send as the smallest divisible unit value +# Algo transfers (payments) + +Algo transfers, or [payments](https://developer.algorand.org/docs/get-details/transactions/#payment-transaction), is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [Algo amount handling](amount.md) and [Transaction management](transaction.md). It allows you to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding. + +To see some usage examples check out the automated tests in the repository. + +## `payment` + +The key function to facilitate Algo transfers is `algorand.send.payment(params)` (immediately send a single payment transaction), `algorand.create_transaction.payment(params)` (construct a payment transaction), or `algorand.new_group().add_payment(params)` (add payment to a group of transactions) per [`AlgorandClient`](algorand-client.md) [transaction semantics](algorand-client.md#creating-and-issuing-transactions). + +The base type for specifying a payment transaction is `PaymentParams`, which has the following parameters in addition to the [common transaction parameters](algorand-client.md#transaction-parameters): + +- `receiver: str` - The address of the account that will receive the Algo +- `amount: AlgoAmount` - The amount of Algo to send +- `close_remainder_to: Optional[str]` - If given, close the sender account and send the remaining balance to this address (**warning:** use this carefully as it can result in loss of funds if used incorrectly) + +```python +# Minimal example +result = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo") + ) +) + +# Advanced example +result2 = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo"), + close_remainder_to="CLOSEREMAINDERTOADDRESS", + lease="lease", + note=b"note", + # Use this with caution, it's generally better to use algorand_client.account.rekey_account + rekey_to="REKEYTOADDRESS", + # You wouldn't normally set this field + first_valid_round=1000, + validity_window=10, + extra_fee=AlgoAmount(1000, "microalgo"), + static_fee=AlgoAmount(1000, "microalgo"), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee=AlgoAmount(3000, "microalgo"), + # Signer only needed if you want to provide one, + # generally you'd register it with AlgorandClient + # against the sender and not need to pass it in + signer=transaction_signer, + ), + send_params=SendParams( + max_rounds_to_wait=5, + suppress_log=True, + ) +) +``` + +## `ensure_funded` + +The `ensure_funded` function automatically funds an account to maintain a minimum amount of [disposable Algo](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance). This is particularly useful for automation and deployment scripts that get run multiple times and consume Algo when run. + +There are 3 variants of this function: + +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + - **Note:** requires environment variables to be set. + - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` + if it’s a rekeyed account, or against default LocalNet if no environment variables present. +- `algorand_client.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +The general structure of these calls is similar, they all take: + +- `account_to_fund: str | Account` - Address or signing account of the account to fund +- The source (dispenser): + - In `ensure_funded`: `dispenser_account: str | Account` - the address or signing account of the account to use as a dispenser + - In `ensure_funded_from_environment`: Not specified, loaded automatically from the ephemeral environment + - In `ensure_funded_from_testnet_dispenser_api`: `dispenser_client: TestNetDispenserApiClient` - a client instance of the TestNet dispenser API +- `min_spending_balance: AlgoAmount` - The minimum balance of Algo that the account should have available to spend (i.e., on top of the minimum balance requirement) +- An `options` object, which has: + - [Common transaction parameters](algorand-client.md#transaction-parameters) (not for TestNet Dispenser API) + - [Execution parameters](algorand-client.md#sending-a-single-transaction) (not for TestNet Dispenser API) + - `min_funding_increment: Optional[AlgoAmount]` - When issuing a funding amount, the minimum amount to transfer; this avoids many small transfers if this function gets called often on an active account + +### Examples + +```python +# From account + +# Basic example +algorand_client.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded( + "ACCOUNTADDRESS", + "DISPENSERADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# From environment + +# Basic example +algorand_client.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded_from_environment( + "ACCOUNTADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# TestNet Dispenser API + +# Basic example +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo") +) +# With configuration +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), +) +``` + +All 3 variants return an `EnsureFundedResponse` (and the first two also return a [single transaction result](algorand-client.md#sending-a-single-transaction)) if a funding transaction was needed, or `None` if no transaction was required: + +- `amount_funded: AlgoAmount` - The number of Algo that was paid +- `transaction_id: str` - The ID of the transaction that funded the account + +If you are using the TestNet Dispenser API then the `transaction_id` is useful if you want to use the [refund functionality](dispenser-client.md#registering-a-refund). ## Dispenser -If you want to programmatically send funds then you will often need a “dispenser” account that has a store of ALGOs that can be sent and a private key available for that dispenser account. +If you want to programmatically send funds to an account so it can transact then you will often need a “dispenser” account that has a store of Algo that can be sent and a private key available for that dispenser account. -There is a standard AlgoKit Utils function to get access to a [dispenser account](account.md#id1): `get_dispenser_account`. When running against -[LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md), the dispenser account can be automatically determined using the -[Kmd API](https://developer.algorand.org/docs/rest-apis/kmd). When running against other networks like TestNet or MainNet the mnemonic of the dispenser account can be provided via environment -variable `DISPENSER_MNEMONIC` +There’s a number of ways to get a dispensing account in AlgoKit Utils: -Please note that this does not refer to the [AlgoKit TestNet Dispenser API](dispenser-client.md) which is a separate abstraction that can be used to fund accounts on TestNet via dedicated API service. +- Get a dispenser via [account manager](account.md#dispenser) - either automatically from [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) or from the environment +- By programmatically creating one of the many account types via [account manager](account.md#accounts) +- By programmatically interacting with [KMD](account.md#kmd-account-management) if running against LocalNet +- By using the [AlgoKit TestNet Dispenser API client](dispenser-client.md) which can be used to fund accounts on TestNet via a dedicated API service diff --git a/docs/markdown/capabilities/typed-app-clients.md b/docs/markdown/capabilities/typed-app-clients.md new file mode 100644 index 00000000..099f3c50 --- /dev/null +++ b/docs/markdown/capabilities/typed-app-clients.md @@ -0,0 +1,194 @@ +# Typed application clients + +Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) or [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract. + +Typed application clients are the recommended way of interacting with smart contracts. If you don’t have/want a typed client, but have an ARC-56/ARC-32 app spec then you can use the [non-typed application clients](app-client.md) and if you want to call a smart contract you don’t have an app spec file for you can use the underlying [app management](app.md) and [app deployment](app-deploy.md) functionality to manually construct transactions. + +## Generating an app spec + +You can generate an app spec file: + +- Using [Algorand Python](https://algorandfoundation.github.io/puya/#quick-start) +- Using [TEALScript](https://tealscript.netlify.app/tutorials/hello-world/0004-artifacts/) +- By hand by following the specification [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258)/[ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) +- Using [Beaker](https://algorand-devrel.github.io/beaker/html/usage.html) (PyTEAL) *(DEPRECATED)* + +## Generating a typed client + +To generate a typed client from an app spec file you can use [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients): + +```default +> algokit generate client application.json --output /absolute/path/to/client.py +``` + +Note: AlgoKit Utils >= 3.0.0 is compatible with the older 1.x.x generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 2.x.x. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version. + +## Getting a typed client instance + +To get an instance of a typed client you can use an [`AlgorandClient`](algorand-client.md) instance or a typed app [`Factory`]() instance. + +The approach to obtaining a client instance depends on how many app clients you require for a given app spec and if the app has already been deployed, which is summarised below: + +### App is deployed + + + + + + + + + + + + + + + + + + + + + + +
Resolve App by IDResolve App by Creator and Name
Single App Client InstanceMultiple App Client InstancesSingle App Client InstanceMultiple App Client Instances
+```python +app_client = algorand.client.get_typed_app_client_by_id(MyContractClient, { + app_id=1234, + # ... +}) +# or +app_client = MyContractClient({ + algorand, + app_id=1234, + # ... +}) +``` + + +```python +app_client1 = factory.get_app_client_by_id( + app_id=1234, + # ... +) +app_client2 = factory.get_app_client_by_id( + app_id=4321, + # ... +) +``` + + +```python +app_client = algorand.client.get_typed_app_client_by_creator_and_name( + MyContractClient, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +# or +app_client = MyContractClient.from_creator_and_name( + algorand, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +``` + + +```python +app_client1 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +app_client2 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name-2", + # ... +) +``` + +
+ +To understand the difference between resolving by ID vs by creator and name see the underlying [app client documentation](app-client.md#appclient). + +### App is not deployed + + + + + + + + + + + + + + +
Deploy a New AppDeploy or Resolve App Idempotently by Creator and Name
+```python +app_client, response = factory.deploy( + args=[], + # ... +) +# or +app_client, response = factory.send.create.METHODNAME( + args=[], + # ... +) +``` + + +```python +app_client, response = factory.deploy( + app_name="contract-name", + # ... +) +``` + +
+ +### Creating a typed factory instance + +If your scenario calls for an app factory, you can create one using the below: + +```python +factory = algorand.client.get_typed_app_factory(MyContractFactory) +# or +factory = MyContractFactory(algorand) +``` + +## Client usage + +See the [official usage docs](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/usage.md) for full details. + +For a simple example that deploys a contract and calls a `"hello"` method, see below: + +```python +# A similar working example can be seen in the AlgoKit init production smart contract templates, when using Python deployment +# In this case the generated factory is called `HelloWorldAppFactory` and is in `./artifacts/HelloWorldApp/client.py` +from artifacts.hello_world_app.client import HelloWorldAppClient, HelloArgs +from algokit_utils import AlgorandClient + +# These require environment variables to be present, or it will retrieve from default LocalNet +algorand = AlgorandClient.from_environment() +deployer = algorand.account.from_environment("DEPLOYER", AlgoAmount.from_algo(1)) + +# Create the typed app factory +factory = algorand.client.get_typed_app_factory(HelloWorldAppFactory, + creator_address=deployer, + default_sender=deployer, +) + +# Create the app and get a typed app client for the created app (note: this creates a new instance of the app every time, +# you can use .deploy() to deploy idempotently if the app wasn't previously +# deployed or needs to be updated if that's allowed) +app_client, response = factory.send.create() + +# Make a call to an ABI method and print the result +response = app_client.send.hello(args=HelloArgs(name="world")) +print(response) +``` diff --git a/docs/markdown/index.md b/docs/markdown/index.md index a3fa0518..f3ecf895 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -1,72 +1,128 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. -This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. -Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. #### NOTE If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). -[Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]() +[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Config and logging](#config-logging) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) # Contents * [Account management](capabilities/account.md) - * [`Account`](capabilities/account.md#account) -* [Client management](capabilities/client.md) - * [Network configuration](capabilities/client.md#network-configuration) - * [Clients](capabilities/client.md#clients) -* [App client](capabilities/app-client.md) - * [Design](capabilities/app-client.md#design) - * [Creating an application client](capabilities/app-client.md#creating-an-application-client) - * [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) - * [Composing calls](capabilities/app-client.md#composing-calls) + * [`AccountManager`](capabilities/account.md#accountmanager) + * [`TransactionSignerAccountProtocol`](capabilities/account.md#transactionsigneraccountprotocol) + * [Registering a signer](capabilities/account.md#registering-a-signer) + * [Default signer](capabilities/account.md#default-signer) + * [Get a signer](capabilities/account.md#get-a-signer) + * [Accounts](capabilities/account.md#accounts) + * [Rekey account](capabilities/account.md#rekey-account) + * [KMD account management](capabilities/account.md#kmd-account-management) +* [Algorand client](capabilities/algorand-client.md) + * [Accessing SDK clients](capabilities/algorand-client.md#accessing-sdk-clients) + * [Accessing manager class instances](capabilities/algorand-client.md#accessing-manager-class-instances) + * [Creating and issuing transactions](capabilities/algorand-client.md#creating-and-issuing-transactions) +* [Algo amount handling](capabilities/amount.md) + * [`AlgoAmount`](capabilities/amount.md#algoamount) +* [App client and App factory](capabilities/app-client.md) + * [`AppFactory`](capabilities/app-client.md#appfactory) + * [`AppClient`](capabilities/app-client.md#appclient) + * [Dynamically creating clients for a given app spec](capabilities/app-client.md#dynamically-creating-clients-for-a-given-app-spec) + * [Creating and deploying an app](capabilities/app-client.md#creating-and-deploying-an-app) + * [Updating and deleting an app](capabilities/app-client.md#updating-and-deleting-an-app) + * [Calling the app](capabilities/app-client.md#calling-the-app) + * [Funding the app account](capabilities/app-client.md#funding-the-app-account) * [Reading state](capabilities/app-client.md#reading-state) * [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) + * [Default arguments](capabilities/app-client.md#default-arguments) * [App deployment](capabilities/app-deploy.md) - * [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) + * [Smart contract development lifecycle](capabilities/app-deploy.md#smart-contract-development-lifecycle) + * [`AppDeployer`](capabilities/app-deploy.md#appdeployer) + * [Deployment metadata](capabilities/app-deploy.md#deployment-metadata) + * [Lookup deployed apps by name](capabilities/app-deploy.md#lookup-deployed-apps-by-name) + * [Performing a deployment](capabilities/app-deploy.md#performing-a-deployment) +* [App management](capabilities/app.md) + * [`AppManager`](capabilities/app.md#appmanager) + * [Calling apps](capabilities/app.md#calling-apps) + * [Accessing state](capabilities/app.md#accessing-state) + * [Getting app information](capabilities/app.md#getting-app-information) + * [Box references](capabilities/app.md#box-references) + * [Common app parameters](capabilities/app.md#common-app-parameters) +* [Assets](capabilities/asset.md) + * [`AssetManager`](capabilities/asset.md#assetmanager) + * [Asset Information](capabilities/asset.md#asset-information) + * [Bulk Operations](capabilities/asset.md#bulk-operations) + * [Get Asset Information](capabilities/asset.md#get-asset-information) +* [Client management](capabilities/client.md) + * [`ClientManager`](capabilities/client.md#clientmanager) + * [Network configuration](capabilities/client.md#network-configuration) + * [Clients](capabilities/client.md#clients) + * [Automatic retry](capabilities/client.md#automatic-retry) + * [Network information](capabilities/client.md#network-information) +* [Debugger](capabilities/debugging.md) + * [Configuration](capabilities/debugging.md#configuration) + * [Debugging Utilities](capabilities/debugging.md#debugging-utilities) * [TestNet Dispenser Client](capabilities/dispenser-client.md) * [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) * [Funding an Account](capabilities/dispenser-client.md#funding-an-account) * [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) * [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) * [Error Handling](capabilities/dispenser-client.md#error-handling) -* [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) +* [Testing](capabilities/testing.md) + * [Basic Test Setup](capabilities/testing.md#basic-test-setup) + * [Creating Test Assets](capabilities/testing.md#creating-test-assets) + * [Testing Application Deployments](capabilities/testing.md#testing-application-deployments) + * [Testing Asset Transfers](capabilities/testing.md#testing-asset-transfers) + * [Testing Application Calls](capabilities/testing.md#testing-application-calls) + * [Testing Box Storage](capabilities/testing.md#testing-box-storage) +* [Transaction composer](capabilities/transaction-composer.md) + * [Constructing a transaction](capabilities/transaction-composer.md#constructing-a-transaction) + * [Simulating a transaction](capabilities/transaction-composer.md#simulating-a-transaction) +* [Transaction management](capabilities/transaction.md) + * [Transaction Results](capabilities/transaction.md#transaction-results) + * [Further reading](capabilities/transaction.md#further-reading) +* [Algo transfers (payments)](capabilities/transfer.md) + * [`payment`](capabilities/transfer.md#payment) + * [`ensure_funded`](capabilities/transfer.md#ensure-funded) + * [Dispenser](capabilities/transfer.md#dispenser) +* [Typed application clients](capabilities/typed-app-clients.md) + * [Generating an app spec](capabilities/typed-app-clients.md#generating-an-app-spec) + * [Generating a typed client](capabilities/typed-app-clients.md#generating-a-typed-client) + * [Getting a typed client instance](capabilities/typed-app-clients.md#getting-a-typed-client-instance) + * [Client usage](capabilities/typed-app-clients.md#client-usage) +* [Migration Guide - v3](v3-migration-guide.md) + * [Migration Steps](v3-migration-guide.md#migration-steps) + * [Breaking Changes](v3-migration-guide.md#breaking-changes) + * [Best Practices](v3-migration-guide.md#best-practices) + * [Troubleshooting](v3-migration-guide.md#troubleshooting) +* [API Reference](autoapi/index.md) + * [composer](autoapi/composer/index.md) + * [algokit_utils](autoapi/algokit_utils/index.md) + * [client_manager](autoapi/client_manager/index.md) + * [algorand_client](autoapi/algorand_client/index.md) + * [account_manager](autoapi/account_manager/index.md) # Core principles -This library is designed with the following principles: +This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles) and is designed with the following principles: -- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are - exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. -- **Type-safety** - This library provides strong TypeScript support with effort put into creating types that provide good type safety and intellisense. -- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write +- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. +- **Type-safety** - This library provides strong type hints with effort put into creating types that provide good type safety and intellisense when used with tools like MyPy. +- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write. # Installation -This library can be installed from PyPi using pip or poetry, e.g.: +This library can be installed from PyPi using pip or poetry: -```default +```bash pip install algokit-utils +# or poetry add algokit-utils ``` @@ -74,50 +130,96 @@ poetry add algokit-utils # Usage -To use this library simply include the following at the top of your file: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: ```python -import algokit_utils +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=...) +# Point to a pre-created algod and indexer client +algorand = AlgorandClient.from_clients(algod=..., indexer=..., kmd=...) +# Point to custom configuration for algod +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod and indexer and kmd +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +indexer_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +kmd_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) ``` -Then you can use intellisense to auto-complete the various functions and types that are available by typing `algokit_utils.` in your favourite Integrated Development Environment (IDE), -or you can refer to the [reference documentation](apidocs/algokit_utils/algokit_utils.md). +# Testing -## Types +AlgoKit Utils provides a dedicated documentation page on various useful snippets that can be reused for testing with tools like [Pytest](https://docs.pytest.org/en/latest/): -The library contains extensive type hinting combined with a tool like MyPy this can help identify issues where incorrect types have been used, or used incorrectly. +- [Testing](capabilities/testing.md) - +# Types -# Capabilities +The library leverages Python’s native type hints and is fully compatible with [MyPy](https://mypy-lang.org/) for static type checking. -The library helps you with the following capabilities: +All public abstractions and methods are organized in logical modules matching their domain functionality. You can import types either directly from the root module or from their source submodules. Refer to [API documentation](autoapi/index.md) for more details. -- Core capabilities - - [**Client management**](capabilities/client.md) - Creation of algod, indexer and kmd clients against various networks resolved from environment or specified configuration - - [**Account management**](capabilities/account.md) - Creation and use of accounts including mnemonic, multisig, transaction signer, idempotent KMD accounts and environment variable injected -- Higher-order use cases - - [**ARC-0032 Application Spec client**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities to provide a high productivity application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker) - - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution - - [**Algo transfers**](capabilities/transfer.md) - Ability to easily initiate algo transfers between accounts, including dispenser management and idempotent account funding - - [**Debugger**](capabilities/debugger.md) - Provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AVM Debugger extension](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). + - +# Config and logging -# Reference documentation +To configure the AlgoKit Utils library you can make use of the [`Config`](autoapi/algokit_utils/config/index.md) object, which has a configure method that lets you configure some or all of the configuration options. + +## Config singleton + +The AlgoKit Utils configuration singleton can be updated using `config.configure()`. Refer to the [Config API documentation](autoapi/algokit_utils/config/index.md) for more details. -We have [auto-generated reference documentation for the code](apidocs/algokit_utils/algokit_utils.md). +## Logging -# Roadmap +AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`]() class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. -This library will naturally evolve with any logical developer experience improvements needed to facilitate the [AlgoKit](https://github.com/algorandfoundation/algokit-cli) roadmap as it evolves. +Each method supports optional suppression of output using the `suppress_log` parameter. -Likely future capability additions include: +## Debug mode -- Typed application client -- Asset management -- Expanded indexer API wrapper support +To turn on debug mode you can use the following: -# Indices and tables +```python +from algokit_utils.config import config +config.configure(debug=True) +``` + +To retrieve the current debug state you can use `debug` property. + +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](). It’s likely this option will result in extra HTTP calls to algod os worth being careful when it’s turned on. + + + +# Capabilities + +The library helps you interact with and develop against the Algorand blockchain with a series of end-to-end capabilities as described below: + +- [**AlgorandClient**](capabilities/algorand-client.md) - The key entrypoint to the AlgoKit Utils functionality +- **Core capabilities** + - [**Client management**](capabilities/client.md) - Creation of (auto-retry) algod, indexer and kmd clients against various networks resolved from environment or specified configuration, and creation of other API clients (e.g. TestNet Dispenser API and app clients) + - [**Account management**](capabilities/account.md) - Creation, use, and management of accounts including mnemonic, rekeyed, multisig, transaction signer, idempotent KMD accounts and environment variable injected + - [**Algo amount handling**](capabilities/amount.md) - Reliable, explicit, and terse specification of microAlgo and Algo amounts and safe conversion between them + - [**Transaction management**](capabilities/transaction.md) - Ability to construct, simulate and send transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, validity, signing, and sending behaviour +- **Higher-order use cases** + - [**Asset management**](capabilities/asset.md) - Creation, transfer, destroying, opting in and out and managing Algorand Standard Assets + - [**Typed application clients**](capabilities/typed-app-clients.md) - Type-safe application clients that are [generated](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients) from ARC-56 or ARC-32 application spec files and allow you to intuitively and productively interact with a deployed app, which is the recommended way of interacting with apps and builds on top of the following capabilities: + - [**ARC-56 / ARC-32 App client and App factory**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities (below) to provide a high productivity application client that works with ARC-56 and ARC-32 application spec defined smart contracts + - [**App management**](capabilities/app.md) - Creation, updating, deleting, calling (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes) + - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution + - [**Algo transfers (payments)**](capabilities/transfer.md) - Ability to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding + - [**Automated testing**](capabilities/testing.md) - Reusable snippets to leverage AlgoKit Utils abstractions in a manner that are useful for when writing tests in tools like [Pytest](https://docs.pytest.org/en/latest/). + + + +# Reference documentation -- [Index](genindex.md) +For detailed API documentation, see the [auto-generated reference documentation](). diff --git a/docs/markdown/v3-migration-guide.md b/docs/markdown/v3-migration-guide.md new file mode 100644 index 00000000..710d6850 --- /dev/null +++ b/docs/markdown/v3-migration-guide.md @@ -0,0 +1,304 @@ +# Migration Guide - v3 + +Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: + +- Easier and simpler consumption experience guided by IDE autocompletion +- Less redundant parameter passing (e.g., `algod` client) +- Better performance through caching of commonly retrieved values like transaction parameters +- More consistent and intuitive API design +- Stronger type safety and better error messages +- Improved ARC-56 compatibility +- Feature parity with `algokit-utils-ts` >= `v7` interfaces + +The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. + +The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, *all v2 abstractions are available* with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. + +> BREAKING CHANGE: the `beta` module is now removed, any imports from `algokit_utils.beta` will now raise an error with a link to a new expected import path. This is due to the fact that the interfaces introduced in `beta` are now refined and available in the main module. + +## Migration Steps + +In general, your codebase might fall into one of the following migration scenarios: + +- Using `algokit-utils-py` v2.x only without use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x only and with use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x with `algokit-client-generator-py` v1.x +- Using `algokit-client-generator-py` v1.x only (implies implicit dependency on `algokit-utils-py` v2.x) + +Given that `algokit-utils-py` v3.x is backwards compatible with `algokit-client-generator-py` v1.x, the following general guidelines are applicable to all scenarios (note that the order of operations is important to ensure straight-forward migration): + +1. Upgrade to `algokit-utils-py` v3.x + - 1.1 (If used) Update imports from `algokit_utils.beta` to `algokit_utils` + - 1.2 Follow hints in deprecation warnings to update your codebase to rely on latest v3 interfaces +2. Upgrade to `algokit-client-generator-py` v2.x and regenerate typed clients + - 2.1 Follow `algokit-client-generator-py` [v2.x migration guide](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/v2-migration-guide.md) + +The remaining set of guidelines are outlining migrations for specific abstractions that had direct equivalents in `algokit-utils-py` v2.x. + +### Prerequisites + +It is important to reiterate that if you have previously relied on `beta` versions of `algokit-utils-py` v2.x, you will need to update your imports to rely on the new interfaces. Errors thrown during import from `beta` will provide a description of the new expected import path. + +> As with `v2.x` all public abstractions in `algokit_utils` are available for direct imports `from algokit_utils import ...`, however underlying modules have been refined to be structured loosely around common AVM domains such as `applications`, `transactions`, `accounts`, `assets`, etc. See [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) for latest and detailed overview. + +### Step 1 - Replace SDK Clients with AlgorandClient + +First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: + +```python +"""Before""" +import algokit_utils +algod = algokit_utils.get_algod_client() +indexer = algokit_utils.get_indexer_client() + +"""After""" +from algokit_utils import AlgorandClient +algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. +``` + +During migration, you can still access SDK clients if needed: + +```python +algod = algorand.client.algod +indexer = algorand.client.indexer +kmd = algorand.client.kmd +``` + +### Step 2 - Update Account Management + +Account management has moved to `algorand.account`: + +#### Before: + +```python +account = algokit_utils.get_account_from_mnemonic( + mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), +) +dispenser = algokit_utils.get_dispenser_account(algod) +``` + +#### After: + +```python +account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) +dispenser = algorand.account.dispenser_from_environment() +``` + +Key changes: + +- `get_account` → `account.from_environment` +- `get_account_from_mnemonic` → `account.from_mnemonic` +- `get_dispenser_account` → `account.dispenser_from_environment` +- `get_localnet_default_account` → `account.localnet_dispenser` + +### Step 3 - Update Transaction Management + +Transaction creation and sending is now more structured: + +#### Before: + +```python +# Single transaction +result = algokit_utils.transfer_algos( + from_account=account, + to_addr="RECEIVER", + amount=algokit_utils.algos(1), + algod_client=algod, +) + +# Transaction groups +atc = AtomicTransactionComposer() +# ... add transactions ... +result = algokit_utils.execute_atc_with_logic_error(atc, algod) +``` + +#### After: + +```python +# Single transaction +result = algorand.send.payment( + sender=account.address, + receiver="RECEIVER", + amount=AlgoAmount.from_algo(1), +) + +# Transaction groups +composer = algorand.new_group() +# ... add transactions ... +result = composer.send() +``` + +Key changes: + +- `transfer_algos` → `algorand.send.payment` +- `transfer_asset` → `algorand.send.asset_transfer` +- `execute_atc_with_logic_error` → `composer.send()` +- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) +- Improved amount handling with dedicated `AlgoAmount` class (e.g., `AlgoAmount.from_algo(1)`) + +### Step 4 - Update `ApplicationSpecification` usage + +`ApplicationSpecification` abstraction is largely identical to v2, however it’s been renamed to `Arc32Contract` to better reflect the fact that it’s a contract specification for a specific ARC and addition of `Arc56Contract` supporting the latest recommended conventions. Hence the main actionable change is to update your import to `from algokit_utils import Arc32Contract` and rename `ApplicationSpecification` to `Arc32Contract`. + +You can instantiate an `Arc56Contract` instance from an `Arc32Contract` instance using the `Arc56Contract.from_arc32` method. For instance: + +```python +testing_app_arc32_app_spec = Arc32Contract.from_json(app_spec_json) +arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) +``` + +> Despite auto conversion of ARC-32 to ARC-56, we recommend recompiling your contract to a fully compliant ARC-56 specification given that auto conversion would skip populating information that can’t be parsed from raw ARC-32. + +### Step 5 - Update `ApplicationClient` usage + +The application client has been in v2 has been responsible for instantiation, deployment and calling of the application. In v3, this has been split into `AppClient`, `AppDeployer` and `AppFactory` to better reflect the different responsibilities: + +```python +"""Before (v2 deployment)""" +from algokit_utils import ApplicationClient, OnUpdate, OnSchemaBreak + +# Initialize client with manual configuration +app_client = ApplicationClient( + algod_client=algod, + app_spec=app_spec, + creator=creator, + app_name="MyApp" +) + +# Deployment with versioning and update policies +deploy_result = app_client.deploy( + version="1.0", + allow_update=True, + allow_delete=False, + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail +) + +# Post-deployment calls +response = app_client.call("initialize", args=["config"]) + + +"""After (v3 factory-based deployment)""" +from algokit_utils import AppFactory, OnUpdate, OnSchemaBreak + +# Factory-based deployment with compiled parameters +app_factory = AppFactory( + AppFactoryParams( + algorand=algorand, + app_spec=app_spec, + app_name="MyApp", + compilation_params=AppClientCompilationParams( + deploy_time_params={"VERSION": 1}, + updatable=True, # Replaces allow_update + deletable=False # Replaces allow_delete + ) + ) +) + +app_client, deploy_result = app_factory.deploy( + version="1.0", + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail, +) # Returns a tuple of (app_client, deploy_result) + +# Type-safe post-deployment calls +response = app_client.send.call("setup", args=[{"max_users": 100}]) +``` + +Notable changes: + +- Split between `AppClient`, `AppDeployer` (for raw creation/deployment) and `AppFactory` (for creation/deployment using factory patterns). In majority of cases, you will only need `AppFactory` as it provides convenience methods for instantiation of `AppClient` and mediates calls to `AppDeployer`. +- More structured transaction building with `.params`, `.create_transaction`, and `.send` +- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) +- ARC-56 support for state management +- Improved error handling and debugging support + +### Step 6 - Update `AppClient` State Management + +State management is now more structured and type-safe: + +```python +"""Before""" +global_state = app_client.get_global_state() +local_state = app_client.get_local_state(account_address) +box_value = app_client.get_box_value("box_name") + +"""After""" +# Global state +global_state = app_client.state.global_state.get_all() +value = app_client.state.global_state.get_value("key_name") +map_value = app_client.state.global_state.get_map_value("map_name", "key") + +# Local state +local_state = app_client.state.local_state(account_address).get_all() +value = app_client.state.local_state(account_address).get_value("key_name") +map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") + +# Box storage +box_value = app_client.state.box.get_value("box_name") +boxes = app_client.state.box.get_all() +map_value = app_client.state.box.get_map_value("map_name", "key") +``` + +### Step 7 - Update Asset Management + +Asset management is now more consistent: + +```python +"""Before""" +result = algokit_utils.opt_in(algod, account, [asset_id]) + +"""After""" +result = algorand.send.asset_opt_in( + params=AssetOptInParams( + sender=account.address, + asset_id=asset_id, + ) +) +``` + +## Breaking Changes + +1. **Client Management** + - Removal of standalone client creation functions + - All clients now accessed through `AlgorandClient` +2. **Account Management** + - Account creation functions moved to `AccountManager` accessible via `algorand.account` property + - Unified `TransactionSignerAccountProtocol` with compliant and typed `SigningAccount`, `TransactionSignerAccount`, `LogicSigAccount`, `MultiSigAccount` classes encapsulating low level `algosdk` abstractions. + - Improved typing for account operations, such as obtaining account information from `algod`, returning a typed information object. +3. **Transaction Management** + - Consistent and intuitive transaction creation and sending interface accessible via `algorand.{send|params|create_transaction}` properties + - New transaction composition interface accessible via `algorand.new_group` + - Removing necessity to interact with low level and untyped `algosdk` abstractions for assembling, signing and sending transaction(s). +4. **Application Client** + - Split into `AppClient`, `AppDeployer` and `AppFactory` + - New intuitive structured interface for creating or sending `AppCall`|`AppMethodCall` transactions + - ARC-56 support along with automatic conversion of specs from ARC-32 to ARC-56 +5. **State Management** + - New hierarchical state access available via `app_client.state.{global_state|local_state|box}` properties + - Improved typing for state values + - Support for ARC-56 state schemas +6. **Asset Management** + - Dedicated `AssetManager` class for asset management accessible via `algorand.asset` property + - Improved typing for asset operations, such as obtaining asset information from `algod`, returning a typed information object. + - Consistent interface for asset opt-in, transfer, freeze, etc. + +## Best Practices + +1. Use the new `AlgorandClient` as the main entry point +2. Leverage IDE autocompletion to discover available functionality, consult with [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) when unsure +3. Use the transaction parameter builders for type-safe transaction creation (`algorand.params.{}`) +4. Use the state accessor patterns for cleaner state management {`algorand.state.{}`} +5. Use high level `TransactionComposer` interface over low level `algosdk` abstractions (where possible) +6. Use source maps and debug mode to quickly troubleshoot on-chain errors +7. Use idempotent deployment patterns with versioning + +## Troubleshooting + +### A v2 interface/method/class does not display a deprecation warning correctly or at all + +Submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a description of the problem and the code that is causing it. + +### Useful scenario of converting v2 to v3 not covered in generic migration guide + +If you have a scenario that you think is useful and not covered in the generic migration guide, please submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a scenario. diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index d0c2b42f..25d87437 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -1,31 +1,213 @@ # Account management -Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, idempotent KMD and environment variable injected accounts -that can be used to sign transactions as well as representing a sender address at the same time. - -(account)= -## `Account` - -Encapsulates a private key with convenience properties for `address`, `signer` and `public_key`. - -There are various methods of obtaining an `Account` instance - -* `get_account`: Returns an `Account` instance with the private key loaded by convention based on the given name identifier: - * from an environment variable containing a mnemonic `{NAME}_MNEMONIC` OR - * loading the account from KMD ny name if it exists (LocalNet only) OR - * creating the account in KMD with associated name (LocalNet only) - - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against - TestNet/MainNet will automatically resolve from environment variables - -* `Account.new_account`: Returns a new `Account` using `algosdk.account.generate_account()` -* `Account(private_key)`: Load an existing account from a private key -* `Account(private_key, address)`: Load an existing account from a private key and address, useful for re-keyed accounts -* `get_account_from_mnemonic`: Load an existing account from a mnemonic -* `get_dispenser_account`: Gets a dispenser account that is funded by either: - * Using the LocalNet default account (LocalNet only) OR - * Loading an account from `DISPENSER_MNEMONIC` - -If working with a LocalNet instance, there are some additional functions that rely on a KMD service being exposed: -* `create_kmd_wallet_account`, `get_kmd_wallet_account` or `get_or_create_kmd_wallet_account`: These functions allow retrieving a KMD wallet account by name, -* `get_localnet_default_account`: Gets default localnet account that is funded with algos +Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, rekeyed, multisig, transaction signer, idempotent KMD and environment variable injected accounts that can be used to sign transactions as well as representing a sender address at the same time. This significantly simplifies management of transaction signing. + +## `AccountManager` + +The [`AccountManager`](../apidocs/algokit_utils/accounts/index) is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](./transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! + +To get an instance of `AccountManager`, you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.account` or instantiate it directly: + +```python +from algokit_utils import AccountManager + +account_manager = AccountManager(client_manager) +``` + +## `TransactionSignerAccountProtocol` + +The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](../apidocs/algokit_utils/protocols/account/index), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. + +The following conform to `TransactionSignerAccountProtocol`: + +- [`TransactionSignerAccount`](../apidocs/algokit_utils/models/account/index) - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- [`SigningAccount`](../apidocs/algokit_utils/models/account/index) - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- [`LogicSigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- [`MultisigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` + +## Registering a signer + +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by [`AlgorandClient`](./algorand-client.md) to automatically sign transactions by that sender. Any of the [methods](#accounts) within `AccountManager` that return an account will automatically register the signer with the sender. + +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of [account based objects](#underlying-account-classes) that combine signer and sender (`TransactionSignerAccount` | `SigningAccount` | `LogicSigAccount` | `MultisigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: + +```python +algorand.account + .set_signer_from_account(TransactionSignerAccount(your_address, your_signer)) + .set_signer_from_account(SigningAccount.new_account()) + .set_signer_from_account( + LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)) + ) + .set_signer_from_account( + MultisigAccount( + MultisigMetadata( + version = 1, + threshold = 1, + addresses = ["ADDRESS1...", "ADDRESS2..."] + ), + [account1, account2] + ) + ) + .set_signer("SENDERADDRESS", transaction_signer) +``` + +## Default signer + +If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can [register a default signer](../code/classes/types_account_manager.AccountManager.md#setdefaultsigner): + +```python +algorand.account.set_default_signer(my_default_signer) +``` + +## Get a signer + +[`AlgorandClient`](./algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer](../apidocs/algokit_utils/accounts/account_manager/index#getsigner) for a given sender address: + +```python +signer = algorand.account.get_signer("SENDER_ADDRESS") +``` + +If there is no signer registered for that sender address it will either return the default signer ([if registered](#default-signer)) or throw an exception. + +## Accounts + +In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`](#accountmanager) (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](./algorand-client.md)): + +- [`algorand.account.from_environment(name, fund_with)`](../apidocs/algokit_utils/accounts/account_manager/index#from_environment) - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) + - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code + - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD +- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`](../apidocs/algokit_utils/accounts/account_manager/index#from_mnemonic) - Registers and returns an account with secret key loaded by taking the mnemonic secret +- [`algorand.account.multisig(multisig_params, signing_accounts)`](../apidocs/algokit_utils/accounts/account_manager/index#multisig) - Registers and returns a multisig account with one or more signing keys loaded +- [`algorand.account.rekeyed(sender, signer)`](../apidocs/algokit_utils/accounts/account_manager/index#rekeyed) - Registers and returns an account representing the given rekeyed sender/signer combination +- [`algorand.account.random()`](../apidocs/algokit_utils/accounts/account_manager/index#random) - Returns a new, cryptographically randomly generated account with private key loaded +- [`algorand.account.from_kmd()`](../apidocs/algokit_utils/accounts/account_manager/index#from_kmd) - Returns an account with private key loaded from the given KMD wallet (identified by name) +- [`algorand.account.logicsig(program, args?)`](../apidocs/algokit_utils/accounts/account_manager/index#logicsig) - Returns an account that represents a logic signature + +### Underlying account classes + +While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. + +- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created +- [`SigningAccount`](../code/classes/types_account.SigningAccount.md) - An abstraction around `algosdk.Account` that supports rekeyed accounts +- `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object +- [`MultisigAccount`](../code/classes/types_account.MultisigAccount.md) - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present + +### Dispenser + +- [`algorand.account.dispenserFromEnvironment()`](../code/classes/types_account_manager.AccountManager.md#dispenserfromenvironment) - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- [`algorand.account.localNetDispenser()`](../code/classes/types_account_manager.AccountManager.md#localnetdispenser) - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account + +## Rekey account + +One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). + +> [!WARNING] +> Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. + +You can issue a transaction to rekey an account by using the [`algorand.account.rekeyAccount(account, rekeyTo, options)`](../code/classes/types_account_manager.AccountManager.md#rekeyaccount) function: + +- `account: string | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekeyTo: string | TransactionSignerAccount` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- An `options` object, which has: + - [Common transaction parameters](./algorand-client.md#transaction-parameters) + - [Execution parameters](./algorand-client.md#sending-a-single-transaction) + +You can also pass in `rekeyTo` as a [common transaction parameter](./algorand-client.md#transaction-parameters) to any transaction. + +### Examples + +```python +# Basic example (with string addresses) + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", +}) + +# Basic example (with signer accounts) + +algorand.account.rekey_account({ + account: account1, + rekey_to: new_signer_account, +}) + +# Advanced example + +algorand.account.rekey_account({ + account: "ACCOUNTADDRESS", + rekey_to: "NEWADDRESS", + lease: "lease", + note: "note", + first_valid_round: 1000, + validity_window: 10, + extra_fee: AlgoAmount.from_micro_algos(1000), + static_fee: AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee: AlgoAmount.from_micro_algos(3000), + max_rounds_to_wait_for_confirmation: 5, + suppress_log: True, +}) + + +# Using a rekeyed account + +Note: if a signing account is passed into `algorand.account.rekey_account` then you don't need to call `rekeyed_account` to register the new signer + +rekeyed_account = algorand.account.rekey_account(account, new_account) +# rekeyed_account can be used to sign transactions on behalf of account... +``` + +## KMD account management + +When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for: + +- Accessing the private key of the default accounts that are pre-seeded with Algo so that other accounts can be funded and it's possible to use LocalNet +- Idempotently creating new accounts against a name that will stay intact while the LocalNet instance is running without you needing to store private keys anywhere (i.e. completely automated) + +The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that's needed. This code has been abstracted away into the `KmdAccountManager` class. + +To get an instance of the `KmdAccountManager` class you can access it from [`AlgorandClient`](./algorand-client.md) via `algorand.account.kmd` or instantiate it directly (passing in a [`ClientManager`](./client.md)): + +```python +from algokit_utils import KmdAccountManager + +kmd_account_manager = KmdAccountManager(client_manager) +``` + +The methods that are available are: + +- [`get_wallet_account(wallet_name, predicate?, sender?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_wallet_account)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- [`get_or_create_wallet_account(name, fund_with?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_or_create_wallet_account)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- [`get_localnet_dispenser_account()`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_localnet_dispenser_account)` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) + +```python +# Get a wallet account that seeded the LocalNet network +default_dispenser_account = kmd_account_manager.get_wallet_account( + "unencrypted-default-wallet", + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 +) +# Same as above, but dedicated method call for convenience +localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() +# Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD +# if creating it then fund it with 2 ALGO from the default dispenser account +new_account = kmd_account_manager.get_or_create_wallet_account( + "account1", + AlgoAmount.from_algos(2) +) +# This will return the same account as above since the name matches +existing_account = kmd_account_manager.get_or_create_wallet_account( + "account1" +) +``` + +Some of this functionality is directly exposed from [`AccountManager`](#accountmanager), which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via [`AlgorandClient`](./algorand-client.md): + +```python +# Get and register LocalNet dispenser +localnet_dispenser = algorand.account.localnet_dispenser() +# Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD +dispenser = algorand.account.dispenser_from_environment() +# Get / create and register account from KMD idempotently by name +account1 = algorand.account.from_kmd("account1", AlgoAmount.from_algos(2)) +``` diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md new file mode 100644 index 00000000..9ed7638c --- /dev/null +++ b/docs/source/capabilities/algorand-client.md @@ -0,0 +1,191 @@ +# Algorand client + +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the [default entrypoint](../index.md#usage) into AlgoKit Utils functionality. + +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](./capabilities/algorand-client.md), e.g.: + +```python +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet +# configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=algod) +# Point to pre-created algod, indexer and kmd clients +algorand = AlgorandClient.from_clients(algod=algod, indexer=indexer, kmd=kmd) +# Point to custom configuration for algod +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod, indexer and kmd +algorand = AlgorandClient.from_config( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config +) +``` + +## Accessing SDK clients + +Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. + +```py +algorand = AlgorandClient.default_localnet() + +algod_client = algorand.client.algod +indexer_client = algorand.client.indexer +kmd_client = algorand.client.kmd +``` + +## Accessing manager class instances + +The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. + +- [`AccountManager`](./account.md) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.setDefaultSigner(signer)` - + - `algorand.setSignerFromAccount(account)` - + - `algorand.setSigner(sender, signer)` +- [`AssetManager`](./asset.md) via `algorand.asset` +- [`ClientManager`](./client.md) via `algorand.client` + +## Creating and issuing transactions + +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](./transaction-composer.md)). + +### Creating transactions + +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](../autoapi/algokit_utils/applications/app_client.md#algokit_utils.applications.app_client.AlgorandClientTransactionCreator) class. Intellisense will guide you on the different options. + +The signature for the calls to send a single transaction usually look like: + +```python +algorand.create_transaction.{method}(params=TxnParams(...), send_params=SendParams(...)) -> Transaction: +``` + +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils` and consist of: + - `AppCallParams`, + - `AppCreateParams`, + - `AppDeleteParams`, + - `AppUpdateParams`, + - `AssetConfigParams`, + - `AssetCreateParams`, + - `AssetDestroyParams`, + - `AssetFreezeParams`, + - `AssetOptInParams`, + - `AssetOptOutParams`, + - `AssetTransferParams`, + - `OfflineKeyRegistrationParams`, + - `OnlineKeyRegistrationParams`, + - `PaymentParams`, +- `SendParams` is a typed dictionary exposing setting to apply during send operation: + - `max_rounds_to_wait_for_confirmation: int | None` - The number of rounds to wait for confirmation. By default until the latest lastValid has past. + - `suppress_log: bool | None` - Whether to suppress log messages from transaction send, default: do not suppress. + - `populate_app_call_resources: bool | None` - Whether to use simulate to automatically populate app call resources in the txn objects. Defaults to `Config.populateAppCallResources`. + - `cover_app_call_inner_transaction_fees: bool | None` - Whether to use simulate to automatically calculate required app call inner transaction fees and cover them in the parent app call transaction fee + +The return type for the ABI method call methods are slightly different: + +```python +algorand.createTransaction.app{call_type}_method_call(params=MethodCallParams(...), send_params=SendParams(...)) -> BuiltTransactions +``` + +MethodCallParams is a union type that can be any of the Algorand method call types, exact dataclasses can be imported from `algokit_utils` and consist of: + +- `AppCreateMethodCallParams`, +- `AppCallMethodCallParams`, +- `AppDeleteMethodCallParams`, +- `AppUpdateMethodCallParams`, + +Where `BuiltTransactions` looks like this: + +```python +@dataclass(frozen=True) +class BuiltTransactions: + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] + signers: dict[int, TransactionSigner] +``` + +This signifies the fact that an ABI method call can actually result in multiple transactions (which in turn may have different signers), that you need ABI metadata to be able to extract the return value from the transaction result. + +### Sending a single transaction + +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`](../autoapi/algokit_utils/applications/app_client.md#algokit_utils.applications.app_client.AlgorandClientTransactionSender) class. Intellisense will guide you on the different options. + +Further documentation is present in the related capabilities: + +- [App management](./app.md) +- [Asset management](./asset.md) +- [Algo transfers](./transfer.md) + +The signature for the calls to send a single transaction usually look like: + +`algorand.send.{method}(params=TxnParams, send_params=SendParams) -> SingleSendTransactionResult` + +- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space). +- `TxnParams` is a union type that can be any of the Algorand transaction types, exact dataclasses can be imported from `algokit_utils`. +- [`SendParams`](../autoapi/algokit_utils/models/transaction/SendParams.md) a typed dictionary exposing setting to apply during send operation. +- [`SendSingleTransactionResult`](../autoapi/algokit_utils/models/transaction/SendSingleTransactionResult.md) is all of the information that is relevant when [sending a single transaction to the network](./transaction.md#sending-a-transaction) + +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppressLog: true`. + +### Composing a group of transactions + +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{Type}()` methods on [`TransactionComposer`](./transaction-composer.md) to add a series of transactions. + +```typescript +result = (algorand + .new_group() + .add_payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=1_000_000 # 1 Algo in microAlgos + ) + ) + .add_asset_opt_in( + AssetOptInParams( + sender="SENDERADDRESS", + asset_id=12345 + ) + ) + .send()) +``` + +`new_group()` returns a new [`TransactionComposer`](./transaction-composer.md) instance, which can also return the group of transactions, simulate them and other things. + +### Transaction parameters + +To create a transaction you instantiate a relevant Transaction parameters dataclass from `algokit_utils.transactions import *` or `from algokit_utils import PaymentParams, AssetOptInParams, etc`. + +All transaction parameters share the following common base parameters: + +- [`CommonTransactionParams`](../autoapi/algokit_utils/models/transaction/CommonTransactionParams.md) + - `sender: str` - The address of the account sending the transaction. + - `signer: algosdk.TransactionSigner | TransactionSignerAccount | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured). + - `rekey_to: string | None` - Change the signing key of the sender to the given address. **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). + - `note: bytes | str | None` - Note to attach to the transaction. Max of 1000 bytes. + - `lease: bytes | str | None` - Prevent multiple transactions with the same lease being included within the validity window. A [lease](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). + - Fee management + - `static_fee: AlgoAmount | None` - The static transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. + - `extra_fee: AlgoAmount | None` - The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. + - `max_fee: AlgoAmount | None` - Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. + - Round validity management + - `validity_window: int | None` - How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. + - `first_valid_round: int | None` - Set the first round this transaction is valid. If left undefined, the value from algod will be used. We recommend you only set this when you intentionally want this to be some time in the future. + - `last_valid_round: int | None` - The last round this transaction is valid. It is recommended to use `validity_window` instead. + +Then on top of that the base type gets extended for the specific type of transaction you are issuing. These are all defined as part of [`TransactionComposer`](./transaction-composer.md) and we recommend reading these docs, especially when leveraging either `populate_app_call_resources` or `cover_app_call_inner_transaction_fees`. + +### Transaction configuration + +AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it's set to `1000`. +- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) +- `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md new file mode 100644 index 00000000..f1d11807 --- /dev/null +++ b/docs/source/capabilities/amount.md @@ -0,0 +1,55 @@ +# Algo amount handling + +Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. + +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the {ref}`modularity principle `) you can safely and explicitly convert to microAlgo or Algo. + +To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. + +## `AlgoAmount` + +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or existing the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). + +To import the AlgoAmount class you can access it via: + +```python +from algokit_utils import AlgoAmount +``` + +### Creating an `AlgoAmount` + +There are a few ways to create an `AlgoAmount`: + +- Algo + - Constructor: `AlgoAmount({"algo": 10})` or `AlgoAmount({"algos": 10})` + - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` +- microAlgo + - Constructor: `AlgoAmount({"microAlgo": 10_000})` or `AlgoAmount({"microAlgos": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` + +### Extracting a value from `AlgoAmount` + +The `AlgoAmount` class has properties to return Algo and microAlgo: + +- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer + +`AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. + +You can also call `str(amount)` or use an `AlgoAmount` directly in string interpolation to convert it to a nice user-facing formatted amount expressed in microAlgo. + +### Additional Features + +The `AlgoAmount` class supports arithmetic operations: + +- Addition: `amount1 + amount2` +- Subtraction: `amount1 - amount2` +- Comparison operations: `<`, `<=`, `>`, `>=`, `==`, `!=` + +Example: + +```python +amount1 = AlgoAmount({"algo": 1}) +amount2 = AlgoAmount({"microAlgo": 500_000}) +total = amount1 + amount2 # Results in 1.5 Algo +``` diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index d76f27b7..71357a46 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -1,157 +1,347 @@ -# App client +# App client and App factory -Application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker). +> [!NOTE] +> This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client is a high productivity application client that works with ARC-0032 application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](./app-deploy.md) and [App management](./app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_app_client_call.py). +> [!NOTE] +> If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don't know the app ID (deferred knowledge or the instance doesn't exist yet on the blockchain) or you have multiple app IDs -## Design +## `AppFactory` -The design for the app client is based on a wrapper for parsing an [ARC-0032](https://github.com/algorandfoundation/ARCs/pull/150) application spec and wrapping the [App deployment](./app-deploy.md) functionality and corresponding [design](./app-deploy.md#design). +The `AppFactory` is a class that, for a given app spec, allows you to create and deploy one or more app instances and to create one or more app clients to interact with those (or other) app instances. -## Creating an application client +To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: -There are two key ways of instantiating an ApplicationClient: +```python +# Minimal example +factory = algorand.get_app_factory( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", +) + +# Advanced example +factory = algorand.get_app_factory( + app_spec=parsed_arc32_or_arc56_app_spec, + default_sender="SENDERADDRESS", + app_name="OverriddenAppName", + version="2.0.0", + compilation_params={ + "updatable": True, + "deletable": False, + "deploy_time_params": { "ONE": 1, "TWO": "value" }, + } +) +``` + +## `AppClient` + +The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). + +To get an instance of `AppClient` you can use either `AlgorandClient` or instantiate it directly: -1. By app ID - When needing to call an existing app by app ID or unconditionally create a new app. -The signature `ApplicationClient(algod_client, app_spec, app_id=..., ...)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `app_id`: The app_id of an existing application, or 0 if creating a new app +```python +# Minimal examples +app_client = AppClient.from_creator_and_name( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + creator_address="CREATORADDRESS", + algorand=algorand, +) + +app_client = AppClient( + AppClientParams( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + app_id=12345, + algorand=algorand, + ) +) + +app_client = AppClient.from_network( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + algorand=algorand, +) + +# Advanced example +app_client = AppClient( + AppClientParams( + app_spec=parsed_app_spec, + app_id=12345, + algorand=algorand, + app_name="OverriddenAppName", + default_sender="SENDERADDRESS", + approval_source_map=approval_teal_source_map, + clear_source_map=clear_teal_source_map, + ) +) +``` -2. By creator and app name - When needing to deploy or find an app associated with a specific creator account and app name. -The signature `ApplicationClient(algod_client, app_spec, creator=..., indexer=..., app_lookup)` requires: - * `algod_client`: An `AlgodClient` - * `app_spec`: An `ApplicationSpecification` - * `creator`: The address or `Account` of the creator of the app for which to search for the deployed app under - * `indexer`: - * `app_lookup`: Optional if an indexer is provided, - * `app_name`: An overridden name to identify the contract with, otherwise `contract.name` is used from the app spec +You can access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. -Both approaches also allow specifying the following parameters that will be used as defaults for all application calls: -* `signer`: `TransactionSigner` to sign transactions with. -* `sender`: Address to use for transaction signing, will be derived from the signer if not provided. -* `suggested_params`: Default `SuggestedParams` to use, will use current network suggested params by default - -Both approaches also allow specifying a mapping of template values via the `template_values` parameter, this will be used before compiling the application to replace any -`TMPL_` variables that may be in the TEAL. The `TMPL_UPDATABLE` and `TMPL_DELETABLE` variables used in some AlgoKit templates are handled by the `deploy` method, but should be included if -using `create` or `update` directly. +## Dynamically creating clients for a given app spec -## Calling methods on the app +The `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. -There are various methods available on `ApplicationClient` that can be used to call an app: +This is possible via two methods on the app factory: -* `call`: Used to call methods with an on complete action of `no_op` -* `create`: Used to create an instance of the app, by using an `app_id` of 0, includes the approval and clear programs in the call -* `update`: Used to update an existing app, includes the approval and clear programs in the call, and is called with an on complete action of `update_application` -* `delete`: Used to remove an existing app, is called with an on complete action of `delete_application` -* `opt_in`: Used to opt in to an existing app, is called with an on complete action of `opt_in` -* `close_out`: Used to close out of an existing app, is called with an on complete action of `opt_in` -* `clear_state`: Used to unconditionally close out from an app, calls the clear program of an app +- `factory.get_app_client_by_id(app_id, ...)` - Returns a new `AppClient` for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified. +- `factory.get_app_client_by_creator_and_name(creator_address, app_name, ...)` - Returns a new `AppClient`, resolving the app by creator address and name using AlgoKit app deployment semantics. Automatically populates app_name, default_sender and source maps from the factory if not specified. -### Specifying which method +```python +app_client1 = factory.get_app_client_by_id(app_id=12345) +app_client2 = factory.get_app_client_by_id(app_id=12346) +app_client3 = factory.get_app_client_by_id( + app_id=12345, + default_sender="SENDER2ADDRESS" +) + +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" +) +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" +) +app_client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName", + ignore_cache=True, # Perform fresh indexer lookups + default_sender="SENDER2ADDRESS" +) +``` -All methods for calling an app that support ABI methods (everything except `clear_state`) take a parameter `call_abi_method` which can be used to specify which method to call. -The method selected can be specified explicitly, or allow the client to infer the method where possible, supported values are: +## Creating and deploying an app -* `None`: The default value, when `None` is passed the client will attempt to find any ABI method or bare method that is compatible with the provided arguments -* `False`: Indicates that an ABI method should not be used, and instead a bare method call is made -* `True`: Indicates that an ABI method should be used, and the client will attempt to find an ABI method that is compatible with the provided arguments -* `str`: If a string is provided, it will be interpreted as either an ABI signature specifying a method, or as an ABI method name -* `algosdk.abi.Method`: The specified ABI method will be called -* `ABIReturnSubroutine`: Any type that has a `method_spec` function that returns an `algosd.abi.Method` +Once you have an app factory you can perform the following actions: -### ABI arguments +- `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. -ABI arguments are passed as python keyword arguments e.g. to pass the ABI parameter `name` for the ABI method `hello` the following syntax is used `client.call("hello", name="world")` +> See [API docs](../api/app-factory.md#deploy) for details on parameter signatures. -### Transaction Parameters +### Create -All methods for calling an app take an optional `transaction_parameters` argument, with the following supported parameters: +The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: -* `signer`: The `TransactionSigner` to use on the call. This overrides any signer specified on the client -* `sender`: The address of the sender to use on the call, must be able to be signed for by the `signer`. This overrides any sender specified on the client -* `suggested_params`: `SuggestedParams` to use on the call. This overrides any suggested_params specified on the client -* `note`: Note to include in the transaction -* `lease`: Lease parameter for the transaction -* `boxes`: A sequence of boxes to use in the transaction, this is a list of (app_index, box_name) tuples `[(0, "box_name"), (0, ...)]` -* `accounts`: Account references to include in the transaction -* `foreign_apps`: Foreign apps to include in the transaction -* `foreign_assets`: Foreign assets to include in the transaction -* `on_complete`: The on complete action to use for the transaction, only available when using `call` or `create` -* `extra_pages`: Additional pages to allocate when calling `create`, by default a sufficient amount will be calculated based on the current approval and clear. This can be overridden, if more is required - for a future update +- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs](../api/app-factory.md#deploy) for details. -Parameters can be passed as one of the dataclasses `CommonCallParameters`, `OnCompleteCallParameters`, `CreateCallParameters` (exact type depends on method used) ```python -client.call("hello", transaction_parameters=algokit_utils.OnCompleteCallParameters(signer=...)) +# Use no-argument bare-call +result, app_client = factory.send.bare.create() + +# Specify parameters for bare-call and override other parameters +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( + args=[bytes([1, 2, 3, 4])], + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { + "ONE": 1, + "TWO": "two", + }, + "updatable": True, + "deletable": False, + } +) + +# Specify parameters for ABI method call +result, app_client = factory.send.create( + AppClientMethodCallParams( + method="create_application", + args=[1, "something"] + ) +) ``` -Alternatively, parameters can be passed as a dictionary e.g. +## Updating and deleting an app + +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app created via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. + +## Calling the app + +You can construct a params object, transaction(s) and sign and send a transaction to call the app that a given `AppClient` instance is pointing to. + +This is done via the following properties: + +- `app_client.params.{method}(params)` - Params for an ABI method call +- `app_client.params.bare.{method}(params)` - Params for a bare call +- `app_client.create_transaction.{method}(params)` - Transaction(s) for an ABI method call +- `app_client.create_transaction.bare.{method}(params)` - Transaction for a bare call +- `app_client.send.{method}(params)` - Sign and send an ABI method call +- `app_client.send.bare.{method}(params)` - Sign and send a bare call + +Where `{method}` is one of: + +- `update` - An update call +- `opt_in` - An opt-in call +- `delete` - A delete application call +- `clear_state` - A clear state call (note: calls the clear program and only applies to bare calls) +- `close_out` - A close-out call +- `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) + ```python -client.call("hello", transaction_parameters={"signer":...}) +call1 = app_client.send.update( + AppClientMethodCallParams( + method="update_abi", + args=["string_io"], + ), + compilation_params={"deploy_time_params": deploy_time_params} +) + +call2 = app_client.send.delete( + AppClientMethodCallParams( + method="delete_abi", + args=["string_io"] + ) +) + +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") +) + +call4 = app_client.send.bare.clear_state() + +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) +) + +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") +) ``` -## Composing calls +## Funding the app account -If multiple calls need to be made in a single transaction, the `compose_` method variants can be used. All these methods take an `AtomicTransactionComposer` as their first argument. -Once all the calls have been added to the ATC, it can then be executed. For example: +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you via `fund_app_account(params)`. -```python -from algokit_utils import ApplicationClient -from algosdk.atomic_transaction_composer import AtomicTransactionComposer +The input parameters are: -client = ApplicationClient(...) -atc = AtomicTransactionComposer() -client.compose_call(atc, "hello", name="world") -... # additional compose calls +- A `FundAppAccountParams` object, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). -response = client.execute_atc(atc) +Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you'll want to get the funding call as a transaction, e.g.: + +```python +result = app_client.send.call( + AppClientMethodCallParams( + method="bootstrap", + args=[ + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) + ) + ) + ], + box_references=["Box1"] + ) +) ``` +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. ## Reading state +`AppClient` has a number of mechanisms to read state (global, local and box storage) from the app instance. + +### App spec methods + +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the generic methods give you. + +You can access this functionality via: + +- `app_client.state.global_state.{method}()` - Global state +- `app_client.state.local_state(address).{method}()` - Local state +- `app_client.state.box.{method}()` - Box storage + +Where `{method}` is one of: + +- `get_all()` - Returns all single-key state values in a dict keyed by the key name and the value a decoded ABI value. +- `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be bytes with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value dict. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. + +```python +values = app_client.state.global_state.get_all() +value = app_client.state.local_state("ADDRESS").get_value("value1") +map_value = app_client.state.box.get_map_value("map1", "mapKey") +map_dict = app_client.state.global_state.get_map("myMap") +``` + +### Generic methods + There are various methods defined that let you read state from the smart contract app: -* `get_global_state` - Gets the current global state of the app -* `get_local_state` - Gets the current local state for the given account address +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](../api/app.md#get_global_state) +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](../api/app.md#get_local_state). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](../api/app.md#get_box_names). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](../api/app.md#get_box_value). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](../api/app.md#get_box_value_from_abi_type). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](../api/app.md#get_box_values). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](../api/app.md#get_box_values_from_abi_type). + +```python +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box") +box_name2: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box2") + +box_names = app_client.get_box_names() +box_value = app_client.get_box_value(box_name) +box_values = app_client.get_box_values([box_name, box_name2]) +box_abi_value = app_client.get_box_value_from_abi_type( + box_name, + algosdk.ABIStringType +) +box_abi_values = app_client.get_box_values_from_abi_type( + [box_name, box_name2], + algosdk.ABIStringType +) +``` ## Handling logic errors and diagnosing errors -Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, -exhaustion of opcode budget, or any number of other reasons. +Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons. When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](./app-deploy.md#compilation-and-template-substitution) you can expose debugging -information that makes it much easier to understand what's happening. +The information in that error message can be parsed and when combined with the [source map from compilation](../api/app-deploy.md#compilation-and-template-substitution) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. + +The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -When an error is thrown then the resulting error that is re-thrown will be a `LogicError`, which has the following fields: +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](todo_paste_url), which has the following fields: -* `logic_error`: Original exception -* `program`: Program source (if available) -* `source_map`: Source map used (if available) -* `transaction_id`: Transaction ID of failing transaction -* `message`: The error message -* `line_no`: The line number in the TEAL program that -* `traces`: A list of Trace objects providing additional insights on simulation when debug mode is active. +- `logic_error: Exception` - The original logic error exception +- `logic_error_str: str` - The string representation of the logic error +- `program: str` - The TEAL program source code +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - Combined error message with debugging information +- `pc: int` - The program counter value where error occurred +- `traces: list[SimulationTrace] | None` - Simulation traces if debug enabled +- `line_no: int | None` - The line number in the TEAL source code +- `lines: list[str]` - The TEAL program split into individual lines -The function `trace()` will provide a formatted output of the surrounding TEAL where the error occurred. +Note: This information will only show if the app client / app factory has a source map. This will occur if: -```{note} -The extended information will only show if the Application Client has a source map. This will occur if: +- You have called `create`, `update` or `deploy` +- You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) +- You had source maps present in an app factory and then used it to [create an app client](#dynamically-creating-clients-for-a-given-app-spec) (they are automatically passed through) -1.) The ApplicationClient instance has already called, `create, `update` or `deploy` OR -2.) `template_values` are provided when creating the ApplicationClient, so a SourceMap can be obtained automatically OR -3.) `approval_source_map` on `ApplicationClient` has been set from a previously compiled approval program OR -4.) A source map has been exported/imported using `export_source_map`/`import_source_map`""" +If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: + +```python +config.configure(debug=True) ``` -### Debug Mode and traces Field -When debug mode is active, the LogicError will contain a field named traces. This field will include raw simulate execution traces, providing a detailed account of the transaction simulation. These traces are crucial for diagnosing complex issues and are automatically included in all application client calls when debug mode is active. +If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. + +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](./debugging.md). + +## Default arguments -```{note} -Remember to enable debug mode (`config.debug = True`) to include raw simulate execution traces in the `LogicError`. -``` \ No newline at end of file +If an ABI method call specifies default argument values for any of its arguments you can pass in `None` for the value of that argument for the default value to be automatically populated. diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index d041576e..82fc97f4 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -1,19 +1,16 @@ # App deployment -Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution +AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet). -App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, -particularly [App management](./app-client.md). It allows you to idempotently (with safe retryability) deploy an app, including deploy-time immutability and permanence control and -TEAL template substitution. +It's optional to use this functionality, since you can construct your own deployment logic using create / update / delete calls and your own mechanism to maintaining app metadata (like app IDs etc.), but this capability is an opinionated out-of-the-box solution that takes care of the heavy lifting for you. -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). +App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App management](./app.md). -(design)= +To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -## Design +## Smart contract development lifecycle -The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). -While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. +The design behind the deployment capability is unique. The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. Namely, it described the concept of a smart contract development lifecycle: @@ -36,86 +33,182 @@ The App deployment capability provided by AlgoKit Utils helps implement **#2 Dep Furthermore, the implementation contains the following implementation characteristics per the original architecture design: - Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) -- Contracts can be built by any smart contract framework that supports [ARC-0032](https://arc.algorand.foundation/ARCs/arc-0032) and - [ARC-0004](https://arc.algorand.foundation/ARCs/arc-0004) ([Beaker](https://beaker.algo.xyz/) or otherwise), which also means the deployment language can be - different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance -- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier - development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) -- Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID - instead +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) and [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) +- Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead + +This design allows you to have the same deployment code across environments without having to specify an ID for each environment. This makes it really easy to apply [continuous delivery](https://continuousdelivery.com/) practices to your smart contract deployment and make the deployment process completely automated. + +## `AppDeployer` -## Finding apps by creator +The [`AppDeployer`](../apidocs/algokit_utils/algokit_utils.md#appdeployer) is a class that is used to manage app deployments and deployment metadata. -There is a method `algokit.get_creator_apps(creatorAccount, indexer)`, which performs a series of indexer lookups that return all apps created by the given creator. These are indexed by the name it -was deployed under if the creation transaction contained the following payload in the transaction note field: +To get an instance of `AppDeployer` you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](./app.md#appmanager), [`AlgorandClientTransactionSender`](./algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): +```python +from algokit_utils.app_deployer import AppDeployer + +app_deployer = AppDeployer(app_manager, transaction_sender, indexer) ``` -ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} + +## Deployment metadata + +When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. + +The deployment metadata is defined in [`AppDeployMetadata`](../apidocs/algokit_utils/algokit_utils.md#appdeploymetadata), which is an object with: + +- `name: str` - The unique name identifier of the app within the creator account +- `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) +- `deletable: bool | None` - Whether or not the app is deletable (`true`) / permanent (`false`) / unspecified (`None`) +- `updatable: bool | None` - Whether or not the app is updatable (`true`) / immutable (`false`) / unspecified (`None`) + +An example of the ARC-2 transaction note that is attached as an app creation / update transaction note to specify this metadata is: + +``` +ALGOKIT_DEPLOYER:j{name:"MyApp",version:"1.0",updatable:true,deletable:false} ``` -Any creation transactions or update transactions are then retrieved and processed in chronological order to result in an `AppLookup` object +## Lookup deployed apps by name -Given there are a number of indexer calls to retrieve this data it's a non-trivial object to create, and it's recommended that for the duration you are performing a single deployment -you hold a value of it rather than recalculating it. Most AlgoKit Utils functions that need it will also take an optional value of it that will be used in preference to retrieving a -fresh version. +In order to resolve what apps have been previously deployed and their metadata, AlgoKit provides a method that does a series of indexer lookups and returns a map of name to app metadata via `get_creator_apps_by_name(creator_address)`. -## Deploying an application +```python +app_lookup = algorand.app_deployer.get_creator_apps_by_name("CREATORADDRESS") +app1_metadata = app_lookup.apps["app1"] +``` -The method that performs the deployment logic is the instance method `ApplicationClient.deploy`. It performs an idempotent (safely retryable) deployment. It will detect if the app already -exists and if it doesn't it will create it. If the app does already exist then it will: +This method caches the result of the lookup, since it's a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -- Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the - deployment configuration. +The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](../apidocs/algokit_utils/algokit_utils.md#applicationlookup): -It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. -This metadata works in concert with `get_creator_apps` to allow the app to be reliably retrieved against that creator in it's currently deployed state. +```python +@dataclasses.dataclass +class ApplicationLookup: + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) +``` -`deploy` automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability. +The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](../apidocs/algokit_utils/algokit_utils.md#applicationmetadata). + +> Refer to the [API docs](../apidocs/algokit_utils/algokit_utils.md#applicationlookup) for latest information on exact types. + +## Performing a deployment + +In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. + +For example: + +```python +deployment_result = algorand.app_deployer.deploy( + AppDeployParams( + metadata=AppDeploymentMetaData( + name="MyApp", + version="1.0.0", + deletable=False, + updatable=False, + ), + create_params=AppCreateParams( + sender="CREATORADDRESS", + approval_program=approval_teal_template_or_byte_code, + clear_state_program=clear_state_teal_template_or_byte_code, + schema=StateSchema( + global_ints=1, + global_byte_slices=2, + local_ints=3, + local_byte_slices=4, + ), + # Other parameters if a create call is made... + ), + update_params=AppUpdateParams( + sender="SENDERADDRESS", + # Other parameters if an update call is made... + ), + delete_params=AppDeleteParams( + sender="SENDERADDRESS", + # Other parameters if a delete call is made... + ), + deploy_time_params={ + "VALUE": 1, # TEAL template variables to replace + }, + on_schema_break=OnSchemaBreak.Append, + on_update=OnUpdate.Update, + send_params=SendParams( + populate_app_call_resources=True, + # Other execution control parameters + ), + ) +) +``` + +This method performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn't it will create it. If the app does already exist then it will: + +- Detect if the app has been updated (i.e. the program logic has changed) and either fail, perform an update, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than were originally requested) and either fail, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. + +It will automatically [add metadata to the transaction note of the create or update transactions](#deployment-metadata) that indicates the name, version, updatability and deletability of the contract. This metadata works in concert with [`appDeployer.get_creator_apps_by_name`](#lookup-deployed-apps-by-name) to allow the app to be reliably retrieved against that creator in it's currently deployed state. It will automatically update it's lookup cache so subsequent calls to `get_creator_apps_by_name` or `deploy` will use the latest metadata without needing to call indexer again. + +`deploy` also automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability if the requisite template parameters are specified in the provided TEAL template. ### Input parameters -The following inputs are used when deploying an App +The first parameter `deployment` is an [`AppDeployParams`](../apidocs/algokit_utils/algokit_utils.md#appdeployparams), which is an object with: -- `version`: The version string for the app defined in app_spec, if not specified the version will automatically increment for existing apps that are updated, and set to 1.0 for new apps -- `signer`, `sender`: Optional signer and sender for deployment operations, sender must be the same as the creator specified -- `allow_update`, `allow_delete`: Control the updatability and deletability of the app, used to populate `TMPL_UPDATABLE` and `TMPL_DELETABLE` template values -- `on_update`: Determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment) -- `on_schema_break`: Determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created) -- `create_args`: Args to use if a create operation is performed -- `update_args`: Args to use if an update operation is performed -- `delete_args`: Args to use if a delete operation is performed -- `template_values`: Values to use for automatic substitution of [deploy-time parameter values](#design) is mapping of `key: value` that will result in `TMPL_{key}` being replaced with `value` +- `metadata: AppDeployMetadata` - determines the [deployment metadata](#deployment-metadata) of the deployment +- `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](./app.md#creation) (raw parameters or ABI method call) +- `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](./app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic +- `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](./app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter +- `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution](#compilation-and-template-substitution) + - [`TealTemplateParams`](../apidocs/algokit_utils/algokit_utils.md#tealtemplateparams) is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onschemabreak) if schema requirements increase (values: 'replace', 'fail', 'append') +- `on_update: OnUpdate | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onupdate) if contract logic changes (values: 'update', 'replace', 'fail', 'append') +- `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries +- `ignore_cache: bool | None` - if True, bypasses cached deployment metadata +- Additional fields from [`SendParams`](../apidocs/algokit_utils/algokit_utils.md#sendparams) - transaction execution parameters ### Idempotency -`deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will -do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. - -(compilation-and-template-substitution)= +`deploy` is idempotent which means you can safely call it again multiple times and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. ### Compilation and template substitution -When compiling TEAL template code, the capabilities described in the [design above](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. +When compiling TEAL template code, the capabilities described in the [above design](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. -In order for a smart contract to be able to use this functionality, it must have a TEAL Template that contains the following: +In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: -- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which wil be automatically hexadecimal encoded +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which will be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn't (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn't (permanent) -If you are building a smart contract using the [beaker_production AlgoKit template](https://github.com/algorandfoundation/algokit-beaker-default-template) if provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. + +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result](../apidocs/algokit_utils/algokit_utils.md#compiledteal) of substituting then compiling the TEAL template(s) in the following properties of the return value: + +- `compiled_approval: CompiledTeal | None` +- `compiled_clear: CompiledTeal | None` + +Template substitution is done by executing `algorand.app.compile_teal_template(teal_template_code, template_params, deployment_metadata)`, which in turn calls the following in order and returns the compilation result per above (all of which can also be invoked directly): + +- `AppManager.strip_teal_comments(teal_code)` - Strips out any TEAL comments to reduce the payload that is sent to algod and reduce the likelihood of hitting the max payload limit +- `AppManager.replace_template_variables(teal_template_code, template_values)` - Replaces the template variables by looking for `TMPL_{key}` +- `AppManager.replace_teal_template_deploy_time_control_params(teal_template_code, params)` - If `params` is provided, it allows for deploy-time immutability and permanence control by replacing `TMPL_UPDATABLE` with `params.get("updatable")` if not `None` and replacing `TMPL_DELETABLE` with `params.get("deletable")` if not `None` +- `algorand.app.compile_teal(teal_code)` - Sends the final TEAL to algod for compilation and returns the result including the source map and caches the compilation result within the `AppManager` instance ### Return value -`deploy` returns a `DeployResponse` object, that describes the action taken. - -- `action_taken`: Describes what happened during deployment - - `Create` - The smart contract app is created. - - `Update` - The smart contract app is updated - - `Replace` - The smart contract app was deleted and created again (in an atomic transaction) - - `Nothing` - Nothing was done since an existing up-to-date app was found -- `create_response`: If action taken was `Create` or `Replace`, the result of the create transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `update_response`: If action taken was `Update`, the result of the update transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `delete_response`: If action taken was `Replace`, the result of the delete transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `app`: An `AppMetaData` object, describing the final app state +When `deploy` executes it will return a [comprehensive result](../apidocs/algokit_utils/algokit_utils.md#appdeployresult) object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. + +The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): + +- `OperationPerformed.CREATE` - The smart contract app was created +- `OperationPerformed.UPDATE` - The smart contract app was updated +- `OperationPerformed.REPLACE` - The smart contract app was deleted and created again (in an atomic transaction) +- `OperationPerformed.NOTHING` - Nothing was done since it was detected the existing smart contract app deployment was up to date + +As well as the `operation_performed` parameter and the [optional compilation result](#compilation-and-template-substitution), the return value will have the [`ApplicationMetaData`](../code/classes/algokit_utils.applications.app_deployer.ApplicationMetaData.md) [fields](#deployment-metadata) present. + +Based on the value of `operation_performed`, there will be other data available in the return value: + +- If `CREATE`, `UPDATE` or `REPLACE` then it will have the relevant [`SendAppTransactionResult`](./app.md#calling-an-app) values: + - `create_result` for create operations + - `update_result` for update operations +- If `REPLACE` then it will also have `delete_result` to capture the result of deleting the existing app diff --git a/docs/source/capabilities/app.md b/docs/source/capabilities/app.md new file mode 100644 index 00000000..55d30b17 --- /dev/null +++ b/docs/source/capabilities/app.md @@ -0,0 +1,163 @@ +# App management + +App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes). + +## `AppManager` + +The `AppManager` is a class that is used to manage app information. To get an instance of `AppManager` you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.app` or instantiate it directly (passing in an algod client instance): + +```python +from algokit_utils import AppManager + +app_manager = AppManager(algod_client) +``` + +## Calling apps + +### App Clients + +The recommended way of interacting with apps is via [App clients](./app-client.md) and [App factory](./app-client.md#appfactory). The methods shown on this page are the underlying mechanisms that app clients use and are for advanced use cases when you want more control. + +### Compilation + +The `AppManager` class allows you to compile TEAL code with caching semantics that allows you to avoid duplicate compilation and keep track of source maps from compiled code. + +```python +# Basic compilation +teal_code = "return 1" +compilation_result = app_manager.compile_teal(teal_code) + +# Get cached compilation result +cached_result = app_manager.get_compilation_result(teal_code) + +# Compile with template substitution +template_code = "int TMPL_VALUE" +template_params = {"VALUE": 1} +compilation_result = app_manager.compile_teal_template( + template_code, + template_params=template_params +) + +# Compile with deployment control (updatable/deletable) +control_template = f"""#pragma version 8 +int {UPDATABLE_TEMPLATE_NAME} +int {DELETABLE_TEMPLATE_NAME}""" +deployment_metadata = {"updatable": True, "deletable": True} +compilation_result = app_manager.compile_teal_template( + control_template, + deployment_metadata=deployment_metadata +) +``` + +The compilation result contains: + +- `teal` - Original TEAL code +- `compiled` - Base64 encoded compiled bytecode +- `compiled_hash` - Hash of compiled bytecode +- `compiled_base64_to_bytes` - Raw bytes of compiled bytecode +- `source_map` - Source map for debugging + +## Accessing state + +### Global state + +To access global state you can use: + +```python +# Get global state for app +global_state = app_manager.get_global_state(app_id) + +# Parse raw state from algod +decoded_state = AppManager.decode_app_state(raw_state) + +# Access state values +key_raw = decoded_state["value1"].key_raw # Raw bytes +key_base64 = decoded_state["value1"].key_base64 # Base64 encoded +value = decoded_state["value1"].value # Parsed value (str or int) +value_raw = decoded_state["value1"].value_raw # Raw bytes if bytes value +value_base64 = decoded_state["value1"].value_base64 # Base64 if bytes value +``` + +### Local state + +To access local state you can use: + +```python +local_state = app_manager.get_local_state(app_id, "ACCOUNT_ADDRESS") +``` + +### Boxes + +To access box storage: + +```python +# Get box names +box_names = app_manager.get_box_names(app_id) + +# Get box values +box_value = app_manager.get_box_value(app_id, box_name) +box_values = app_manager.get_box_values(app_id, [box_name1, box_name2]) + +# Get decoded ABI values +abi_value = app_manager.get_box_value_from_abi_type( + app_id, box_name, algosdk.abi.StringType() +) +abi_values = app_manager.get_box_values_from_abi_type( + app_id, [box_name1, box_name2], algosdk.abi.StringType() +) + +# Get box reference for transaction +box_ref = AppManager.get_box_reference(box_id) +``` + +## Getting app information + +To get app information: + +```python +# Get app info by ID +app_info = app_manager.get_by_id(app_id) + +# Get ABI return value from transaction +abi_return = AppManager.get_abi_return(confirmation, abi_method) +``` + +## Box references + +Box references can be specified in several ways: + +```python +# String name (encoded to bytes) +box_ref = "my_box" + +# Raw bytes +box_ref = b"my_box" + +# Account signer (uses address as name) +box_ref = account_signer + +# Box reference with app ID +box_ref = BoxReference(app_id=123, name=b"my_box") +``` + +## Common app parameters + +When interacting with apps (creating, updating, deleting, calling), there are common parameters that can be passed: + +- `app_id` - ID of the application +- `sender` - Address of transaction sender +- `signer` - Transaction signer (optional) +- `args` - Arguments to pass to the smart contract +- `account_references` - Account addresses to reference +- `app_references` - App IDs to reference +- `asset_references` - Asset IDs to reference +- `box_references` - Box references to load +- `on_complete` - On complete action +- Other common transaction parameters like `note`, `lease`, etc. + +For ABI method calls, additional parameters: + +- `method` - The ABI method to call +- `args` - ABI typed arguments to pass + +See [App client](./app-client.md) for more details on constructing app calls. diff --git a/docs/source/capabilities/asset.md b/docs/source/capabilities/asset.md new file mode 100644 index 00000000..731af016 --- /dev/null +++ b/docs/source/capabilities/asset.md @@ -0,0 +1,134 @@ +# Assets + +The Algorand Standard Asset (ASA) management functions include creating, opting in and transferring assets, which are fundamental to asset interaction in a blockchain environment. + +## `AssetManager` + +The `AssetManager` class provides functionality for managing Algorand Standard Assets (ASAs). It can be accessed through the `AlgorandClient` via `algorand.asset` or instantiated directly: + +```python +from algokit_utils import AssetManager, TransactionComposer +from algosdk.v2client import algod + +asset_manager = AssetManager( + algod_client=algod_client, + new_group=lambda: TransactionComposer() +) +``` + +## Asset Information + +The `AssetManager` provides two key data classes for asset information: + +### `AssetInformation` + +Contains details about an Algorand Standard Asset (ASA): + +```python +@dataclass +class AssetInformation: + asset_id: int # The ID of the asset + creator: str # Address of the creator account + total: int # Total units created + decimals: int # Number of decimal places + default_frozen: bool | None = None # Whether asset is frozen by default + manager: str | None = None # Optional manager address + reserve: str | None = None # Optional reserve address + freeze: str | None = None # Optional freeze address + clawback: str | None = None # Optional clawback address + unit_name: str | None = None # Optional unit name (e.g. ticker) + asset_name: str | None = None # Optional asset name + url: str | None = None # Optional URL for more info + metadata_hash: bytes | None = None # Optional 32-byte metadata hash +``` + +### `AccountAssetInformation` + +Contains information about an account's holding of a particular asset: + +```python +@dataclass +class AccountAssetInformation: + asset_id: int # The ID of the asset + balance: int # Amount held by the account + frozen: bool # Whether frozen for this account + round: int # Round this info was retrieved at +``` + +## Bulk Operations + +The `AssetManager` provides methods for bulk opt-in/opt-out operations: + +### Bulk Opt-In + +```python +# Basic example +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_in( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + signer=transaction_signer, + note=b"opt-in note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +### Bulk Opt-Out + +```python +# Basic example +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890] +) + +# Advanced example with optional parameters +result = asset_manager.bulk_opt_out( + account="ACCOUNT_ADDRESS", + asset_ids=[12345, 67890], + ensure_zero_balance=True, + signer=transaction_signer, + note=b"opt-out note", + lease=b"lease", + static_fee=AlgoAmount(1000), + extra_fee=AlgoAmount(500), + max_fee=AlgoAmount(2000), + validity_window=10, + send_params=SendParams(...) +) +``` + +The bulk operations return a list of `BulkAssetOptInOutResult` objects containing: + +- `asset_id`: The ID of the asset opted into/out of +- `transaction_id`: The transaction ID of the opt-in/out + +## Get Asset Information + +### Getting Asset Parameters + +You can get the current parameters of an asset from algod using `get_by_id()`: + +```python +asset_info = asset_manager.get_by_id(12345) +``` + +### Getting Account Holdings + +You can get an account's current holdings of an asset using `get_account_information()`: + +```python +address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" +asset_id = 12345 +account_info = asset_manager.get_account_information(address, asset_id) +``` diff --git a/docs/source/capabilities/client.md b/docs/source/capabilities/client.md index 851c66ff..00b92731 100644 --- a/docs/source/capabilities/client.md +++ b/docs/source/capabilities/client.md @@ -1,29 +1,111 @@ # Client management -Client management is one of the core capabilities provided by AlgoKit Utils. -It allows you to create [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) -and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. +Client management is one of the core capabilities provided by AlgoKit Utils. It allows you to create (auto-retry) [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration. -Any AlgoKit Utils function that needs one of these clients will take the underlying `algosdk` classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, -`algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the -Client management capability if you prefer. +Any AlgoKit Utils function that needs one of these clients will take the underlying algosdk classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, `algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the Client management capability if you prefer. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_network_clients.py). +## `ClientManager` + +The `ClientManager` is a class that is used to manage client instances. + +To get an instance of `ClientManager` you can instantiate it directly: + +```python +from algokit_utils import ClientManager, AlgoSdkClients, AlgoClientConfigs +from algosdk.v2client.algod import AlgodClient + +# Using AlgoSdkClients +algod_client = AlgodClient(...) +algorand_client = ... # Get AlgorandClient instance from somewhere +clients = AlgoSdkClients(algod=algod_client, indexer=indexer_client, kmd=kmd_client) +client_manager = ClientManager(clients, algorand_client) + +# Using AlgoClientConfigs +algod_config = AlgoClientNetworkConfig(server="https://...", token="") +configs = AlgoClientConfigs(algod_config=algod_config) +client_manager = ClientManager(configs, algorand_client) +``` + ## Network configuration -The network configuration is specified using the `AlgoClientConfig` class. This same interface is used to specify the config for algod, indexer and kmd clients. +The network configuration is specified using the `AlgoClientConfig` type. This same type is used to specify the config for [algod](https://developer.algorand.org/docs/sdks/python/), [indexer](https://developer.algorand.org/docs/sdks/python/) and [kmd](https://developer.algorand.org/docs/sdks/python/) SDK clients. There are a number of ways to produce one of these configuration objects: -- Manually creating the object, e.g. `AlgoClientConfig(server="https://myalgodnode.com", token="SECRET_TOKEN")` -- `algokit_utils.get_algonode_config(network, config, token)`: Loads an Algod or indexer config against [Nodely](https://nodely.io/docs/free/start) to either MainNet or TestNet -- `algokit_utils.get_default_localnet_config(configOrPort)`: Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration +- Manually specifying a dataclass, e.g. + + ```python + from algokit_utils import AlgoClientNetworkConfig + + config = AlgoClientNetworkConfig( + server="https://myalgodnode.com", + token="SECRET_TOKEN" # optional + ) + ``` + +- `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables +- `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change +- `ClientManager.get_algonode_config(network)` - Loads an Algod or indexer config against [AlgoNode free tier](https://nodely.io/docs/free/start) to either MainNet or TestNet +- `ClientManager.get_default_localnet_config()` - Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration ## Clients -Once you have the configuration for a client, to get the client you can use the following functions: +### Creating an SDK client instance + +Once you have the configuration for a client, to get a new client you can use the following functions: + +- `ClientManager.get_algod_client(config)` - Returns an Algod client for the given configuration; the client automatically retries on transient HTTP errors +- `ClientManager.get_indexer_client(config)` - Returns an Indexer client for given configuration +- `ClientManager.get_kmd_client(config)` - Returns a Kmd client for the given configuration + +You can also shortcut needing to write the likes of `ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())` with environment shortcut methods: + +- `ClientManager.get_algod_client_from_environment()` - Returns an Algod client by loading the config from environment variables +- `ClientManager.get_indexer_client_from_environment()` - Returns an indexer client by loading the config from environment variables +- `ClientManager.get_kmd_client_from_environment()` - Returns a kmd client by loading the config from environment variables + +### Accessing SDK clients via ClientManager instance + +Once you have a `ClientManager` instance, you can access the SDK clients: + +```python +client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) + +algod_client = client_manager.algod +indexer_client = client_manager.indexer +kmd_client = client_manager.kmd +``` + +If the method to create the `ClientManager` doesn't configure indexer or kmd (both of which are optional), then accessing those clients will trigger an error. + +### Creating a TestNet dispenser API client instance + +You can also create a [TestNet dispenser API client instance](./dispenser-client.md) from `ClientManager` too. + +## Automatic retry + +When receiving an Algod or Indexer client from AlgoKit Utils, it will be a special wrapper client that handles retrying transient failures. + +## Network information + +You can get information about the current network you are connected to: + +```python +# Get network information +network = client_manager.network() +print(f"Is mainnet: {network.is_mainnet}") +print(f"Is testnet: {network.is_testnet}") +print(f"Is localnet: {network.is_localnet}") +print(f"Genesis ID: {network.genesis_id}") +print(f"Genesis hash: {network.genesis_hash}") + +# Convenience methods +is_mainnet = client_manager.is_mainnet() +is_testnet = client_manager.is_testnet() +is_localnet = client_manager.is_localnet() +``` -- `algokit_utils.get_algod_client(config)`: Returns an Algod client for the given configuration or if none is provided retrieves a configuration from the environment using `ALGOD_SERVER`, `ALGOD_TOKEN` and optionally `ALGOD_PORT`. -- `algokit_utils.get_indexer_client(config)`: Returns an Indexer client for given configuration or if none is provided retrieves a configuration from the environment using `INDEXER_SERVER`, `INDEXER_TOKEN` and optionally `INDEXER_PORT` -- `algokit_utils.get_kmd_client_from_algod_client(config)`: - Returns a Kmd client based on the provided algod client configuration, with the assumption the KMD services is running on the same host but a different port (either `KMD_PORT` environment variable or `4002` by default) +The first time `network()` is called it will make a HTTP call to algod to get the network parameters, but from then on it will be cached within that `ClientManager` instance for subsequent calls. diff --git a/docs/source/capabilities/debugger.md b/docs/source/capabilities/debugger.md deleted file mode 100644 index ac23a73e..00000000 --- a/docs/source/capabilities/debugger.md +++ /dev/null @@ -1,45 +0,0 @@ -# Debugger - -The AlgoKit Python Utilities package provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AlgoKit AVM Debugger extension](https://marketplace.visualstudio.com/items?itemName=algorandfoundation.algokit-avm-vscode-debugger). - -## Configuration - -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: - -- `debug`: Indicates whether debug mode is enabled. -- `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. -- `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. -- `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. -- `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. - -The `configure` method can be used to set these attributes. - -To enable debug mode in your project you can configure it as follows: - -```py -from algokit_utils.config import config - -config.configure(debug=True) -``` - -## Debugging Utilities - -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: - -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. - -### Trace filename format - -The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: - -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; -``` - -Where: - -- `timestamp`: The time when the trace file was created, in ISO 8601 format, with colons and periods removed. -- `last_round`: The last round when the simulation was performed. -- `transaction_types`: A string representing the types and counts of transactions in the atomic group. Each transaction type is represented as `${count}${type}`, and different transaction types are separated by underscores. - -For example, a trace file might be named `20220301T123456Z_lr1000_2pay_1axfer.trace.avm.json`, indicating that the trace file was created at `2022-03-01T12:34:56Z`, the last round was `1000`, and the atomic group contained 2 payment transactions and 1 asset transfer transaction. diff --git a/docs/html/_sources/capabilities/debugger.md.txt b/docs/source/capabilities/debugging.md similarity index 63% rename from docs/html/_sources/capabilities/debugger.md.txt rename to docs/source/capabilities/debugging.md index ac23a73e..66ec29db 100644 --- a/docs/html/_sources/capabilities/debugger.md.txt +++ b/docs/source/capabilities/debugging.md @@ -4,36 +4,66 @@ The AlgoKit Python Utilities package provides a set of debugging tools that can ## Configuration -The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. The class has the following attributes: +The `config.py` file contains the `UpdatableConfig` class which manages and updates configuration settings for the AlgoKit project. - `debug`: Indicates whether debug mode is enabled. - `project_root`: The path to the project root directory. Can be ignored if you are using `algokit_utils` inside an `algokit` compliant project (containing `.algokit.toml` file). For non algokit compliant projects, simply provide the path to the folder where you want to store sourcemaps and traces to be used with [`AlgoKit AVM Debugger`](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). Alternatively you can also set the value via the `ALGOKIT_PROJECT_ROOT` environment variable. - `trace_all`: Indicates whether to trace all operations. Defaults to false, this means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will store responses from `simulate` endpoint. These files are called traces, and can be used with `AlgoKit AVM Debugger` to debug TEAL source codes, transactions in the atomic group and etc. - `trace_buffer_size_mb`: The size of the trace buffer in megabytes. By default uses 256 megabytes. When output folder containing debug trace files exceedes the size, oldest files are removed to optimize for storage consumption. - `max_search_depth`: The maximum depth to search for a an `algokit` config file. By default it will traverse at most 10 folders searching for `.algokit.toml` file which will be used to assume algokit compliant project root path. +- `populate_app_call_resources`: Indicates whether to populate app call resources. Defaults to false, which means that when debug mode is enabled, any (or all) application client calls performed via `algokit_utils` will not populate app call resources. The `configure` method can be used to set these attributes. To enable debug mode in your project you can configure it as follows: -```py +```python from algokit_utils.config import config -config.configure(debug=True) +config.configure( + debug=True, + project_root=Path("./my-project"), + trace_all=True, + trace_buffer_size_mb=512, + max_search_depth=15, + populate_app_call_resources=True, +) ``` ## Debugging Utilities -Debugging utilities can be used to simplify gathering artifacts to be used with [AlgoKit AVM Debugger](https://github.com/algorandfoundation/algokit-avm-vscode-debugger) in non algokit compliant projects. The following methods are provided: +When debug mode is enabled, AlgoKit Utils will automatically: -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. +- Generate transaction traces compatible with the AVM Debugger +- Manage trace file storage with automatic cleanup +- Provide source map generation for TEAL contracts + +The following methods are provided for manual debugging operations: + +- `persist_sourcemaps`: Persists sourcemaps for given TEAL contracts as AVM Debugger-compliant artifacts. Parameters: + + - `sources`: List of TEAL sources to generate sourcemaps for + - `project_root`: Project root directory for storage + - `client`: AlgodClient instance + - `with_sources`: Whether to include TEAL source files (default: True) + +- `simulate_and_persist_response`: Simulates transactions and persists debug traces. Parameters: + - `atc`: AtomicTransactionComposer containing transactions + - `project_root`: Project root directory for storage + - `algod_client`: AlgodClient instance + - `buffer_size_mb`: Maximum trace storage in MB (default: 256) + - `allow_empty_signatures`: Allow unsigned transactions (default: True) + - `allow_unnamed_resources`: Allow unnamed resources (default: True) + - `extra_opcode_budget`: Additional opcode budget + - `exec_trace_config`: Custom trace configuration + - `simulation_round`: Specific round to simulate ### Trace filename format The trace files are named in a specific format to provide useful information about the transactions they contain. The format is as follows: -```ts -`${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json`; +``` +${timestamp}_lr${last_round}_${transaction_types}.trace.avm.json ``` Where: diff --git a/docs/source/capabilities/dispenser-client.md b/docs/source/capabilities/dispenser-client.md index 315b52f4..d9370f0a 100644 --- a/docs/source/capabilities/dispenser-client.md +++ b/docs/source/capabilities/dispenser-client.md @@ -7,54 +7,85 @@ The TestNet Dispenser Client is a utility for interacting with the AlgoKit TestN To create a Dispenser Client, you need to provide an authorization token. This can be done in two ways: 1. Pass the token directly to the client constructor as `auth_token`. -2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) on how to obtain the token). +2. Set the token as an environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN` (see [docs](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) on how to obtain the token). If both methods are used, the constructor argument takes precedence. -```py +```python +import algokit_utils + +# With auth token +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", +) + +# With auth token and timeout +dispenser = algorand.client.get_testnet_dispenser( + auth_token="your_auth_token", + request_timeout=2, # seconds +) + +# From environment variables +# i.e. os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' +dispenser = algorand.client.get_testnet_dispenser_from_environment() + +# Alternatively, you can construct it directly from algokit_utils import TestNetDispenserApiClient # Using constructor argument - client = TestNetDispenserApiClient(auth_token="your_auth_token") # Using environment variable - import os -os.environ["ALGOKIT_DISPENSER_ACCESS_TOKEN"] = "your_auth_token" +os.environ['ALGOKIT_DISPENSER_ACCESS_TOKEN'] = 'your_auth_token' client = TestNetDispenserApiClient() ``` ## Funding an Account -To fund an account with Algos from the dispenser API, use the `fund` method. This method requires the receiver's address, the amount to be funded, and the asset ID. +To fund an account with Algo from the dispenser API, use the `fund` method. This method requires the receiver's address and the amount to be funded. -```py -response = client.fund(address="receiver_address", amount=1000, asset_id=0) +```python +response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, # Amount in microAlgos +) ``` -The `fund` method returns a `FundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. +The `fund` method returns a `DispenserFundResponse` object, which contains the transaction ID (`tx_id`) and the amount funded. ## Registering a Refund To register a refund for a transaction with the dispenser API, use the `refund` method. This method requires the transaction ID of the refund transaction. -```py -client.refund(refund_txn_id="transaction_id") +```python +dispenser.refund("transaction_id") ``` -> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by send funds back to TestNet Dispenser, then you can invoke this `refund` endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the `sender` field of any issued `fund` transaction initiated via [`fund`](#funding-an-account). +> Keep in mind, to perform a refund you need to perform a payment transaction yourself first by sending funds back to TestNet Dispenser, then you can invoke this refund endpoint and pass the txn_id of your refund txn. You can obtain dispenser address by inspecting the sender field of any issued fund transaction initiated via [fund](#funding-an-account). ## Getting Current Limit -To get the current limit for an account with Algos from the dispenser API, use the `get_limit` method. This method requires the account address. +To get the current limit for an account with Algo from the dispenser API, use the `get_limit` method. -```py -response = client.get_limit(address="account_address") +```python +response = dispenser.get_limit() ``` -The `get_limit` method returns a `LimitResponse` object, which contains the current limit amount. +The `get_limit` method returns a `DispenserLimitResponse` object, which contains the current limit amount. ## Error Handling If an error occurs while making a request to the dispenser API, an exception will be raised with a message indicating the type of error. Refer to [Error Handling docs](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md#error-handling) for details on how you can handle each individual error `code`. + +Here's an example of handling errors: + +```python +try: + response = dispenser.fund( + receiver="RECEIVER_ADDRESS", + amount=1000, + ) +except Exception as e: + print(f"Error occurred: {str(e)}") +``` diff --git a/docs/source/capabilities/testing.md b/docs/source/capabilities/testing.md new file mode 100644 index 00000000..bdc6ff7a --- /dev/null +++ b/docs/source/capabilities/testing.md @@ -0,0 +1,204 @@ +# Testing + +The following is a collection of useful snippets that can help you get started with testing your Algorand applications using AlgoKit utils. For the sake of simplicity, we'll use [pytest](https://docs.pytest.org/en/latest/) in the examples below. + +## Basic Test Setup + +Here's a basic test setup using pytest fixtures that provides common testing utilities: + +```python +import pytest +from algokit_utils import Account, SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.amount import AlgoAmount + +@pytest.fixture +def algorand() -> AlgorandClient: + """Get an AlgorandClient instance configured for LocalNet""" + return AlgorandClient.default_localnet() + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + """Create and fund a test account with ALGOs""" + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_algos(100), + min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account +``` + +Refer to [pytest fixture scopes](https://docs.pytest.org/en/latest/how-to/fixtures.html#fixture-scopes) for more information on how to control lifecycle of fixtures. + +## Creating Test Assets + +Here's a helper function to create test ASAs (Algorand Standard Assets): + +```python +def generate_test_asset(algorand: AlgorandClient, sender: Account, total: int | None = None) -> int: + """Create a test asset and return its ID""" + if total is None: + total = random.randint(20, 120) + + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TST", + asset_name=f"Test Asset {random.randint(1,100)}", + url="https://example.com", + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + ) + ) + + return int(create_result.confirmation["asset-index"]) +``` + +## Testing Application Deployments + +Here's how one can test smart contract application deployments: + +```python +def test_app_deployment(algorand: AlgorandClient, funded_account: SigningAccount): + """Test deploying a smart contract application""" + + # Load the application spec + app_spec = Path("artifacts/application.json").read_text() + + # Create app factory + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + + # Deploy the app + app_client, deploy_response = factory.deploy( + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, + ) + + # Verify deployment + assert deploy_response.app.app_id > 0 + assert deploy_response.app.app_address +``` + +## Testing Asset Transfers + +Here's how one can test ASA transfers between accounts: + +```python +def test_asset_transfer(algorand: AlgorandClient, funded_account: SigningAccount): + """Test ASA transfers between accounts""" + + # Create receiver account + receiver = algorand.account.random() + algorand.account.ensure_funded( + account_to_fund=receiver, + dispenser_account=funded_account, + min_spending_balance=AlgoAmount.from_algos(1) + ) + + # Create test asset + asset_id = generate_test_asset(algorand, funded_account, 100) + + # Opt receiver into asset + algorand.send.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer + ) + ) + + # Transfer asset + transfer_amount = 5 + result = algorand.send.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=receiver.address, + asset_id=asset_id, + amount=transfer_amount + ) + ) + + # Verify transfer + receiver_balance = algorand.asset.get_account_information(receiver, asset_id) + assert receiver_balance.balance == transfer_amount +``` + +## Testing Application Calls + +Here's how to test application method calls: + +```python +def test_app_method_call(algorand: AlgorandClient, funded_account: SigningAccount): + """Test calling ABI methods on an application""" + + # Deploy application first + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Call application method + result = app_client.send.call( + AppClientMethodCallParams( + method="hello", + args=["world"] + ) + ) + + # Verify result + assert result.abi_return == "Hello, world" +``` + +## Testing Box Storage + +Here's how to test application box storage: + +```python +def test_box_storage(algorand: AlgorandClient, funded_account: SigningAccount): + """Test application box storage""" + + # Deploy application + app_spec = Path("artifacts/application.json").read_text() + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address + ) + app_client, _ = factory.deploy() + + # Fund app account for box storage MBR + app_client.fund_app_account( + FundAppAccountParams(amount=AlgoAmount.from_algos(1)) + ) + + # Store value in box + box_name = b"test_box" + box_value = "test_value" + app_client.send.call( + AppClientMethodCallParams( + method="set_box", + args=[box_name, box_value], + box_references=[box_name] + ) + ) + + # Verify box value + stored_value = app_client.get_box_value(box_name) + assert stored_value == box_value.encode() +``` diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md new file mode 100644 index 00000000..f3c5df0e --- /dev/null +++ b/docs/source/capabilities/transaction-composer.md @@ -0,0 +1,228 @@ +# Transaction composer + +The `TransactionComposer` class allows you to easily compose one or more compliant Algorand transactions and execute and/or simulate them. + +It's the core of how the `AlgorandClient` class composes and sends transactions. + +```python +from algokit_utils import TransactionComposer, AppManager +from algokit_utils.transactions import ( + PaymentParams, + AppCallMethodCallParams, + AssetCreateParams, + AppCreateParams, + # ... other transaction parameter types +) +``` + +To get an instance of `TransactionComposer` you can either get it from an app client, from an `AlgorandClient`, or by instantiating via the constructor. + +```python +# From AlgorandClient +composer_from_algorand = algorand.new_group() + +# From AppClient +composer_from_app_client = app_client.algorand.new_group() + +# From constructor +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer +) + +# From constructor with optional params +composer_from_constructor = TransactionComposer( + algod=algod, + # Return the TransactionSigner for this address + get_signer=lambda address: signer, + # Custom function to get suggested params + get_suggested_params=lambda: algod.suggested_params(), + # Number of rounds the transaction should be valid for + default_validity_window=1000, + # Optional AppManager instance for TEAL compilation + app_manager=AppManager(algod) +) +``` + +## Constructing a transaction + +To construct a transaction you need to add it to the composer, passing in the relevant params object for that transaction. Params are Python dataclasses aavailable for import from `algokit_utils.transactions`. + +Parameter types include: + +- `PaymentParams` - For ALGO transfers +- `AssetCreateParams` - For creating ASAs +- `AssetConfigParams` - For reconfiguring ASAs +- `AssetTransferParams` - For ASA transfers +- `AssetOptInParams` - For opting in to ASAs +- `AssetOptOutParams` - For opting out of ASAs +- `AssetDestroyParams` - For destroying ASAs +- `AssetFreezeParams` - For freezing ASA balances +- `AppCreateParams` - For creating applications +- `AppCreateMethodCallParams` - For creating applications with ABI method calls +- `AppCallParams` - For calling applications +- `AppCallMethodCallParams` - For calling ABI methods on applications +- `AppUpdateParams` - For updating applications +- `AppUpdateMethodCallParams` - For updating applications with ABI method calls +- `AppDeleteParams` - For deleting applications +- `AppDeleteMethodCallParams` - For deleting applications with ABI method calls +- `OnlineKeyRegistrationParams` - For online key registration transactions +- `OfflineKeyRegistrationParams` - For offline key registration transactions + +The methods to construct a transaction are all named `add_{transaction_type}` and return an instance of the composer so they can be chained together fluently to construct a transaction group. + +For example: + +```python +from algokit_utils import AlgoAmount +from algokit_utils.transactions import AppCallMethodCallParams, PaymentParams + +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100), + note=b"Payment note" + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + boxes=[box_reference] # Optional box references + )) +) +``` + +## Simulating a transaction + +Transactions can be simulated using the simulate endpoint in algod, which enables evaluating the transaction on the network without it actually being committed to a block. +This is a powerful feature, which has a number of options which are detailed in the [simulate API docs](https://developer.algorand.org/docs/rest-apis/algod/#post-v2transactionssimulate). + +The `simulate()` method accepts several optional parameters that are passed through to the algod simulate endpoint: + +- `allow_more_logs: bool | None` - Allow more logs than standard +- `allow_empty_signatures: bool | None` - Allow transactions without signatures +- `allow_unnamed_resources: bool | None` - Allow unnamed resources in app calls +- `extra_opcode_budget: int | None` - Additional opcode budget +- `exec_trace_config: SimulateTraceConfig | None` - Execution trace configuration +- `simulation_round: int | None` - Round to simulate at +- `skip_signatures: int | None` - Skip signature verification + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate() +) + +# Access simulation results +simulate_response = result.simulate_response +confirmations = result.confirmations +transactions = result.transactions +returns = result.returns # ABI returns if any +``` + +### Simulate without signing + +There are situations where you may not be able to (or want to) sign the transactions when executing simulate. +In these instances you should set `skip_signatures=True` which automatically builds empty transaction signers and sets both `fix-signers` and `allow-empty-signatures` to `True` when sending the algod API call. + +For example: + +```python +result = ( + algorand.new_group() + .add_payment(PaymentParams( + sender="SENDER", + receiver="RECEIVER", + amount=AlgoAmount.from_micro_algos(100) + )) + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + )) + .simulate( + skip_signatures=True, + allow_more_logs=True, # Optional: allow more logs + extra_opcode_budget=700 # Optional: increase opcode budget + ) +) +``` + +### Resource Population + +The `TransactionComposer` includes automatic resource population capabilities for application calls. When sending or simulating transactions, it can automatically detect and populate required references for: + +- Account references +- Application references +- Asset references +- Box references + +This happens automatically when either: + +1. The global `algokit_utils.config` instance is set to `populate_app_call_resources=True` (default is `False`) +2. The `populate_app_call_resources` parameter is explicitly passed as `True` when sending transactions + +```python +# Automatic resource population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3] + # Resources will be automatically populated! + )) + .send(params=SendParams(populate_app_call_resources=True)) +) + +# Or disable automatic population +result = ( + algorand.new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender="SENDER", + app_id=123, + method=abi_method, + args=[1, 2, 3], + # Explicitly specify required resources + account_references=["ACCOUNT"], + app_references=[456], + asset_references=[789], + box_references=[box_reference] + )) + .send(params=SendParams(populate_app_call_resources=False)) +) +``` + +The resource population: + +- Respects the maximum limits (4 for accounts, 8 for foreign references) +- Handles cross-reference resources efficiently (e.g., asset holdings and local state) +- Automatically distributes resources across multiple transactions in a group when needed +- Raises descriptive errors if resource limits are exceeded + +This feature is particularly useful when: + +- Working with complex smart contracts that access various resources +- Building transaction groups where resources need to be coordinated +- Developing applications where resource requirements may change dynamically + +Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. diff --git a/docs/source/capabilities/transaction.md b/docs/source/capabilities/transaction.md new file mode 100644 index 00000000..e10a8e7b --- /dev/null +++ b/docs/source/capabilities/transaction.md @@ -0,0 +1,147 @@ +# Transaction management + +Transaction management is one of the core capabilities provided by AlgoKit Utils. It allows you to construct, simulate and send single or grouped transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, multiple sender account types, and sending behavior. + +## Transaction Results + +All AlgoKit Utils functions that send transactions return either a `SendSingleTransactionResult` or `SendAtomicTransactionComposerResults`, providing consistent mechanisms to interpret transaction outcomes. + +### SendSingleTransactionResult + +The base `SendSingleTransactionResult` class is used for single transactions: + +```python +@dataclass(frozen=True, kw_only=True) +class SendSingleTransactionResult: + transaction: TransactionWrapper # Last transaction + confirmation: AlgodResponseType # Last confirmation + group_id: str + tx_id: str | None = None # Transaction ID of the last transaction + tx_ids: list[str] # All transaction IDs in the group + transactions: list[TransactionWrapper] + confirmations: list[AlgodResponseType] + returns: list[ABIReturn] | None = None # ABI returns if applicable +``` + +Common variations include: + +- `SendSingleAssetCreateTransactionResult` - Adds `asset_id` +- `SendAppTransactionResult` - Adds `abi_return` +- `SendAppUpdateTransactionResult` - Adds compilation results +- `SendAppCreateTransactionResult` - Adds `app_id` and `app_address` + +### SendAtomicTransactionComposerResults + +When using the atomic transaction composer directly via `TransactionComposer.send()` or `TransactionComposer.simulate()`, you'll receive a `SendAtomicTransactionComposerResults`: + +```python +@dataclass +class SendAtomicTransactionComposerResults: + group_id: str # The group ID if this was a transaction group + confirmations: list[AlgodResponseType] # The confirmation info for each transaction + tx_ids: list[str] # The transaction IDs that were sent + transactions: list[TransactionWrapper] # The transactions that were sent + returns: list[ABIReturn] # The ABI return values from any ABI method calls + simulate_response: dict[str, Any] | None = None # Simulation response if simulated +``` + +### Application-specific Result Types + +When working with applications via `AppClient` or `AppFactory`, you'll get enhanced result types that provide direct access to parsed ABI values: + +- `SendAppFactoryTransactionResult` +- `SendAppUpdateFactoryTransactionResult` +- `SendAppCreateFactoryTransactionResult` + +These types extend the base transaction results to add an `abi_value` field that contains the parsed ABI return value according to the ARC-56 specification. The `Arc56ReturnValueType` can be: + +- A primitive ABI value (bool, int, str, bytes) +- An ABI struct (as a Python dict) +- None (for void returns) + +### Where You'll Encounter Each Result Type + +Different interfaces return different result types: + +1. **Direct Transaction Composer** + + - `TransactionComposer.send()` → `SendAtomicTransactionComposerResults` + - `TransactionComposer.simulate()` → `SendAtomicTransactionComposerResults` + +2. **AlgorandClient Methods** + + - `.send.payment()` → `SendSingleTransactionResult` + - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` (contains raw ABI return) + - `.send.app_create()` → `SendAppCreateTransactionResult` (with app ID/address) + - `.send.app_update()` → `SendAppUpdateTransactionResult` (with compilation info) + +3. **AppClient Methods** + + - `.call()` → `SendAppTransactionResult` + - `.create()` → `SendAppCreateTransactionResult` + - `.update()` → `SendAppUpdateTransactionResult` + +4. **AppFactory Methods** + - `.create()` → `SendAppCreateFactoryTransactionResult` + - `.call()` → `SendAppFactoryTransactionResult` + - `.update()` → `SendAppUpdateFactoryTransactionResult` + +Example usage with AppFactory for easy access to ABI returns: + +```python +# Using AppFactory +result = app_factory.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Access the parsed ABI return value directly +parsed_value = result.abi_value # Already decoded per ARC-56 spec + +# Compared to base AppClient where you need to parse manually +base_result = app_client.send.call(AppCallMethodCallParams( + method="my_method", + args=[1, 2, 3], + sender=sender +)) +# Need to manually handle ABI return parsing +if base_result.abi_return: + parsed_value = base_result.abi_return.value +``` + +Key differences between result types: + +1. **Base Transaction Results** (`SendSingleTransactionResult`) + + - Focus on transaction confirmation details + - Include group support but optimized for single transactions + - No direct ABI value parsing + +2. **Atomic Transaction Results** (`SendAtomicTransactionComposerResults`) + + - Built for transaction groups + - Include simulation support + - Raw ABI returns via `.returns` + - No single transaction convenience fields + +3. **Application Results** (`SendAppTransactionResult` family) + + - Add application-specific fields (`app_id`, compilation results) + - Include raw ABI returns via `.abi_return` + - Base application transaction support + +4. **Factory Results** (`SendAppFactoryTransactionResult` family) + - Highest level of abstraction + - Direct access to parsed ABI values via `.abi_value` + - Automatic ARC-56 compliant value parsing + - Combines app-specific fields with parsed ABI returns + +## Further reading + +To understand how to create, simulate and send transactions consult: + +- The [`TransactionComposer`](./transaction-composer.md) documentation for composing transaction groups +- The [`AlgorandClient`](./algorand-client.md) documentation for a high-level interface to send transactions + +The transaction composer documentation covers the details of constructing transactions and transaction groups, while the Algorand client documentation covers the high-level interface for sending transactions. diff --git a/docs/source/capabilities/transfer.md b/docs/source/capabilities/transfer.md index af28b6d1..2f61e8e2 100644 --- a/docs/source/capabilities/transfer.md +++ b/docs/source/capabilities/transfer.md @@ -1,58 +1,151 @@ -# Algo transfers - -Algo transfers is a higher-order use case capability provided by AlgoKit Utils allows you to easily initiate algo transfers between accounts, including dispenser management and -idempotent account funding. - -To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_transfer.py). - -## Transferring Algos - -The key function to facilitate Algo transfers is `algokit.transfer(algod_client, transfer_parameters)`, which returns the underlying `EnsureFundedResponse` and takes a `TransferParameters` - -The following fields on `TransferParameters` are required to transfer ALGOs: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `micro_algos`: The amount of micro ALGOs to send - -## Ensuring minimum Algos - -The ability to automatically fund an account to have a minimum amount of disposable ALGOs to spend is incredibly useful for automation and deployment scripts. -The function to facilitate this is `ensure_funded(client, parameters)`, which takes an `EnsureBalanceParameters` instance and returns the underlying `EnsureFundedResponse` if a payment was made, a string if the dispenser API was used, or None otherwise. - -The following fields on `EnsureBalanceParameters` are required to ensure minimum ALGOs: - -- `account_to_fund`: The account address that will receive the ALGOs. This can be an `Account` instance, an `AccountTransactionSigner` instance, or a string. -- `min_spending_balance_micro_algos`: The minimum balance of micro ALGOs that the account should have available to spend (i.e. on top of minimum balance requirement). -- `min_funding_increment_micro_algos`: When issuing a funding amount, the minimum amount to transfer (avoids many small transfers if this gets called often on an active account). Default is 0. -- `funding_source`: The account (with private key) or signer that will send the ALGOs. If not set, it will use `get_dispenser_account`. This can be an `Account` instance, an `AccountTransactionSigner` instance, [`TestNetDispenserApiClient`](https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/capabilities/dispenser-client.md) instance, or None. -- `suggested_params`: (optional) Transaction parameters, an instance of `SuggestedParams`. -- `note`: (optional) The transaction note, default is "Funding account to meet minimum requirement". -- `fee_micro_algos`: (optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call. -- `max_fee_micro_algos`: (optional) The maximum fee that you are happy to pay (default: unbounded). If this is set it's possible the transaction could get rejected during network congestion. - -The function calls Algod to find the current balance and minimum balance requirement, gets the difference between those two numbers and checks to see if it's more than the `min_spending_balance_micro_algos`. If so, it will send the difference, or the `min_funding_increment_micro_algos` if that is specified. If the account is on TestNet and `use_dispenser_api` is True, the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md) will be used to fund the account. - -> Please note, if you are attempting to fund via Dispenser API, make sure to set `ALGOKIT_DISPENSER_ACCESS_TOKEN` environment variable prior to invoking `ensure_funded`. To generate the token refer to [AlgoKit CLI documentation](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/dispenser.md#login) - -## Transfering Assets - -The key function to facilitate asset transfers is `transfer_asset(algod_client, transfer_parameters)`, which returns a `AssetTransferTxn` and takes a `TransferAssetParameters`: - -The following fields on `TransferAssetParameters` are required to transfer assets: - -- `from_account`: The account or signer that will send the ALGOs -- `to_address`: The address of the account that will receive the ALGOs -- `asset_id`: The asset id that will be transfered -- `amount`: The amount to send as the smallest divisible unit value +# Algo transfers (payments) + +Algo transfers, or [payments](https://developer.algorand.org/docs/get-details/transactions/#payment-transaction), is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [Algo amount handling](./amount.md) and [Transaction management](./transaction.md). It allows you to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding. + +To see some usage examples check out the automated tests in the repository. + +## `payment` + +The key function to facilitate Algo transfers is `algorand.send.payment(params)` (immediately send a single payment transaction), `algorand.create_transaction.payment(params)` (construct a payment transaction), or `algorand.new_group().add_payment(params)` (add payment to a group of transactions) per [`AlgorandClient`](./algorand-client.md) [transaction semantics](./algorand-client.md#creating-and-issuing-transactions). + +The base type for specifying a payment transaction is `PaymentParams`, which has the following parameters in addition to the [common transaction parameters](./algorand-client.md#transaction-parameters): + +- `receiver: str` - The address of the account that will receive the Algo +- `amount: AlgoAmount` - The amount of Algo to send +- `close_remainder_to: Optional[str]` - If given, close the sender account and send the remaining balance to this address (**warning:** use this carefully as it can result in loss of funds if used incorrectly) + +```python +# Minimal example +result = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo") + ) +) + +# Advanced example +result2 = algorand_client.send.payment( + PaymentParams( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=AlgoAmount(4, "algo"), + close_remainder_to="CLOSEREMAINDERTOADDRESS", + lease="lease", + note=b"note", + # Use this with caution, it's generally better to use algorand_client.account.rekey_account + rekey_to="REKEYTOADDRESS", + # You wouldn't normally set this field + first_valid_round=1000, + validity_window=10, + extra_fee=AlgoAmount(1000, "microalgo"), + static_fee=AlgoAmount(1000, "microalgo"), + # Max fee doesn't make sense with extra_fee AND static_fee + # already specified, but here for completeness + max_fee=AlgoAmount(3000, "microalgo"), + # Signer only needed if you want to provide one, + # generally you'd register it with AlgorandClient + # against the sender and not need to pass it in + signer=transaction_signer, + ), + send_params=SendParams( + max_rounds_to_wait=5, + suppress_log=True, + ) +) +``` + +## `ensure_funded` + +The `ensure_funded` function automatically funds an account to maintain a minimum amount of [disposable Algo](https://developer.algorand.org/docs/get-details/accounts/#minimum-balance). This is particularly useful for automation and deployment scripts that get run multiple times and consume Algo when run. + +There are 3 variants of this function: + +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + - **Note:** requires environment variables to be set. + - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` + if it's a rekeyed account, or against default LocalNet if no environment variables present. +- `algorand_client.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). + +The general structure of these calls is similar, they all take: + +- `account_to_fund: str | Account` - Address or signing account of the account to fund +- The source (dispenser): + - In `ensure_funded`: `dispenser_account: str | Account` - the address or signing account of the account to use as a dispenser + - In `ensure_funded_from_environment`: Not specified, loaded automatically from the ephemeral environment + - In `ensure_funded_from_testnet_dispenser_api`: `dispenser_client: TestNetDispenserApiClient` - a client instance of the TestNet dispenser API +- `min_spending_balance: AlgoAmount` - The minimum balance of Algo that the account should have available to spend (i.e., on top of the minimum balance requirement) +- An `options` object, which has: + - [Common transaction parameters](./algorand-client.md#transaction-parameters) (not for TestNet Dispenser API) + - [Execution parameters](./algorand-client.md#sending-a-single-transaction) (not for TestNet Dispenser API) + - `min_funding_increment: Optional[AlgoAmount]` - When issuing a funding amount, the minimum amount to transfer; this avoids many small transfers if this function gets called often on an active account + +### Examples + +```python +# From account + +# Basic example +algorand_client.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded( + "ACCOUNTADDRESS", + "DISPENSERADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# From environment + +# Basic example +algorand_client.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +# With configuration +algorand_client.account.ensure_funded_from_environment( + "ACCOUNTADDRESS", + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), + fee=AlgoAmount(1000, "microalgo"), + send_params=SendParams( + suppress_log=True, + ), +) + +# TestNet Dispenser API + +# Basic example +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo") +) +# With configuration +algorand_client.account.ensure_funded_from_testnet_dispenser_api( + "ACCOUNTADDRESS", + algorand_client.client.get_testnet_dispenser_from_environment(), + AlgoAmount(1, "algo"), + min_funding_increment=AlgoAmount(2, "algo"), +) +``` + +All 3 variants return an `EnsureFundedResponse` (and the first two also return a [single transaction result](./algorand-client.md#sending-a-single-transaction)) if a funding transaction was needed, or `None` if no transaction was required: + +- `amount_funded: AlgoAmount` - The number of Algo that was paid +- `transaction_id: str` - The ID of the transaction that funded the account + +If you are using the TestNet Dispenser API then the `transaction_id` is useful if you want to use the [refund functionality](./dispenser-client.md#registering-a-refund). ## Dispenser -If you want to programmatically send funds then you will often need a "dispenser" account that has a store of ALGOs that can be sent and a private key available for that dispenser account. +If you want to programmatically send funds to an account so it can transact then you will often need a "dispenser" account that has a store of Algo that can be sent and a private key available for that dispenser account. -There is a standard AlgoKit Utils function to get access to a [dispenser account](./account.md#account): `get_dispenser_account`. When running against -[LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md), the dispenser account can be automatically determined using the -[Kmd API](https://developer.algorand.org/docs/rest-apis/kmd). When running against other networks like TestNet or MainNet the mnemonic of the dispenser account can be provided via environment -variable `DISPENSER_MNEMONIC` +There's a number of ways to get a dispensing account in AlgoKit Utils: -Please note that this does not refer to the [AlgoKit TestNet Dispenser API](./dispenser-client.md) which is a separate abstraction that can be used to fund accounts on TestNet via dedicated API service. +- Get a dispenser via [account manager](./account.md#dispenser) - either automatically from [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) or from the environment +- By programmatically creating one of the many account types via [account manager](./account.md#accounts) +- By programmatically interacting with [KMD](./account.md#kmd-account-management) if running against LocalNet +- By using the [AlgoKit TestNet Dispenser API client](./dispenser-client.md) which can be used to fund accounts on TestNet via a dedicated API service diff --git a/docs/source/capabilities/typed-app-clients.md b/docs/source/capabilities/typed-app-clients.md new file mode 100644 index 00000000..710323c1 --- /dev/null +++ b/docs/source/capabilities/typed-app-clients.md @@ -0,0 +1,200 @@ +# Typed application clients + +Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) or [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract. + +Typed application clients are the recommended way of interacting with smart contracts. If you don't have/want a typed client, but have an ARC-56/ARC-32 app spec then you can use the [non-typed application clients](./app-client.md) and if you want to call a smart contract you don't have an app spec file for you can use the underlying [app management](./app.md) and [app deployment](./app-deploy.md) functionality to manually construct transactions. + +## Generating an app spec + +You can generate an app spec file: + +- Using [Algorand Python](https://algorandfoundation.github.io/puya/#quick-start) +- Using [TEALScript](https://tealscript.netlify.app/tutorials/hello-world/0004-artifacts/) +- By hand by following the specification [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258)/[ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) +- Using [Beaker](https://algorand-devrel.github.io/beaker/html/usage.html) (PyTEAL) _(DEPRECATED)_ + +## Generating a typed client + +To generate a typed client from an app spec file you can use [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients): + +``` +> algokit generate client application.json --output /absolute/path/to/client.py +``` + +Note: AlgoKit Utils >= 3.0.0 is compatible with the older 1.x.x generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 2.x.x. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version. + +## Getting a typed client instance + +To get an instance of a typed client you can use an [`AlgorandClient`](./algorand-client.md) instance or a typed app [`Factory`](#creating-a-typed-factory-instance) instance. + +The approach to obtaining a client instance depends on how many app clients you require for a given app spec and if the app has already been deployed, which is summarised below: + +### App is deployed + + + + + + + + + + + + + + + + + + + + + + +
Resolve App by IDResolve App by Creator and Name
Single App Client InstanceMultiple App Client InstancesSingle App Client InstanceMultiple App Client Instances
+ +```python +app_client = algorand.client.get_typed_app_client_by_id(MyContractClient, { + app_id=1234, + # ... +}) +# or +app_client = MyContractClient({ + algorand, + app_id=1234, + # ... +}) +``` + + + +```python +app_client1 = factory.get_app_client_by_id( + app_id=1234, + # ... +) +app_client2 = factory.get_app_client_by_id( + app_id=4321, + # ... +) +``` + + + +```python +app_client = algorand.client.get_typed_app_client_by_creator_and_name( + MyContractClient, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +# or +app_client = MyContractClient.from_creator_and_name( + algorand, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +``` + + + +```python +app_client1 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +app_client2 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name-2", + # ... +) +``` + +
+ +To understand the difference between resolving by ID vs by creator and name see the underlying [app client documentation](./app-client.md#appclient). + +### App is not deployed + + + + + + + + + + + + + + +
Deploy a New AppDeploy or Resolve App Idempotently by Creator and Name
+ +```python +app_client, response = factory.deploy( + args=[], + # ... +) +# or +app_client, response = factory.send.create.METHODNAME( + args=[], + # ... +) +``` + + + +```python +app_client, response = factory.deploy( + app_name="contract-name", + # ... +) +``` + +
+ +### Creating a typed factory instance + +If your scenario calls for an app factory, you can create one using the below: + +```python +factory = algorand.client.get_typed_app_factory(MyContractFactory) +# or +factory = MyContractFactory(algorand) +``` + +## Client usage + +See the [official usage docs](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/usage.md) for full details. + +For a simple example that deploys a contract and calls a `"hello"` method, see below: + +```python +# A similar working example can be seen in the AlgoKit init production smart contract templates, when using Python deployment +# In this case the generated factory is called `HelloWorldAppFactory` and is in `./artifacts/HelloWorldApp/client.py` +from artifacts.hello_world_app.client import HelloWorldAppClient, HelloArgs +from algokit_utils import AlgorandClient + +# These require environment variables to be present, or it will retrieve from default LocalNet +algorand = AlgorandClient.from_environment() +deployer = algorand.account.from_environment("DEPLOYER", AlgoAmount.from_algo(1)) + +# Create the typed app factory +factory = algorand.client.get_typed_app_factory(HelloWorldAppFactory, + creator_address=deployer, + default_sender=deployer, +) + +# Create the app and get a typed app client for the created app (note: this creates a new instance of the app every time, +# you can use .deploy() to deploy idempotently if the app wasn't previously +# deployed or needs to be updated if that's allowed) +app_client, response = factory.send.create() + +# Make a call to an ABI method and print the result +response = app_client.send.hello(args=HelloArgs(name="world")) +print(response) +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 35b6a76e..72fa0770 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,75 +1,3 @@ -from __future__ import annotations - -import typing as t - -import typing_extensions as te -from autodoc2.render.myst_ import MystRenderer -from autodoc2.utils import ItemData -from sphinx.domains.python import PythonDomain - - -class AlgoKitRenderer(MystRenderer): - """Render the documentation as MyST. - - Based on the code in - https://github.com/bytewax/bytewax/blob/58bc1d9517f11578c914407a057960b76d8d9b1b/docs/renderer.py#L16 - - """ - - @te.override - def render_package(self, item: ItemData) -> t.Iterable[str]: # noqa: C901 - if self.standalone and self.is_hidden(item): - yield from ["---", "orphan: true", "---", ""] - - full_name = item["full_name"] - - yield f"# {{py:mod}}`{full_name}`" - yield "" - - yield f"```{{py:module}} {full_name}" - if self.no_index(item): - yield ":noindex:" - if self.is_module_deprecated(item): - yield ":deprecated:" - yield from ["```", ""] - - if self.show_docstring(item): - yield f"```{{autodoc2-docstring}} {item['full_name']}" - if parser_name := self.get_doc_parser(item["full_name"]): - yield f":parser: {parser_name}" - yield ":allowtitles:" - yield "```" - yield "" - - visible_submodules = [i["full_name"] for i in self.get_children(item, {"module", "package"})] - if visible_submodules: - yield "## Submodules" - yield "" - yield "```{toctree}" - yield ":titlesonly:" - yield "" - yield from sorted(visible_submodules) - yield "```" - yield "" - - visible_children = [i["full_name"] for i in self.get_children(item) if i["type"] not in ("package", "module")] - if not visible_children: - return - - for heading, types in [ - ("Data", {"data"}), - ("Classes", {"class"}), - ("Functions", {"function"}), - ("External", {"external"}), - ]: - visible_items = list(self.get_children(item, types)) - if visible_items: - yield from [f"## {heading}", ""] - for i in visible_items: - yield from self.render_item(i["full_name"]) - yield "" - - # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -77,74 +5,52 @@ def render_package(self, item: ItemData) -> t.Iterable[str]: # noqa: C901 # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from __future__ import annotations -project = "algokit-utils" -copyright = "2023, Algorand Foundation" # noqa: A001 -author = "Algorand Foundation" -release = "1.0" +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + from sphinx.domains.python import PyObject + +project = 'algokit-utils-py' +copyright = '2025, Algorand Foundation' +author = 'Algorand Foundation' +release = '3.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [ - "sphinx.ext.githubpages", - "sphinx.ext.intersphinx", - "myst_parser", - "autodoc2", -] -templates_path = ["_templates"] -exclude_patterns = [] # type: ignore -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "algosdk": ("https://py-algorand-sdk.readthedocs.io/en/latest", None), - "pyteal": ("https://pyteal.readthedocs.io/en/stable/", None), -} -# allows type aliases to be used as type references -PythonDomain.object_types["data"].roles = ("data", "class", "obj") - +extensions = ['myst_parser', 'autoapi.extension', "sphinx.ext.autosectionlabel", "sphinx.ext.githubpages"] + +templates_path = ['_templates'] +exclude_patterns = [] + +autoapi_dirs = ['../../src/algokit_utils'] +autoapi_options = ['members', + 'undoc-members', + 'show-inheritance', + 'show-module-summary'] +autoapi_ignore = ['*algokit_utils/beta/__init__.py', + '*algokit_utils/asset.py', + '*algokit_utils/deploy.py', + "*algokit_utils/network_clients.py", + "*algokit_utils/common.py", + "*algokit_utils/account.py", + "*algokit_utils/application_client.py", + "*algokit_utils/application_specification.py", + "*algokit_utils/logic_error.py", + "*algokit_utils/dispenser_api.py"] + +myst_heading_anchors = 5 +myst_all_links_external = False +autosectionlabel_prefix_document = True # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "sphinx_rtd_theme" -html_static_path = [] # type: ignore - - -# -- Options for myst --- -myst_enable_extensions = [ - "colon_fence", - "fieldlist", - "deflist", - "tasklist", - "attrs_inline", - "attrs_block", - "substitution", - "linkify", -] - -myst_heading_anchors = 3 -myst_all_links_external = False - -# -- Options for autodoc2 --- -autodoc2_packages = [ - { - "path": "../../src/algokit_utils", - }, -] -autodoc2_skip_module_regexes = [r"algokit_utils\..*"] -autodoc2_module_all_regexes = [ - r"algokit_utils", -] -autodoc2_docstring_parser_regexes = [ - # this will render all docstrings as Markdown - (r".*", "myst"), -] -autodoc2_hidden_objects = [ - "undoc", # undocumented objects - "dunder", # double-underscore methods, e.g. __str__ - "private", # single-underscore methods, e.g. _private - "inherited", -] -autodoc2_render_plugin = AlgoKitRenderer -autodoc2_sort_names = True -autodoc2_index_template = None +html_theme = 'furo' +html_static_path = ['_static'] +pygments_style = "sphinx" +pygments_dark_style = "monokai" +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] diff --git a/docs/source/index.md b/docs/source/index.md index ede55d1b..1cc22eff 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,16 +1,14 @@ # AlgoKit Python Utilities -A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. -This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). +A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli). -The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. -Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. +The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. ```{note} If you prefer TypeScript there's an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). ``` -[Core principles](#core-principles) | [Installation](#installation) | [Usage](#usage) | [Capabilities](#capabilities) | [Reference docs](#reference-documentation) +{ref}`Core principles ` | {ref}`Installation ` | {ref}`Usage ` | {ref}`Config and logging ` | {ref}`Capabilities ` | {ref}`Reference docs ` ```{toctree} --- @@ -19,34 +17,42 @@ caption: Contents --- capabilities/account -capabilities/client +capabilities/algorand-client +capabilities/amount capabilities/app-client capabilities/app-deploy -capabilities/transfer +capabilities/app +capabilities/asset +capabilities/client +capabilities/debugging capabilities/dispenser-client -capabilities/debugger -apidocs/algokit_utils/algokit_utils +capabilities/testing +capabilities/transaction-composer +capabilities/transaction +capabilities/transfer +capabilities/typed-app-clients +v3-migration-guide ``` (core-principles)= # Core principles -This library is designed with the following principles: +This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/algokit.md#guiding-principles) and is designed with the following principles: -- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are - exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. -- **Type-safety** - This library provides strong TypeScript support with effort put into creating types that provide good type safety and intellisense. -- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write +- **Modularity** - This library is a thin wrapper of modular building blocks over the Algorand SDK; the primitives from the underlying Algorand SDK are exposed and used wherever possible so you can opt-in to which parts of this library you want to use without having to use an all or nothing approach. +- **Type-safety** - This library provides strong type hints with effort put into creating types that provide good type safety and intellisense when used with tools like MyPy. +- **Productivity** - This library is built to make solution developers highly productive; it has a number of mechanisms to make common code easier and terser to write. (installation)= # Installation -This library can be installed from PyPi using pip or poetry, e.g.: +This library can be installed from PyPi using pip or poetry: -``` +```bash pip install algokit-utils +# or poetry add algokit-utils ``` @@ -54,50 +60,96 @@ poetry add algokit-utils # Usage -To use this library simply include the following at the top of your file: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: ```python -import algokit_utils +# Point to the network configured through environment variables or +# if no environment variables it will point to the default LocalNet configuration +algorand = AlgorandClient.from_environment() +# Point to default LocalNet configuration +algorand = AlgorandClient.default_localnet() +# Point to TestNet using AlgoNode free tier +algorand = AlgorandClient.testnet() +# Point to MainNet using AlgoNode free tier +algorand = AlgorandClient.mainnet() +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=...) +# Point to a pre-created algod and indexer client +algorand = AlgorandClient.from_clients(algod=..., indexer=..., kmd=...) +# Point to custom configuration for algod +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod and indexer and kmd +algod_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +indexer_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +kmd_config = AlgoClientNetworkConfig(server=..., token=..., port=...) +algorand = AlgorandClient.from_config(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) ``` -Then you can use intellisense to auto-complete the various functions and types that are available by typing `algokit_utils.` in your favourite Integrated Development Environment (IDE), -or you can refer to the [reference documentation](apidocs/algokit_utils/algokit_utils.md). +# Testing -## Types +AlgoKit Utils provides a dedicated documentation page on various useful snippets that can be reused for testing with tools like [Pytest](https://docs.pytest.org/en/latest/): -The library contains extensive type hinting combined with a tool like MyPy this can help identify issues where incorrect types have been used, or used incorrectly. +- [Testing](capabilities/testing) -(capabilities)= +# Types -# Capabilities +The library leverages Python's native type hints and is fully compatible with [MyPy](https://mypy-lang.org/) for static type checking. -The library helps you with the following capabilities: +All public abstractions and methods are organized in logical modules matching their domain functionality. You can import types either directly from the root module or from their source submodules. Refer to [API documentation](autoapi/index) for more details. -- Core capabilities - - [**Client management**](capabilities/client.md) - Creation of algod, indexer and kmd clients against various networks resolved from environment or specified configuration - - [**Account management**](capabilities/account.md) - Creation and use of accounts including mnemonic, multisig, transaction signer, idempotent KMD accounts and environment variable injected -- Higher-order use cases - - [**ARC-0032 Application Spec client**](capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities to provide a high productivity application client that works with ARC-0032 application spec defined smart contracts (e.g. via Beaker) - - [**App deployment**](capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution - - [**Algo transfers**](capabilities/transfer.md) - Ability to easily initiate algo transfers between accounts, including dispenser management and idempotent account funding - - [**Debugger**](capabilities/debugger.md) - Provides a set of debugging tools that can be used to simulate and trace transactions on the Algorand blockchain. These tools and methods are optimized for developers who are building applications on Algorand and need to test and debug their smart contracts via [AVM Debugger extension](https://github.com/algorandfoundation/algokit-avm-vscode-debugger). +(config-logging)= -(reference-documentation)= +# Config and logging -# Reference documentation +To configure the AlgoKit Utils library you can make use of the [`Config`](autoapi/algokit_utils/config/index) object, which has a configure method that lets you configure some or all of the configuration options. + +## Config singleton + +The AlgoKit Utils configuration singleton can be updated using `config.configure()`. Refer to the [Config API documentation](autoapi/algokit_utils/config/index) for more details. + +## Logging -We have [auto-generated reference documentation for the code](apidocs/algokit_utils/algokit_utils.md). +AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`](apidocs/algokit_utils/config/index) class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. -# Roadmap +Each method supports optional suppression of output using the `suppress_log` parameter. -This library will naturally evolve with any logical developer experience improvements needed to facilitate the [AlgoKit](https://github.com/algorandfoundation/algokit-cli) roadmap as it evolves. +## Debug mode -Likely future capability additions include: +To turn on debug mode you can use the following: -- Typed application client -- Asset management -- Expanded indexer API wrapper support +```python +from algokit_utils.config import config +config.configure(debug=True) +``` + +To retrieve the current debug state you can use `debug` property. + +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](capabilities/debugger). It's likely this option will result in extra HTTP calls to algod os worth being careful when it's turned on. + +(capabilities)= -# Indices and tables +# Capabilities + +The library helps you interact with and develop against the Algorand blockchain with a series of end-to-end capabilities as described below: + +- [**AlgorandClient**](./capabilities/algorand-client.md) - The key entrypoint to the AlgoKit Utils functionality +- **Core capabilities** + - [**Client management**](./capabilities/client.md) - Creation of (auto-retry) algod, indexer and kmd clients against various networks resolved from environment or specified configuration, and creation of other API clients (e.g. TestNet Dispenser API and app clients) + - [**Account management**](./capabilities/account.md) - Creation, use, and management of accounts including mnemonic, rekeyed, multisig, transaction signer, idempotent KMD accounts and environment variable injected + - [**Algo amount handling**](./capabilities/amount.md) - Reliable, explicit, and terse specification of microAlgo and Algo amounts and safe conversion between them + - [**Transaction management**](./capabilities/transaction.md) - Ability to construct, simulate and send transactions with consistent and highly configurable semantics, including configurable control of transaction notes, logging, fees, validity, signing, and sending behaviour +- **Higher-order use cases** + - [**Asset management**](./capabilities/asset.md) - Creation, transfer, destroying, opting in and out and managing Algorand Standard Assets + - [**Typed application clients**](./capabilities/typed-app-clients.md) - Type-safe application clients that are [generated](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients) from ARC-56 or ARC-32 application spec files and allow you to intuitively and productively interact with a deployed app, which is the recommended way of interacting with apps and builds on top of the following capabilities: + - [**ARC-56 / ARC-32 App client and App factory**](./capabilities/app-client.md) - Builds on top of the App management and App deployment capabilities (below) to provide a high productivity application client that works with ARC-56 and ARC-32 application spec defined smart contracts + - [**App management**](./capabilities/app.md) - Creation, updating, deleting, calling (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes) + - [**App deployment**](./capabilities/app-deploy.md) - Idempotent (safely retryable) deployment of an app, including deploy-time immutability and permanence control and TEAL template substitution + - [**Algo transfers (payments)**](./capabilities/transfer.md) - Ability to easily initiate Algo transfers between accounts, including dispenser management and idempotent account funding + - [**Automated testing**](./capabilities/testing.md) - Reusable snippets to leverage AlgoKit Utils abstractions in a manner that are useful for when writing tests in tools like [Pytest](https://docs.pytest.org/en/latest/). + +(reference-documentation)= + +# Reference documentation -- {ref}`genindex` +For detailed API documentation, see the [auto-generated reference documentation](apidocs/algokit_utils/algokit_utils.md). diff --git a/docs/source/v3-migration-guide.md b/docs/source/v3-migration-guide.md new file mode 100644 index 00000000..9d64cc16 --- /dev/null +++ b/docs/source/v3-migration-guide.md @@ -0,0 +1,314 @@ +# Migration Guide - v3 + +Version 3 of `algokit-utils-ts` moved from a stateless function-based interface to a stateful class-based interfaces. This change allows for: + +- Easier and simpler consumption experience guided by IDE autocompletion +- Less redundant parameter passing (e.g., `algod` client) +- Better performance through caching of commonly retrieved values like transaction parameters +- More consistent and intuitive API design +- Stronger type safety and better error messages +- Improved ARC-56 compatibility +- Feature parity with `algokit-utils-ts` >= `v7` interfaces + +The entry point to most functionality in AlgoKit Utils is now available via a single entry-point, the `AlgorandClient` class. + +The v2 interfaces and abstractions will be removed in future major version bumps, however in order to ensure gradual migration, _all v2 abstractions are available_ with respective deprecation warnings. The new way to use AlgoKit Utils is via the `AlgorandClient` class, which is easier, simpler, and more convenient to use and has powerful new features. + +> BREAKING CHANGE: the `beta` module is now removed, any imports from `algokit_utils.beta` will now raise an error with a link to a new expected import path. This is due to the fact that the interfaces introduced in `beta` are now refined and available in the main module. + +## Migration Steps + +In general, your codebase might fall into one of the following migration scenarios: + +- Using `algokit-utils-py` v2.x only without use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x only and with use of abstractions from `beta` module +- Using `algokit-utils-py` v2.x with `algokit-client-generator-py` v1.x +- Using `algokit-client-generator-py` v1.x only (implies implicit dependency on `algokit-utils-py` v2.x) + +Given that `algokit-utils-py` v3.x is backwards compatible with `algokit-client-generator-py` v1.x, the following general guidelines are applicable to all scenarios (note that the order of operations is important to ensure straight-forward migration): + +1. Upgrade to `algokit-utils-py` v3.x + - 1.1 (If used) Update imports from `algokit_utils.beta` to `algokit_utils` + - 1.2 Follow hints in deprecation warnings to update your codebase to rely on latest v3 interfaces +2. Upgrade to `algokit-client-generator-py` v2.x and regenerate typed clients + - 2.1 Follow `algokit-client-generator-py` [v2.x migration guide](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/v2-migration-guide.md) + +The remaining set of guidelines are outlining migrations for specific abstractions that had direct equivalents in `algokit-utils-py` v2.x. + +### Prerequisites + +It is important to reiterate that if you have previously relied on `beta` versions of `algokit-utils-py` v2.x, you will need to update your imports to rely on the new interfaces. Errors thrown during import from `beta` will provide a description of the new expected import path. + +> As with `v2.x` all public abstractions in `algokit_utils` are available for direct imports `from algokit_utils import ...`, however underlying modules have been refined to be structured loosely around common AVM domains such as `applications`, `transactions`, `accounts`, `assets`, etc. See [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) for latest and detailed overview. + +### Step 1 - Replace SDK Clients with AlgorandClient + +First, replace your SDK client initialization with `AlgorandClient`. Look for `get_algod_client` calls and replace with an appropriate `AlgorandClient` initialization: + +```python +"""Before""" +import algokit_utils +algod = algokit_utils.get_algod_client() +indexer = algokit_utils.get_indexer_client() + +"""After""" +from algokit_utils import AlgorandClient +algorand = AlgorandClient.from_environment() # or .testnet(), .mainnet(), etc. +``` + +During migration, you can still access SDK clients if needed: + +```python +algod = algorand.client.algod +indexer = algorand.client.indexer +kmd = algorand.client.kmd +``` + +### Step 2 - Update Account Management + +Account management has moved to `algorand.account`: + +#### Before: + +```python +account = algokit_utils.get_account_from_mnemonic( + mnemonic=os.getenv("MY_ACCOUNT_MNEMONIC"), +) +dispenser = algokit_utils.get_dispenser_account(algod) +``` + +#### After: + +```python +account = algorand.account.from_mnemonic(os.getenv("MY_ACCOUNT_MNEMONIC")) +dispenser = algorand.account.dispenser_from_environment() +``` + +Key changes: + +- `get_account` → `account.from_environment` +- `get_account_from_mnemonic` → `account.from_mnemonic` +- `get_dispenser_account` → `account.dispenser_from_environment` +- `get_localnet_default_account` → `account.localnet_dispenser` + +### Step 3 - Update Transaction Management + +Transaction creation and sending is now more structured: + +#### Before: + +```python +# Single transaction +result = algokit_utils.transfer_algos( + from_account=account, + to_addr="RECEIVER", + amount=algokit_utils.algos(1), + algod_client=algod, +) + +# Transaction groups +atc = AtomicTransactionComposer() +# ... add transactions ... +result = algokit_utils.execute_atc_with_logic_error(atc, algod) +``` + +#### After: + +```python +# Single transaction +result = algorand.send.payment( + sender=account.address, + receiver="RECEIVER", + amount=AlgoAmount.from_algo(1), +) + +# Transaction groups +composer = algorand.new_group() +# ... add transactions ... +result = composer.send() +``` + +Key changes: + +- `transfer_algos` → `algorand.send.payment` +- `transfer_asset` → `algorand.send.asset_transfer` +- `execute_atc_with_logic_error` → `composer.send()` +- Transaction parameters are now more consistently named (e.g., `sender` instead of `from_account`) +- Improved amount handling with dedicated `AlgoAmount` class (e.g., `AlgoAmount.from_algo(1)`) + +### Step 4 - Update `ApplicationSpecification` usage + +`ApplicationSpecification` abstraction is largely identical to v2, however it's been renamed to `Arc32Contract` to better reflect the fact that it's a contract specification for a specific ARC and addition of `Arc56Contract` supporting the latest recommended conventions. Hence the main actionable change is to update your import to `from algokit_utils import Arc32Contract` and rename `ApplicationSpecification` to `Arc32Contract`. + +You can instantiate an `Arc56Contract` instance from an `Arc32Contract` instance using the `Arc56Contract.from_arc32` method. For instance: + +```python +testing_app_arc32_app_spec = Arc32Contract.from_json(app_spec_json) +arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) +``` + +> Despite auto conversion of ARC-32 to ARC-56, we recommend recompiling your contract to a fully compliant ARC-56 specification given that auto conversion would skip populating information that can't be parsed from raw ARC-32. + +### Step 5 - Update `ApplicationClient` usage + +The application client has been in v2 has been responsible for instantiation, deployment and calling of the application. In v3, this has been split into `AppClient`, `AppDeployer` and `AppFactory` to better reflect the different responsibilities: + +```python +"""Before (v2 deployment)""" +from algokit_utils import ApplicationClient, OnUpdate, OnSchemaBreak + +# Initialize client with manual configuration +app_client = ApplicationClient( + algod_client=algod, + app_spec=app_spec, + creator=creator, + app_name="MyApp" +) + +# Deployment with versioning and update policies +deploy_result = app_client.deploy( + version="1.0", + allow_update=True, + allow_delete=False, + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail +) + +# Post-deployment calls +response = app_client.call("initialize", args=["config"]) + + +"""After (v3 factory-based deployment)""" +from algokit_utils import AppFactory, OnUpdate, OnSchemaBreak + +# Factory-based deployment with compiled parameters +app_factory = AppFactory( + AppFactoryParams( + algorand=algorand, + app_spec=app_spec, + app_name="MyApp", + compilation_params=AppClientCompilationParams( + deploy_time_params={"VERSION": 1}, + updatable=True, # Replaces allow_update + deletable=False # Replaces allow_delete + ) + ) +) + +app_client, deploy_result = app_factory.deploy( + version="1.0", + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.Fail, +) # Returns a tuple of (app_client, deploy_result) + +# Type-safe post-deployment calls +response = app_client.send.call("setup", args=[{"max_users": 100}]) +``` + +Notable changes: + +- Split between `AppClient`, `AppDeployer` (for raw creation/deployment) and `AppFactory` (for creation/deployment using factory patterns). In majority of cases, you will only need `AppFactory` as it provides convenience methods for instantiation of `AppClient` and mediates calls to `AppDeployer`. +- More structured transaction building with `.params`, `.create_transaction`, and `.send` +- Consistent parameter naming (`args` instead of `method_args`, `box_references` instead of `boxes`) +- ARC-56 support for state management +- Improved error handling and debugging support + +### Step 6 - Update `AppClient` State Management + +State management is now more structured and type-safe: + +```python +"""Before""" +global_state = app_client.get_global_state() +local_state = app_client.get_local_state(account_address) +box_value = app_client.get_box_value("box_name") + +"""After""" +# Global state +global_state = app_client.state.global_state.get_all() +value = app_client.state.global_state.get_value("key_name") +map_value = app_client.state.global_state.get_map_value("map_name", "key") + +# Local state +local_state = app_client.state.local_state(account_address).get_all() +value = app_client.state.local_state(account_address).get_value("key_name") +map_value = app_client.state.local_state(account_address).get_map_value("map_name", "key") + +# Box storage +box_value = app_client.state.box.get_value("box_name") +boxes = app_client.state.box.get_all() +map_value = app_client.state.box.get_map_value("map_name", "key") +``` + +### Step 7 - Update Asset Management + +Asset management is now more consistent: + +```python +"""Before""" +result = algokit_utils.opt_in(algod, account, [asset_id]) + +"""After""" +result = algorand.send.asset_opt_in( + params=AssetOptInParams( + sender=account.address, + asset_id=asset_id, + ) +) +``` + +## Breaking Changes + +1. **Client Management** + + - Removal of standalone client creation functions + - All clients now accessed through `AlgorandClient` + +2. **Account Management** + + - Account creation functions moved to `AccountManager` accessible via `algorand.account` property + - Unified `TransactionSignerAccountProtocol` with compliant and typed `SigningAccount`, `TransactionSignerAccount`, `LogicSigAccount`, `MultiSigAccount` classes encapsulating low level `algosdk` abstractions. + - Improved typing for account operations, such as obtaining account information from `algod`, returning a typed information object. + +3. **Transaction Management** + + - Consistent and intuitive transaction creation and sending interface accessible via `algorand.{send|params|create_transaction}` properties + - New transaction composition interface accessible via `algorand.new_group` + - Removing necessity to interact with low level and untyped `algosdk` abstractions for assembling, signing and sending transaction(s). + +4. **Application Client** + + - Split into `AppClient`, `AppDeployer` and `AppFactory` + - New intuitive structured interface for creating or sending `AppCall`|`AppMethodCall` transactions + - ARC-56 support along with automatic conversion of specs from ARC-32 to ARC-56 + +5. **State Management** + + - New hierarchical state access available via `app_client.state.{global_state|local_state|box}` properties + - Improved typing for state values + - Support for ARC-56 state schemas + +6. **Asset Management** + - Dedicated `AssetManager` class for asset management accessible via `algorand.asset` property + - Improved typing for asset operations, such as obtaining asset information from `algod`, returning a typed information object. + - Consistent interface for asset opt-in, transfer, freeze, etc. + +## Best Practices + +1. Use the new `AlgorandClient` as the main entry point +2. Leverage IDE autocompletion to discover available functionality, consult with [API reference](https://algokit-utils-py.readthedocs.io/en/latest/api_reference/index.html) when unsure +3. Use the transaction parameter builders for type-safe transaction creation (`algorand.params.{}`) +4. Use the state accessor patterns for cleaner state management {`algorand.state.{}`} +5. Use high level `TransactionComposer` interface over low level `algosdk` abstractions (where possible) +6. Use source maps and debug mode to quickly troubleshoot on-chain errors +7. Use idempotent deployment patterns with versioning + +## Troubleshooting + +### A v2 interface/method/class does not display a deprecation warning correctly or at all + +Submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a description of the problem and the code that is causing it. + +### Useful scenario of converting v2 to v3 not covered in generic migration guide + +If you have a scenario that you think is useful and not covered in the generic migration guide, please submit an issue to [algokit-utils-py](https://github.com/algorandfoundation/algokit-utils-py/issues) with a scenario. diff --git a/docs/.nojekyll b/legacy_v2_tests/__init__.py similarity index 100% rename from docs/.nojekyll rename to legacy_v2_tests/__init__.py diff --git a/legacy_v2_tests/app_client_test.json b/legacy_v2_tests/app_client_test.json new file mode 100644 index 00000000..a5bd4359 --- /dev/null +++ b/legacy_v2_tests/app_client_test.json @@ -0,0 +1 @@ +{"hints": {"version()uint64": {"call_config": {"no_op": "CALL"}}, "readonly(uint64)void": {"read_only": true, "call_config": {"no_op": "CALL"}}, "set_box(byte[4],string)void": {"call_config": {"no_op": "CALL"}}, "get_box(byte[4])string": {"call_config": {"no_op": "CALL"}}, "get_box_readonly(byte[4])string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "update()void": {"call_config": {"update_application": "CALL"}}, "update_args(string)void": {"call_config": {"update_application": "CALL"}}, "delete()void": {"call_config": {"delete_application": "CALL"}}, "delete_args(string)void": {"call_config": {"delete_application": "CALL"}}, "create_opt_in()void": {"call_config": {"opt_in": "CREATE"}}, "update_greeting(string)void": {"call_config": {"no_op": "CALL"}}, "create()void": {"call_config": {"no_op": "CREATE"}}, "create_args(string)void": {"call_config": {"no_op": "CREATE"}}, "hello(string)string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "hello_remember(string)string": {"call_config": {"no_op": "CALL"}}, "get_last()string": {"read_only": true, "call_config": {"no_op": "CALL"}}, "opt_in()void": {"call_config": {"opt_in": "CALL"}}, "opt_in_args(string)void": {"call_config": {"opt_in": "CALL"}}, "close_out()void": {"call_config": {"close_out": "CALL"}}, "close_out_args(string)void": {"call_config": {"close_out": "CALL"}}, "call_with_payment(pay)string": {"call_config": {"no_op": "CALL"}}}, "source": {"approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweCAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweDZjNjE3Mzc0IDB4NTk2NTczIDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sNDQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxOWQ2YjE4NiAvLyAidmVyc2lvbigpdWludDY0Igo9PQpibnogbWFpbl9sNDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1M2JkNjE4NiAvLyAicmVhZG9ubHkodWludDY0KXZvaWQiCj09CmJueiBtYWluX2w0Mgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGE0YjRhMjMwIC8vICJzZXRfYm94KGJ5dGVbNF0sc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2w0MQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDdmNWRlMjhmIC8vICJnZXRfYm94KGJ5dGVbNF0pc3RyaW5nIgo9PQpibnogbWFpbl9sNDAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxM2QxMmI1MCAvLyAiZ2V0X2JveF9yZWFkb25seShieXRlWzRdKXN0cmluZyIKPT0KYm56IG1haW5fbDM5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTBlODE4NzIgLy8gInVwZGF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDM4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4N2QwODUxOGIgLy8gInVwZGF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzcKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg1ODYxYmI1MCAvLyAiZGVsZXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDhiZGY5ZWIwIC8vICJjcmVhdGVfb3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMzQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMDU1ZjAwNiAvLyAidXBkYXRlX2dyZWV0aW5nKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMzMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg0YzVjNjFiYSAvLyAiY3JlYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkMTQ1NGM3OCAvLyAiY3JlYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAyYmVjZTExIC8vICJoZWxsbyhzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMzAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhiYzFjMWRkNCAvLyAiaGVsbG9fcmVtZW1iZXIoc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDI5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YTlhZTc2MjcgLy8gImdldF9sYXN0KClzdHJpbmciCj09CmJueiBtYWluX2wyOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDMwYzZkNThhIC8vICJvcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDIyYzdkZWRhIC8vICJvcHRfaW5fYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MTY1OGFhMmYgLy8gImNsb3NlX291dCgpdm9pZCIKPT0KYm56IG1haW5fbDI1CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZGU4NGQ5YWQgLy8gImNsb3NlX291dF9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4ODk2M2M5OSAvLyAiY2FsbF93aXRoX3BheW1lbnQocGF5KXN0cmluZyIKPT0KYm56IG1haW5fbDIzCmVycgptYWluX2wyMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjYWxsd2l0aHBheW1lbnRjYXN0ZXJfNDYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGFyZ3NjYXN0ZXJfNDUKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGNhc3Rlcl80NAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluYXJnc2Nhc3Rlcl80MwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluY2FzdGVyXzQyCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0Y2FzdGVyXzQxCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyOToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb3JlbWVtYmVyY2FzdGVyXzQwCmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBoZWxsb2Nhc3Rlcl8zOQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYXJnc2Nhc3Rlcl8zOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlY2FzdGVyXzM3CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVncmVldGluZ2Nhc3Rlcl8zNgppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluY2FzdGVyXzM1CmludGNfMSAvLyAxCnJldHVybgptYWluX2wzNToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlYXJnc2Nhc3Rlcl8zNAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZWNhc3Rlcl8zMwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMzc6CnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHVwZGF0ZWFyZ3NjYXN0ZXJfMzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM4Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVjYXN0ZXJfMzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDM5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGdldGJveHJlYWRvbmx5Y2FzdGVyXzMwCmludGNfMSAvLyAxCnJldHVybgptYWluX2w0MDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRib3hjYXN0ZXJfMjkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQxOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHNldGJveGNhc3Rlcl8yOAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgcmVhZG9ubHljYXN0ZXJfMjcKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIHZlcnNpb25jYXN0ZXJfMjYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDQ0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CmJueiBtYWluX2w1NAp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQpibnogbWFpbl9sNTMKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KYm56IG1haW5fbDUyCnR4biBPbkNvbXBsZXRpb24KcHVzaGludCA0IC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2w1MQp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sNTAKZXJyCm1haW5fbDUwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUxOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUyOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBjbG9zZW91dGJhcmVfMjMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDUzOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBvcHRpbmJhcmVfMjAKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDU0Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzEzCmludGNfMSAvLyAxCnJldHVybgoKLy8gdmVyc2lvbgp2ZXJzaW9uXzA6CnByb3RvIDAgMQppbnRjXzAgLy8gMApwdXNoaW50IFRNUExfVkVSU0lPTiAvLyBUTVBMX1ZFUlNJT04KZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gcmVhZG9ubHkKcmVhZG9ubHlfMToKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpibnogcmVhZG9ubHlfMV9sMgppbnRjXzEgLy8gMQpyZXR1cm4KcmVhZG9ubHlfMV9sMjoKaW50Y18wIC8vIDAKLy8gQW4gZXJyb3IKYXNzZXJ0CnJldHN1YgoKLy8gc2V0X2JveApzZXRib3hfMjoKcHJvdG8gMiAwCmZyYW1lX2RpZyAtMgpib3hfZGVsCnBvcApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJveF9wdXQKcmV0c3ViCgovLyBnZXRfYm94CmdldGJveF8zOgpwcm90byAxIDEKYnl0ZWNfMCAvLyAiIgpmcmFtZV9kaWcgLTEKYm94X2dldApzdG9yZSAxCnN0b3JlIDAKbG9hZCAxCmFzc2VydApsb2FkIDAKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfYm94X3JlYWRvbmx5CmdldGJveHJlYWRvbmx5XzQ6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCmZyYW1lX2RpZyAtMQpib3hfZ2V0CnN0b3JlIDMKc3RvcmUgMgpsb2FkIDMKYXNzZXJ0CmxvYWQgMgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIHVwZGF0ZQp1cGRhdGVfNToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfNjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfNzoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfODoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzk6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc18xMDoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzExOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18xMjoKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzEzOgpwcm90byAwIDAKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfMTQ6CnByb3RvIDAgMApieXRlY18xIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDE0MjQ5IC8vICJIZWxsbyBBQkkiCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc18xNToKcHJvdG8gMSAwCmJ5dGVjXzEgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBoZWxsbwpoZWxsb18xNjoKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyCmhlbGxvcmVtZW1iZXJfMTc6CnByb3RvIDEgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9sb2NhbF9wdXQKYnl0ZWNfMSAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CmJ5dGVjIDUgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIGdldF9sYXN0CmdldGxhc3RfMTg6CnByb3RvIDAgMQpieXRlY18wIC8vICIiCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKYXBwX2xvY2FsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWIKCi8vIG9wdF9pbgpvcHRpbl8xOToKcHJvdG8gMCAwCnR4biBTZW5kZXIKYnl0ZWNfMyAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTQyNDkgLy8gIk9wdCBJbiBBQkkiCmFwcF9sb2NhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW5fYmFyZQpvcHRpbmJhcmVfMjA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzMgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDI2MTcyNjUgLy8gIk9wdCBJbiBCYXJlIgphcHBfbG9jYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gb3B0X2luX2FyZ3MKb3B0aW5hcmdzXzIxOgpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjIDQgLy8gIlllcyIKPT0KLy8gcGFzc2VzIG9wdF9pbiBjaGVjawphc3NlcnQKdHhuIFNlbmRlcgpieXRlY18zIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQxNzI2NzczIC8vICJPcHQgSW4gQXJncyIKYXBwX2xvY2FsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF8yMjoKcHJvdG8gMCAwCmludGNfMSAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2JhcmUKY2xvc2VvdXRiYXJlXzIzOgpwcm90byAwIDAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXRfYXJncwpjbG9zZW91dGFyZ3NfMjQ6CnByb3RvIDEgMApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWMgNCAvLyAiWWVzIgo9PQovLyBwYXNzZXMgY2xvc2Vfb3V0IGNoZWNrCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNhbGxfd2l0aF9wYXltZW50CmNhbGx3aXRocGF5bWVudF8yNToKcHJvdG8gMSAxCmJ5dGVjXzAgLy8gIiIKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudAppbnRjXzAgLy8gMAo+CmFzc2VydApwdXNoYnl0ZXMgMHgwMDEyNTA2MTc5NmQ2NTZlNzQyMDUzNzU2MzYzNjU3MzczNjY3NTZjIC8vIDB4MDAxMjUwNjE3OTZkNjU2ZTc0MjA1Mzc1NjM2MzY1NzM3MzY2NzU2YwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2ZXJzaW9uX2Nhc3Rlcgp2ZXJzaW9uY2FzdGVyXzI2Ogpwcm90byAwIDAKaW50Y18wIC8vIDAKY2FsbHN1YiB2ZXJzaW9uXzAKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMAppdG9iCmNvbmNhdApsb2cKcmV0c3ViCgovLyByZWFkb25seV9jYXN0ZXIKcmVhZG9ubHljYXN0ZXJfMjc6CnByb3RvIDAgMAppbnRjXzAgLy8gMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmNhbGxzdWIgcmVhZG9ubHlfMQpyZXRzdWIKCi8vIHNldF9ib3hfY2FzdGVyCnNldGJveGNhc3Rlcl8yODoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKZHVwCnR4bmEgQXBwbGljYXRpb25BcmdzIDEKZnJhbWVfYnVyeSAwCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAwCmZyYW1lX2RpZyAxCmNhbGxzdWIgc2V0Ym94XzIKcmV0c3ViCgovLyBnZXRfYm94X2Nhc3RlcgpnZXRib3hjYXN0ZXJfMjk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGdldGJveF8zCmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9ib3hfcmVhZG9ubHlfY2FzdGVyCmdldGJveHJlYWRvbmx5Y2FzdGVyXzMwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBnZXRib3hyZWFkb25seV80CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIHVwZGF0ZV9jYXN0ZXIKdXBkYXRlY2FzdGVyXzMxOgpwcm90byAwIDAKY2FsbHN1YiB1cGRhdGVfNQpyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzX2Nhc3Rlcgp1cGRhdGVhcmdzY2FzdGVyXzMyOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWFyZ3NfNwpyZXRzdWIKCi8vIGRlbGV0ZV9jYXN0ZXIKZGVsZXRlY2FzdGVyXzMzOgpwcm90byAwIDAKY2FsbHN1YiBkZWxldGVfOApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzX2Nhc3RlcgpkZWxldGVhcmdzY2FzdGVyXzM0Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGRlbGV0ZWFyZ3NfMTAKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luX2Nhc3RlcgpjcmVhdGVvcHRpbmNhc3Rlcl8zNToKcHJvdG8gMCAwCmNhbGxzdWIgY3JlYXRlb3B0aW5fMTEKcmV0c3ViCgovLyB1cGRhdGVfZ3JlZXRpbmdfY2FzdGVyCnVwZGF0ZWdyZWV0aW5nY2FzdGVyXzM2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzEyCnJldHN1YgoKLy8gY3JlYXRlX2Nhc3RlcgpjcmVhdGVjYXN0ZXJfMzc6CnByb3RvIDAgMApjYWxsc3ViIGNyZWF0ZV8xNApyZXRzdWIKCi8vIGNyZWF0ZV9hcmdzX2Nhc3RlcgpjcmVhdGVhcmdzY2FzdGVyXzM4Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIGNyZWF0ZWFyZ3NfMTUKcmV0c3ViCgovLyBoZWxsb19jYXN0ZXIKaGVsbG9jYXN0ZXJfMzk6CnByb3RvIDAgMApieXRlY18wIC8vICIiCmR1cAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpjYWxsc3ViIGhlbGxvXzE2CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGhlbGxvX3JlbWVtYmVyX2Nhc3RlcgpoZWxsb3JlbWVtYmVyY2FzdGVyXzQwOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpkdXAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDEKZnJhbWVfZGlnIDEKY2FsbHN1YiBoZWxsb3JlbWVtYmVyXzE3CmZyYW1lX2J1cnkgMApieXRlY18yIC8vIDB4MTUxZjdjNzUKZnJhbWVfZGlnIDAKY29uY2F0CmxvZwpyZXRzdWIKCi8vIGdldF9sYXN0X2Nhc3RlcgpnZXRsYXN0Y2FzdGVyXzQxOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgpjYWxsc3ViIGdldGxhc3RfMTgKZnJhbWVfYnVyeSAwCmJ5dGVjXzIgLy8gMHgxNTFmN2M3NQpmcmFtZV9kaWcgMApjb25jYXQKbG9nCnJldHN1YgoKLy8gb3B0X2luX2Nhc3RlcgpvcHRpbmNhc3Rlcl80MjoKcHJvdG8gMCAwCmNhbGxzdWIgb3B0aW5fMTkKcmV0c3ViCgovLyBvcHRfaW5fYXJnc19jYXN0ZXIKb3B0aW5hcmdzY2FzdGVyXzQzOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApjYWxsc3ViIG9wdGluYXJnc18yMQpyZXRzdWIKCi8vIGNsb3NlX291dF9jYXN0ZXIKY2xvc2VvdXRjYXN0ZXJfNDQ6CnByb3RvIDAgMApjYWxsc3ViIGNsb3Nlb3V0XzIyCnJldHN1YgoKLy8gY2xvc2Vfb3V0X2FyZ3NfY2FzdGVyCmNsb3Nlb3V0YXJnc2Nhc3Rlcl80NToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gIiIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKY2FsbHN1YiBjbG9zZW91dGFyZ3NfMjQKcmV0c3ViCgovLyBjYWxsX3dpdGhfcGF5bWVudF9jYXN0ZXIKY2FsbHdpdGhwYXltZW50Y2FzdGVyXzQ2Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiIgppbnRjXzAgLy8gMAp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApmcmFtZV9kaWcgMQpjYWxsc3ViIGNhbGx3aXRocGF5bWVudF8yNQpmcmFtZV9idXJ5IDAKYnl0ZWNfMiAvLyAweDE1MWY3Yzc1CmZyYW1lX2RpZyAwCmNvbmNhdApsb2cKcmV0c3Vi", "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4="}, "state": {"global": {"num_byte_slices": 1, "num_uints": 0}, "local": {"num_byte_slices": 1, "num_uints": 0}}, "schema": {"global": {"declared": {"greeting": {"type": "bytes", "key": "greeting", "descr": ""}}, "reserved": {}}, "local": {"declared": {"last": {"type": "bytes", "key": "last", "descr": ""}}, "reserved": {}}}, "contract": {"name": "HelloWorldApp", "methods": [{"name": "version", "args": [], "returns": {"type": "uint64"}}, {"name": "readonly", "args": [{"type": "uint64", "name": "error"}], "returns": {"type": "void"}}, {"name": "set_box", "args": [{"type": "byte[4]", "name": "name"}, {"type": "string", "name": "value"}], "returns": {"type": "void"}}, {"name": "get_box", "args": [{"type": "byte[4]", "name": "name"}], "returns": {"type": "string"}}, {"name": "get_box_readonly", "args": [{"type": "byte[4]", "name": "name"}], "returns": {"type": "string"}}, {"name": "update", "args": [], "returns": {"type": "void"}}, {"name": "update_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "delete", "args": [], "returns": {"type": "void"}}, {"name": "delete_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "create_opt_in", "args": [], "returns": {"type": "void"}}, {"name": "update_greeting", "args": [{"type": "string", "name": "greeting"}], "returns": {"type": "void"}}, {"name": "create", "args": [], "returns": {"type": "void"}}, {"name": "create_args", "args": [{"type": "string", "name": "greeting"}], "returns": {"type": "void"}}, {"name": "hello", "args": [{"type": "string", "name": "name"}], "returns": {"type": "string"}}, {"name": "hello_remember", "args": [{"type": "string", "name": "name"}], "returns": {"type": "string"}}, {"name": "get_last", "args": [], "returns": {"type": "string"}}, {"name": "opt_in", "args": [], "returns": {"type": "void"}}, {"name": "opt_in_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "close_out", "args": [], "returns": {"type": "void"}}, {"name": "close_out_args", "args": [{"type": "string", "name": "check"}], "returns": {"type": "void"}}, {"name": "call_with_payment", "args": [{"type": "pay", "name": "payment"}], "returns": {"type": "string"}}], "networks": {}}, "bare_call_config": {"close_out": "CALL", "delete_application": "CALL", "no_op": "CREATE", "opt_in": "CALL", "update_application": "CALL"}} \ No newline at end of file diff --git a/tests/app_client_test.py b/legacy_v2_tests/app_client_test.py similarity index 100% rename from tests/app_client_test.py rename to legacy_v2_tests/app_client_test.py diff --git a/tests/app_multi_underscore_template_var.py b/legacy_v2_tests/app_multi_underscore_template_var.py similarity index 100% rename from tests/app_multi_underscore_template_var.py rename to legacy_v2_tests/app_multi_underscore_template_var.py diff --git a/tests/app_resolve.json b/legacy_v2_tests/app_resolve.json similarity index 100% rename from tests/app_resolve.json rename to legacy_v2_tests/app_resolve.json diff --git a/tests/app_v1.json b/legacy_v2_tests/app_v1.json similarity index 100% rename from tests/app_v1.json rename to legacy_v2_tests/app_v1.json diff --git a/tests/app_v2.json b/legacy_v2_tests/app_v2.json similarity index 100% rename from tests/app_v2.json rename to legacy_v2_tests/app_v2.json diff --git a/tests/app_v3.json b/legacy_v2_tests/app_v3.json similarity index 100% rename from tests/app_v3.json rename to legacy_v2_tests/app_v3.json diff --git a/legacy_v2_tests/conftest.py b/legacy_v2_tests/conftest.py new file mode 100644 index 00000000..f8989eb8 --- /dev/null +++ b/legacy_v2_tests/conftest.py @@ -0,0 +1,211 @@ +import inspect +import math +import random +import subprocess +from pathlib import Path +from typing import TYPE_CHECKING +from uuid import uuid4 + +import algosdk.transaction +import pytest +from dotenv import load_dotenv + +from algokit_utils import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + Account, + ApplicationClient, + ApplicationSpecification, + EnsureBalanceParameters, + ensure_funded, + get_account, + get_algod_client, + get_indexer_client, + get_kmd_client_from_algod_client, + replace_template_variables, +) +from legacy_v2_tests import app_client_test + +if TYPE_CHECKING: + from algosdk.kmd import KMDClient + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + +@pytest.fixture(autouse=True, scope="session") +def _environment_fixture() -> None: + env_path = Path(__file__).parent / ".." / "example.env" + load_dotenv(env_path) + + +def check_output_stability(logs: str, *, test_name: str | None = None) -> None: + """Test that the contract output hasn't changed for an Application, using git diff""" + caller_frame = inspect.stack()[1] + caller_path = Path(caller_frame.filename).resolve() + caller_dir = caller_path.parent + test_name = test_name or caller_frame.function + caller_stem = Path(caller_frame.filename).stem + output_dir = caller_dir / f"{caller_stem}.approvals" + output_dir.mkdir(exist_ok=True) + output_file = output_dir / f"{test_name}.approved.txt" + output_file_str = str(output_file) + output_file_did_exist = output_file.exists() + output_file.write_text(logs, encoding="utf-8") + + git_diff = subprocess.run( + [ + "git", + "diff", + "--exit-code", + "--no-ext-diff", + "--no-color", + output_file_str, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + # first fail if there are any changes to already committed files, you must manually add them in that case + assert git_diff.returncode == 0, git_diff.stdout + + # if first time running, fail in case of accidental change to output directory + if not output_file_did_exist: + pytest.fail( + f"New output folder created at {output_file_str} from test {test_name} - " + "if this was intentional, please commit the files to the git repo" + ) + + +def read_spec( + file_name: str, + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> ApplicationSpecification: + path = Path(__file__).parent / file_name + spec = ApplicationSpecification.from_json(Path(path).read_text(encoding="utf-8")) + + template_variables = template_values or {} + if updatable is not None: + template_variables["UPDATABLE"] = int(updatable) + + if deletable is not None: + template_variables["DELETABLE"] = int(deletable) + + spec.approval_program = ( + replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) + return spec + + +def get_specs( + updatable: bool | None = None, + deletable: bool | None = None, +) -> tuple[ApplicationSpecification, ApplicationSpecification, ApplicationSpecification]: + return ( + read_spec("app_v1.json", updatable=updatable, deletable=deletable), + read_spec("app_v2.json", updatable=updatable, deletable=deletable), + read_spec("app_v3.json", updatable=updatable, deletable=deletable), + ) + + +def get_unique_name() -> str: + name = str(uuid4()).replace("-", "") + assert name.isalnum() + return name + + +def is_opted_in(client_fixture: ApplicationClient) -> bool: + _, sender = client_fixture.resolve_signer_sender() + account_info = client_fixture.algod_client.account_info(sender) + assert isinstance(account_info, dict) + apps_local_state = account_info["apps-local-state"] + return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) + + +@pytest.fixture(scope="session") +def algod_client() -> "AlgodClient": + return get_algod_client() + + +@pytest.fixture(scope="session") +def kmd_client(algod_client: "AlgodClient") -> "KMDClient": + return get_kmd_client_from_algod_client(algod_client) + + +@pytest.fixture(scope="session") +def indexer_client() -> "IndexerClient": + return get_indexer_client() + + +@pytest.fixture +def creator(algod_client: "AlgodClient") -> Account: + creator_name = get_unique_name() + return get_account(algod_client, creator_name) + + +@pytest.fixture(scope="session") +def funded_account(algod_client: "AlgodClient") -> Account: + creator_name = get_unique_name() + return get_account(algod_client, creator_name) + + +@pytest.fixture(scope="session") +def app_spec() -> ApplicationSpecification: + app_spec = app_client_test.app.build() + path = Path(__file__).parent / "app_client_test.json" + path.write_text(app_spec.to_json()) + return read_spec("app_client_test.json", deletable=True, updatable=True, template_values={"VERSION": 1}) + + +def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int | None) -> int: + if total is None: + total = math.floor(random.random() * 100) + 20 + + decimals = 0 + asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}" + + params = algod_client.suggested_params() + + txn = algosdk.transaction.AssetConfigTxn( + sender=sender.address, + sp=params, + total=total * 10**decimals, + decimals=decimals, + default_frozen=False, + unit_name="", + asset_name=asset_name, + manager=sender.address, + reserve=sender.address, + freeze=sender.address, + clawback=sender.address, + url="https://path/to/my/asset/details", + metadata_hash=None, + note=None, + lease=None, + rekey_to=None, + ) + + 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") + + +def assure_funds(algod_client: "AlgodClient", account: Account) -> None: + ensure_funded( + algod_client, + EnsureBalanceParameters( + account_to_fund=account, + min_spending_balance_micro_algos=300000, + min_funding_increment_micro_algos=1, + ), + ) diff --git a/tests/test_account.py b/legacy_v2_tests/test_account.py similarity index 88% rename from tests/test_account.py rename to legacy_v2_tests/test_account.py index 1536bd68..e1ee2228 100644 --- a/tests/test_account.py +++ b/legacy_v2_tests/test_account.py @@ -1,8 +1,7 @@ from typing import TYPE_CHECKING from algokit_utils import get_account - -from tests.conftest import get_unique_name +from legacy_v2_tests.conftest import get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app.py b/legacy_v2_tests/test_app.py similarity index 99% rename from tests/test_app.py rename to legacy_v2_tests/test_app.py index 07e258a1..1b79f708 100644 --- a/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/tests/test_app_client.py b/legacy_v2_tests/test_app_client.py similarity index 99% rename from tests/test_app_client.py rename to legacy_v2_tests/test_app_client.py index 87826175..b6565148 100644 --- a/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/tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_imported_source_map.approved.txt diff --git a/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 new file mode 100644 index 00000000..fb8b9ea5 --- /dev/null +++ b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt @@ -0,0 +1,7 @@ +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 diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_source_map.approved.txt diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt b/legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt similarity index 100% rename from tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt rename to legacy_v2_tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_provided_template_values.approved.txt diff --git a/tests/test_app_client_call.py b/legacy_v2_tests/test_app_client_call.py similarity index 90% rename from tests/test_app_client_call.py rename to legacy_v2_tests/test_app_client_call.py index 6d72f037..14933f1b 100644 --- a/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,7 +12,17 @@ ) from algosdk.transaction import ApplicationCallTxn, PaymentTxn -from tests.conftest import check_output_stability, get_unique_name +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: from algosdk.abi import Method @@ -40,7 +43,7 @@ def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecificati # If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. @pytest.fixture(autouse=True) def mock_config() -> Generator[Mock, None, None]: - with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + with patch("algokit_utils._legacy_v2.application_client.config", new_callable=Mock) as mock_config: mock_config.debug = True mock_config.project_root = None yield mock_config @@ -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/tests/test_app_client_clear_state.py b/legacy_v2_tests/test_app_client_clear_state.py similarity index 96% rename from tests/test_app_client_clear_state.py rename to legacy_v2_tests/test_app_client_clear_state.py index c8de6eba..1d2f6529 100644 --- a/tests/test_app_client_clear_state.py +++ b/legacy_v2_tests/test_app_client_clear_state.py @@ -2,20 +2,20 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, ) - -from tests.conftest import is_opted_in +from legacy_v2_tests.conftest import is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt b/legacy_v2_tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt diff --git a/tests/test_app_client_close_out.py b/legacy_v2_tests/test_app_client_close_out.py similarity index 95% rename from tests/test_app_client_close_out.py rename to legacy_v2_tests/test_app_client_close_out.py index b5ba1cd3..81ac5ea9 100644 --- a/tests/test_app_client_close_out.py +++ b/legacy_v2_tests/test_app_client_close_out.py @@ -1,21 +1,21 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - -from tests.conftest import check_output_stability, is_opted_in +from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt b/legacy_v2_tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt similarity index 100% rename from tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt rename to legacy_v2_tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt diff --git a/tests/test_app_client_create.py b/legacy_v2_tests/test_app_client_create.py similarity index 99% rename from tests/test_app_client_create.py rename to legacy_v2_tests/test_app_client_create.py index be29a5e4..00fd9691 100644 --- a/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,10 +13,7 @@ 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 tests.conftest import check_output_stability, get_unique_name +from legacy_v2_tests.conftest import check_output_stability, get_unique_name if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt b/legacy_v2_tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt diff --git a/tests/test_app_client_delete.py b/legacy_v2_tests/test_app_client_delete.py similarity index 94% rename from tests/test_app_client_delete.py rename to legacy_v2_tests/test_app_client_delete.py index 6fc3ec5a..d5df42cb 100644 --- a/tests/test_app_client_delete.py +++ b/legacy_v2_tests/test_app_client_delete.py @@ -1,21 +1,21 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - -from tests.conftest import check_output_stability +from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/tests/test_app_client_deploy.py b/legacy_v2_tests/test_app_client_deploy.py similarity index 95% rename from tests/test_app_client_deploy.py rename to legacy_v2_tests/test_app_client_deploy.py index d1c8eba5..e51392b4 100644 --- a/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,15 +10,14 @@ TransferParameters, transfer, ) - -from tests.conftest import get_unique_name, read_spec +from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt b/legacy_v2_tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt diff --git a/tests/test_app_client_opt_in.py b/legacy_v2_tests/test_app_client_opt_in.py similarity index 95% rename from tests/test_app_client_opt_in.py rename to legacy_v2_tests/test_app_client_opt_in.py index 9244a826..afc1fb1e 100644 --- a/tests/test_app_client_opt_in.py +++ b/legacy_v2_tests/test_app_client_opt_in.py @@ -1,21 +1,21 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - -from tests.conftest import check_output_stability, is_opted_in +from legacy_v2_tests.conftest import check_output_stability, is_opted_in if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -@pytest.fixture() +@pytest.fixture def client_fixture( algod_client: "AlgodClient", indexer_client: "IndexerClient", diff --git a/tests/test_app_client_prepare.py b/legacy_v2_tests/test_app_client_prepare.py similarity index 99% rename from tests/test_app_client_prepare.py rename to legacy_v2_tests/test_app_client_prepare.py index 6c6355b0..affacd50 100644 --- a/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/tests/test_app_client_resolve.py b/legacy_v2_tests/test_app_client_resolve.py similarity index 97% rename from tests/test_app_client_resolve.py rename to legacy_v2_tests/test_app_client_resolve.py index 2482149a..d7e8b1d1 100644 --- a/tests/test_app_client_resolve.py +++ b/legacy_v2_tests/test_app_client_resolve.py @@ -5,8 +5,7 @@ ApplicationClient, DefaultArgumentDict, ) - -from tests.conftest import read_spec +from legacy_v2_tests.conftest import read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_app_client_signer_sender.py b/legacy_v2_tests/test_app_client_signer_sender.py similarity index 97% rename from tests/test_app_client_signer_sender.py rename to legacy_v2_tests/test_app_client_signer_sender.py index d6c383cb..cfdef0ac 100644 --- a/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/tests/test_app_client_template_values.py b/legacy_v2_tests/test_app_client_template_values.py similarity index 97% rename from tests/test_app_client_template_values.py rename to legacy_v2_tests/test_app_client_template_values.py index 0bf5ab70..a01f53d9 100644 --- a/tests/test_app_client_template_values.py +++ b/legacy_v2_tests/test_app_client_template_values.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING -import algokit_utils import pytest -from tests.conftest import get_unique_name, read_spec +import algokit_utils +from legacy_v2_tests.conftest import get_unique_name, read_spec if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -153,7 +153,7 @@ def test_deploy_with_multi_underscore_template_value( indexer_client: "IndexerClient", funded_account: algokit_utils.Account, ) -> None: - from tests.app_multi_underscore_template_var import app + from legacy_v2_tests.app_multi_underscore_template_var import app some_value = 123 app_spec = app.build(algod_client) diff --git a/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt b/legacy_v2_tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt similarity index 100% rename from tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt rename to legacy_v2_tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt diff --git a/tests/test_app_client_update.py b/legacy_v2_tests/test_app_client_update.py similarity index 96% rename from tests/test_app_client_update.py rename to legacy_v2_tests/test_app_client_update.py index 24dcf366..4dc082e0 100644 --- a/tests/test_app_client_update.py +++ b/legacy_v2_tests/test_app_client_update.py @@ -1,14 +1,14 @@ from typing import TYPE_CHECKING import pytest + from algokit_utils import ( Account, ApplicationClient, ApplicationSpecification, LogicError, ) - -from tests.conftest import check_output_stability +from legacy_v2_tests.conftest import check_output_stability if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient diff --git a/tests/test_asset.py b/legacy_v2_tests/test_asset.py similarity index 98% rename from tests/test_asset.py rename to legacy_v2_tests/test_asset.py index d5612d26..c26906ff 100644 --- a/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, @@ -16,10 +17,10 @@ from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient -from tests.conftest import assure_funds, generate_test_asset, get_unique_name +from legacy_v2_tests.conftest import assure_funds, generate_test_asset, get_unique_name -@pytest.fixture() +@pytest.fixture def to_account(kmd_client: "KMDClient") -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name()) diff --git a/tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps.approved.txt similarity index 100% rename from tests/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps.approved.txt diff --git a/tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt b/legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps_without_sources.approved.txt similarity index 100% rename from tests/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt rename to legacy_v2_tests/test_debug_utils.approvals/test_legacy_build_teal_sourcemaps_without_sources.approved.txt diff --git a/legacy_v2_tests/test_debug_utils.py b/legacy_v2_tests/test_debug_utils.py new file mode 100644 index 00000000..13b4f518 --- /dev/null +++ b/legacy_v2_tests/test_debug_utils.py @@ -0,0 +1,159 @@ +import json +from typing import TYPE_CHECKING +from unittest.mock import Mock + +import pytest +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + +from algokit_utils._debugging import ( + PersistSourceMapInput, + persist_sourcemaps, + simulate_and_persist_response, +) +from algokit_utils._legacy_v2.account import get_account +from algokit_utils._legacy_v2.application_client import ApplicationClient +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.models import Account +from algokit_utils.common import Program +from legacy_v2_tests.conftest import get_unique_name + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + + +@pytest.fixture +def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, signer=creator) + create_response = client.create("create") + assert create_response.tx_id + return client + + +def test_legacy_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory) -> None: + cwd = tmp_path_factory.mktemp("cwd") + + approval = """ +#pragma version 9 +int 1 +""" + clear = """ +#pragma version 9 +int 1 +""" + sources = [ + PersistSourceMapInput(raw_teal=approval, app_name="cool_app", file_name="approval.teal"), + PersistSourceMapInput(raw_teal=clear, app_name="cool_app", file_name="clear"), + ] + + persist_sourcemaps(sources=sources, project_root=cwd, client=algod_client) + + root_path = cwd / ".algokit" / "sources" + sourcemap_file_path = root_path / "sources.avm.json" + app_output_path = root_path / "cool_app" + + assert not (sourcemap_file_path).exists() + assert (app_output_path / "approval.teal").exists() + assert (app_output_path / "approval.teal.map").exists() + assert (app_output_path / "clear.teal").exists() + assert (app_output_path / "clear.teal.map").exists() + + +def test_legacy_build_teal_sourcemaps_without_sources( + algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory +) -> None: + cwd = tmp_path_factory.mktemp("cwd") + + approval = """ +#pragma version 9 +int 1 +""" + clear = """ +#pragma version 9 +int 1 +""" + compiled_approval = Program(approval, algod_client) + compiled_clear = Program(clear, algod_client) + sources = [ + PersistSourceMapInput(compiled_teal=compiled_approval, app_name="cool_app", file_name="approval.teal"), + PersistSourceMapInput(compiled_teal=compiled_clear, app_name="cool_app", file_name="clear"), + ] + + persist_sourcemaps(sources=sources, project_root=cwd, client=algod_client, with_sources=False) + + root_path = cwd / ".algokit" / "sources" + sourcemap_file_path = root_path / "sources.avm.json" + app_output_path = root_path / "cool_app" + + assert not (sourcemap_file_path).exists() + assert not (app_output_path / "approval.teal").exists() + assert (app_output_path / "approval.teal.map").exists() + assert json.loads((app_output_path / "approval.teal.map").read_text())["sources"] == [] + assert not (app_output_path / "clear.teal").exists() + assert (app_output_path / "clear.teal.map").exists() + assert json.loads((app_output_path / "clear.teal.map").read_text())["sources"] == [] + + +def test_legacy_simulate_and_persist_response_via_app_call( + tmp_path_factory: pytest.TempPathFactory, + client_fixture: ApplicationClient, + mocker: Mock, +) -> None: + mock_config = mocker.patch("algokit_utils._legacy_v2.application_client.config") + mock_config.debug = True + mock_config.trace_all = True + mock_config.trace_buffer_size_mb = 256 + cwd = tmp_path_factory.mktemp("cwd") + mock_config.project_root = cwd + + client_fixture.call("hello", name="test") + + output_path = cwd / "debug_traces" + + content = list(output_path.iterdir()) + assert len(list(output_path.iterdir())) == 1 + trace_file_content = json.loads(content[0].read_text()) + simulated_txn = trace_file_content["txn-groups"][0]["txn-results"][0]["txn-result"]["txn"]["txn"] + assert simulated_txn["type"] == "appl" + assert simulated_txn["apid"] == client_fixture.app_id + + +def test_legacy_simulate_and_persist_response( + tmp_path_factory: pytest.TempPathFactory, client_fixture: ApplicationClient, mocker: Mock, funded_account: Account +) -> None: + mock_config = mocker.patch("algokit_utils._legacy_v2.application_client.config") + mock_config.debug = True + mock_config.trace_all = True + cwd = tmp_path_factory.mktemp("cwd") + mock_config.project_root = cwd + + payment = PaymentTxn( + sender=funded_account.address, + receiver=client_fixture.app_address, + amt=1_000_000, + note=b"Payment", + sp=client_fixture.algod_client.suggested_params(), + ) # type: ignore[no-untyped-call] + txn_with_signer = TransactionWithSigner(payment, AccountTransactionSigner(funded_account.private_key)) + atc = AtomicTransactionComposer() + atc.add_transaction(txn_with_signer) + + simulate_and_persist_response(atc, cwd, client_fixture.algod_client) + + output_path = cwd / "debug_traces" + content = list(output_path.iterdir()) + assert len(list(output_path.iterdir())) == 1 + trace_file_content = json.loads(content[0].read_text()) + simulated_txn = trace_file_content["txn-groups"][0]["txn-results"][0]["txn-result"]["txn"]["txn"] + assert simulated_txn["type"] == "pay" + + trace_file_path = content[0] + while trace_file_path.exists(): + tmp_atc = atc.clone() + simulate_and_persist_response(tmp_atc, cwd, client_fixture.algod_client, buffer_size_mb=0.01) diff --git a/tests/test_deploy.approvals/test_comment_stripping.approved.txt b/legacy_v2_tests/test_deploy.approvals/test_comment_stripping.approved.txt similarity index 100% rename from tests/test_deploy.approvals/test_comment_stripping.approved.txt rename to legacy_v2_tests/test_deploy.approvals/test_comment_stripping.approved.txt diff --git a/tests/test_deploy.approvals/test_template_substitution.approved.txt b/legacy_v2_tests/test_deploy.approvals/test_template_substitution.approved.txt similarity index 100% rename from tests/test_deploy.approvals/test_template_substitution.approved.txt rename to legacy_v2_tests/test_deploy.approvals/test_template_substitution.approved.txt diff --git a/tests/test_deploy.py b/legacy_v2_tests/test_deploy.py similarity index 93% rename from tests/test_deploy.py rename to legacy_v2_tests/test_deploy.py index 6a806f5d..4d2cf8c0 100644 --- a/tests/test_deploy.py +++ b/legacy_v2_tests/test_deploy.py @@ -1,9 +1,8 @@ from algokit_utils import ( replace_template_variables, ) -from algokit_utils.deploy import strip_comments - -from tests.conftest import check_output_stability +from algokit_utils._legacy_v2.deploy import strip_comments +from legacy_v2_tests.conftest import check_output_stability def test_template_substitution() -> None: diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_immutable_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_updatable_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_app_with_no_existing_app_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_templated_app_with_changing_parameters_succeeds.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change_append.approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.Fail-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.No-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.No].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update[OnUpdate.UpdateApp-Updatable.Yes-Deletable.Yes].approved.txt diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt b/legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt rename to legacy_v2_tests/test_deploy_scenarios.approvals/test_deploy_with_update_append.approved.txt diff --git a/tests/test_deploy_scenarios.py b/legacy_v2_tests/test_deploy_scenarios.py similarity index 98% rename from tests/test_deploy_scenarios.py rename to legacy_v2_tests/test_deploy_scenarios.py index d2740876..c230ce37 100644 --- a/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,8 +20,7 @@ get_indexer_client, get_localnet_default_account, ) - -from tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec +from legacy_v2_tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ # If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. @pytest.fixture(autouse=True) def mock_config(tmp_path_factory: pytest.TempPathFactory) -> Generator[Mock, None, None]: - with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + with patch("algokit_utils._legacy_v2.application_client.config", new_callable=Mock) as mock_config: mock_config.debug = True cwd = tmp_path_factory.mktemp("cwd") mock_config.project_root = cwd @@ -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/tests/test_dispenser_api_client.py b/legacy_v2_tests/test_dispenser_api_client.py similarity index 99% rename from tests/test_dispenser_api_client.py rename to legacy_v2_tests/test_dispenser_api_client.py index baa2e1db..ac7fa0f8 100644 --- a/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/tests/test_network_clients.py b/legacy_v2_tests/test_network_clients.py similarity index 100% rename from tests/test_network_clients.py rename to legacy_v2_tests/test_network_clients.py diff --git a/tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt b/legacy_v2_tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt similarity index 100% rename from tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt rename to legacy_v2_tests/test_transfer.approvals/test_transfer_algo_max_fee_fails.approved.txt diff --git a/tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt b/legacy_v2_tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt similarity index 100% rename from tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt rename to legacy_v2_tests/test_transfer.approvals/test_transfer_asset_max_fee_fails.approved.txt diff --git a/tests/test_transfer.py b/legacy_v2_tests/test_transfer.py similarity index 98% rename from tests/test_transfer.py rename to legacy_v2_tests/test_transfer.py index 7e13cdfb..335fcf1a 100644 --- a/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,13 +24,8 @@ ) 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 tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name -from tests.test_network_clients import DEFAULT_TOKEN +from legacy_v2_tests.conftest import assure_funds, check_output_stability, generate_test_asset, get_unique_name +from legacy_v2_tests.test_network_clients import DEFAULT_TOKEN if TYPE_CHECKING: from algosdk.kmd import KMDClient @@ -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 eb3dedd7..88a18c0d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,47 +1,47 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" -version = "0.7.16" +version = "1.0.0" description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [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)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, - {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] [package.dependencies] @@ -91,6 +91,27 @@ algokit-utils = ">=2.0.0,<3.0.0" py-algorand-sdk = ">=2.0.0" pyteal = ">=0.24,<0.25" +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "boolean-py" version = "4.0" @@ -104,13 +125,13 @@ files = [ [[package]] name = "cachecontrol" -version = "0.14.0" +version = "0.14.2" 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.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, + {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, ] [package.dependencies] @@ -119,19 +140,19 @@ 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)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -226,127 +247,114 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -379,73 +387,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.3" +version = "7.6.10" 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.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -456,51 +464,51 @@ 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-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-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]] @@ -536,23 +544,6 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -[[package]] -name = "deprecated" -version = "1.2.14" -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.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - [[package]] name = "distlib" version = "0.3.9" @@ -575,15 +566,26 @@ files = [ {file = "docstring_parser-0.14.1.tar.gz", hash = "sha256:2c77522e31b7c88b1ab457a1f3c9ae38947ad719732260ba77ee8a3deb58622a"}, ] +[[package]] +name = "docstring-parser-fork" +version = "0.0.12" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "docstring_parser_fork-0.0.12-py3-none-any.whl", hash = "sha256:55d7cbbc8b367655efd64372b9a0b33a49bae930a8ddd5cdc4c6112312e28a87"}, + {file = "docstring_parser_fork-0.0.12.tar.gz", hash = "sha256:b44c5e0be64ae80f395385f01497d381bd094a57221fd9ff020987d06857b2a0"}, +] + [[package]] name = "docutils" -version = "0.18.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.9" files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -641,29 +643,46 @@ tests = ["asttokens", "littleutils", "pytest", "rich"] [[package]] name = "filelock" -version = "3.16.1" +version = "3.17.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "furo" +version = "2024.8.6" +description = "A clean customisable Sphinx documentation theme." +optional = false +python-versions = ">=3.8" +files = [ + {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, + {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +pygments = ">=2.7" +sphinx = ">=6.0,<9.0" +sphinx-basic-ng = ">=1.0.0.beta2" + [[package]] name = "gitdb" -version = "4.0.11" +version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] @@ -671,20 +690,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.43" +version = "3.1.44" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, + {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, + {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] @@ -721,57 +740,58 @@ lxml = ["lxml"] [[package]] name = "httpcore" -version = "0.16.3" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] -anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.28.1" description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" +httpcore = "==1.*" +idna = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.6" 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.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, ] [package.extras] @@ -804,13 +824,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.6.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [package.dependencies] @@ -822,7 +842,7 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -922,13 +942,13 @@ trio = ["async_generator", "trio"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -939,17 +959,17 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.4.1" +version = "25.6.0" description = "Store and access your passwords safely." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "keyring-25.4.1-py3-none-any.whl", hash = "sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf"}, - {file = "keyring-25.4.1.tar.gz", hash = "sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b"}, + {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, + {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" "jaraco.context" = "*" "jaraco.functools" = "*" @@ -968,13 +988,13 @@ type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "license-expression" -version = "30.3.1" +version = "30.4.1" description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." optional = false -python-versions = ">=3.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.1-py3-none-any.whl", hash = "sha256:679646bc3261a17690494a3e1cada446e5ee342dbd87dcfa4a0c24cc5dce13ee"}, + {file = "license_expression-30.4.1.tar.gz", hash = "sha256:9f02105f9e0fcecba6a85dfbbed7d94ea1c3a70cf23ddbfb5adf3438a6f6fce0"}, ] [package.dependencies] @@ -1006,13 +1026,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] @@ -1025,96 +1045,96 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] 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]] name = "mdit-py-plugins" -version = "0.3.5" +version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, ] [package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" +markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -1130,13 +1150,13 @@ files = [ [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.6.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, - {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, ] [[package]] @@ -1214,52 +1234,59 @@ files = [ [[package]] name = "mypy" -version = "1.12.0" +version = "1.14.1" 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.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -1277,53 +1304,61 @@ files = [ [[package]] name = "myst-parser" -version = "1.0.0" +version = "4.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "myst-parser-1.0.0.tar.gz", hash = "sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae"}, - {file = "myst_parser-1.0.0-py3-none-any.whl", hash = "sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c"}, + {file = "myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d"}, + {file = "myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531"}, ] [package.dependencies] -docutils = ">=0.15,<0.20" +docutils = ">=0.19,<0.22" jinja2 = "*" -markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.4,<0.4.0" +markdown-it-py = ">=3.0,<4.0" +mdit-py-plugins = ">=0.4.1,<1.0" pyyaml = "*" -sphinx = ">=5,<7" +sphinx = ">=7,<9" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.7.5,<0.8.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] +linkify = ["linkify-it-py (>=2.0,<3.0)"] +rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "nh3" -version = "0.2.18" -description = "Python bindings to the ammonia HTML sanitization library." +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "nh3-0.2.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.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, ] [[package]] @@ -1339,13 +1374,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 +1391,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 +1413,13 @@ files = [ [[package]] name = "pip" -version = "24.2" +version = "25.0" 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-25.0-py3-none-any.whl", hash = "sha256:b6eb97a803356a52b2dd4bb73ba9e65b2ba16caa6bcb25a7497350a4e5859b65"}, + {file = "pip-25.0.tar.gz", hash = "sha256:8e0a97f7b4c47ae4a494560da84775e9e2f671d415d8d828e052efefb206b30b"}, ] [[package]] @@ -1450,13 +1485,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] @@ -1531,13 +1566,13 @@ virtualenv = ">=20.10.0" [[package]] name = "py-algorand-sdk" -version = "2.6.1" +version = "2.7.0" description = "Algorand SDK in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "py-algorand-sdk-2.6.1.tar.gz", hash = "sha256:9223929d05f532a9295711c5ff945aa8aa854bc5efedb37b821f15335106ea14"}, - {file = "py_algorand_sdk-2.6.1-py3-none-any.whl", hash = "sha256:1257b0999f4c67dd66e0517da5081e014953d0a7d14edecc45d53b8aba1b7328"}, + {file = "py-algorand-sdk-2.7.0.tar.gz", hash = "sha256:9bb20d794aa4c67452330ad76fcd016195241c7bee2a39720cea688df6620a1b"}, + {file = "py_algorand_sdk-2.7.0-py3-none-any.whl", hash = "sha256:4e04e8705ac65b38adcd14ccfc39d21406019ddbd6ae5b3aba287471d139be34"}, ] [package.dependencies] @@ -1611,15 +1646,34 @@ files = [ {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, ] +[[package]] +name = "pydoclint" +version = "0.6.0" +description = "A Python docstring linter that checks arguments, returns, yields, and raises sections" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydoclint-0.6.0-py2.py3-none-any.whl", hash = "sha256:f1fb9676efe70c9a0443c7177186a01001a2227c9100272ef72d7da269ae9bbd"}, + {file = "pydoclint-0.6.0.tar.gz", hash = "sha256:bee5b509f5407c8ae180ff86a6776895084129097a4600b749fd133fd58a1cf4"}, +] + +[package.dependencies] +click = ">=8.1.0" +docstring_parser_fork = ">=0.0.12" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +flake8 = ["flake8 (>=4)"] + [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1653,13 +1707,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyparsing" -version = "3.2.0" +version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" files = [ - {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, - {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, ] [package.extras] @@ -1685,13 +1739,13 @@ tabulate = ">=0.9.0,<0.10.0" [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1699,47 +1753,47 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-httpx" -version = "0.21.3" +version = "0.35.0" description = "Send responses to httpx." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest_httpx-0.21.3-py3-none-any.whl", hash = "sha256:50b52b910f6f6cfb0aa65039d6f5bedb6ae3a0c02a98c4a7187543fe437c428a"}, - {file = "pytest_httpx-0.21.3.tar.gz", hash = "sha256:edcb62baceffbd57753c1a7afc4656b0e71e91c7a512e143c0adbac762d979c1"}, + {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, + {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, ] [package.dependencies] -httpx = "==0.23.*" -pytest = ">=6.0,<8.0" +httpx = "==0.28.*" +pytest = "==8.*" [package.extras] -testing = ["pytest-asyncio (==0.20.*)", "pytest-cov (==4.*)"] +testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==6.*)"] [[package]] name = "pytest-mock" @@ -1758,6 +1812,25 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +files = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + [[package]] name = "pytest-xdist" version = "3.6.1" @@ -1917,17 +1990,17 @@ files = [ [[package]] name = "readme-renderer" -version = "43.0" +version = "44.0" description = "readme_renderer is a library for rendering readme descriptions for Warehouse" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"}, - {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"}, + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, ] [package.dependencies] -docutils = ">=0.13.1" +docutils = ">=0.21.2" nh3 = ">=0.2.14" Pygments = ">=2.5.1" @@ -1971,30 +2044,27 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rfc3986" -version = "1.5.0" +version = "2.0.0" description = "Validating URI References per RFC 3986" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, ] -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - [package.extras] idna2008 = ["idna"] [[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 +2077,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.4.10" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.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.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] [[package]] @@ -2072,26 +2143,46 @@ files = [ {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] +[[package]] +name = "setuptools" +version = "75.8.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + [[package]] name = "six" -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]] name = "smmap" -version = "5.0.1" +version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] @@ -2127,117 +2218,129 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "sphinx" -version = "6.2.1" +version = "8.1.3" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"}, - {file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"}, + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.20" +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" imagesize = ">=1.3" -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.13" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] -name = "sphinx-autodoc2" -version = "0.5.0" -description = "Analyse a python project and create documentation for it." +name = "sphinx-autoapi" +version = "3.4.0" +description = "Sphinx API documentation generator" optional = false python-versions = ">=3.8" files = [ - {file = "sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e"}, - {file = "sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a"}, + {file = "sphinx_autoapi-3.4.0-py3-none-any.whl", hash = "sha256:4027fef2875a22c5f2a57107c71641d82f6166bf55beb407a47aaf3ef14e7b92"}, + {file = "sphinx_autoapi-3.4.0.tar.gz", hash = "sha256:e6d5371f9411bbb9fca358c00a9e57aef3ac94cbfc5df4bab285946462f69e0c"}, ] [package.dependencies] -astroid = ">=2.7,<4" -tomli = {version = "*", markers = "python_version < \"3.11\""} -typing-extensions = "*" - -[package.extras] -cli = ["typer[all]"] -docs = ["furo", "myst-parser", "sphinx (>=4.0.0)"] -sphinx = ["sphinx (>=4.0.0)"] -testing = ["pytest", "pytest-cov", "pytest-regressions", "sphinx (>=4.0.0,<7)"] +astroid = [ + {version = ">=2.7", markers = "python_version < \"3.12\""}, + {version = ">=3.0.0a1", markers = "python_version >= \"3.12\""}, +] +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=6.1.0" [[package]] -name = "sphinx-copybutton" -version = "0.5.2" -description = "Add a copy button to each of your code cells." +name = "sphinx-autobuild" +version = "2024.10.3" +description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, - {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, + {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, + {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, ] [package.dependencies] -sphinx = ">=1.8" +colorama = ">=0.4.6" +sphinx = "*" +starlette = ">=0.35" +uvicorn = ">=0.25" +watchfiles = ">=0.20" +websockets = ">=11" [package.extras] -code-style = ["pre-commit (==2.12.1)"] -rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] +test = ["httpx", "pytest (>=6)"] [[package]] -name = "sphinx-markdown-builder" -version = "0.6.7" -description = "A Sphinx extension to add markdown generation support." +name = "sphinx-basic-ng" +version = "1.0.0b2" +description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" files = [ - {file = "sphinx_markdown_builder-0.6.7-py3-none-any.whl", hash = "sha256:6d52b63d2b7b3504ca664773e805b0ee8957239f2ca86103e793d96103970839"}, - {file = "sphinx_markdown_builder-0.6.7.tar.gz", hash = "sha256:9623c8d5963e18b3733ec8335a48b58c3e556a96529b73e4c65113cabd8e8591"}, + {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, + {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, ] [package.dependencies] -docutils = "*" -sphinx = ">=5.1.0" -tabulate = "*" +sphinx = ">=4.0" [package.extras] -dev = ["black", "bumpver", "coveralls", "flake8", "isort", "pip-tools", "pylint", "pytest", "pytest-cov", "sphinx (>=5.3.0)", "sphinxcontrib-plantuml", "sphinxcontrib.httpdomain"] +docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] [[package]] -name = "sphinx-rtd-theme" -version = "1.3.0" -description = "Read the Docs theme for Sphinx" +name = "sphinx-markdown-builder" +version = "0.6.8" +description = "A Sphinx extension to add markdown generation support." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, - {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, + {file = "sphinx_markdown_builder-0.6.8-py3-none-any.whl", hash = "sha256:f04ab42d52449363228b9104569c56b778534f9c41a168af8cfc721a1e0e3edc"}, + {file = "sphinx_markdown_builder-0.6.8.tar.gz", hash = "sha256:6141b566bf18dd1cd515a0a90efd91c6c4d10fc638554fab2fd19cba66543dd7"}, ] [package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<8" -sphinxcontrib-jquery = ">=4,<5" +docutils = "*" +sphinx = ">=5.1.0" +tabulate = "*" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] +dev = ["black", "bumpver", "coveralls", "flake8", "isort", "pip-tools", "pylint", "pytest", "pytest-cov", "sphinx (>=5.3.0)", "sphinxcontrib-plantuml", "sphinxcontrib.httpdomain"] [[package]] name = "sphinxcontrib-applehelp" @@ -2287,20 +2390,6 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -2347,6 +2436,23 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] +[[package]] +name = "starlette" +version = "0.45.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +files = [ + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tabulate" version = "0.9.0" @@ -2361,6 +2467,20 @@ files = [ [package.extras] widechars = ["wcwidth"] +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + [[package]] name = "toml" version = "0.10.2" @@ -2374,13 +2494,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]] @@ -2396,20 +2546,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"] @@ -2439,13 +2590,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]] @@ -2475,13 +2626,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -2490,15 +2641,34 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.34.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +files = [ + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.29.1" 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.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, ] [package.dependencies] @@ -2510,6 +2680,89 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchfiles" +version = "1.0.4" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, + {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, + {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, + {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, + {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, + {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, + {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, + {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + [[package]] name = "webencodings" version = "0.5.1" @@ -2521,108 +2774,107 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "websockets" +version = "14.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, + {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, + {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, + {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, + {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, + {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, + {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, + {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, + {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, + {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, + {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, + {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, + {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, + {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, + {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, + {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, + {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, + {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, + {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, + {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, + {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, + {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, + {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, + {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, + {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, + {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, + {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, + {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, + {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, + {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, + {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, + {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, + {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, + {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, + {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, + {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, + {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, + {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, +] + [[package]] name = "wheel" -version = "0.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] test = ["pytest (>=6.0.0)", "setuptools (>=65)"] -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -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"}, -] - [[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] @@ -2636,4 +2888,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "59127574db0011d8eb6e5a2d55be3048e9cb4a68e34c9c3e5f4a836d488b7318" +content-hash = "c831facca8536a1b7d018cd049682ee762ee374ed77ddec03c9a0acd62086b19" diff --git a/pyproject.toml b/pyproject.toml index cbe8ab41..278a5a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,31 +9,34 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" py-algorand-sdk = "^2.4.0" -httpx = "^0.23.1" -deprecated = "^1.2.14" +httpx = "^0.28" +typing-extensions = ">=4.6.0" # Add this line [tool.poetry.group.dev.dependencies] -pytest = "^7.2.0" -ruff = ">=0.1.6,<0.5.0" +pytest = "^8" +ruff = ">=0.1.6,<=0.8.3" pip-audit = "^2.5.6" -pytest-mock = "^3.11.1" +pytest-mock = "^3.14" mypy = "^1.5.1" python-semantic-release = "^7.34.3" -pytest-cov = "^4.1.0" +pytest-cov = "^6" pre-commit = "^3.4.0" python-dotenv = "^1.0.0" -sphinx = "^6.1.3" -myst-parser = "^1.0.0" -sphinx-copybutton = "^0.5.1" -sphinx-rtd-theme = "^1.2.0" -sphinx-autodoc2 = ">=0.4.2,<0.6.0" +sphinx = "^8.0.0" poethepoet = ">=0.19,<0.26" beaker-pyteal = "^1.1.1" -types-deprecated = "^1.2.9.2" -pytest-httpx = "^0.21.3" -pytest-xdist = "^3.4.0" -sphinx-markdown-builder = "^0.6.6" +pytest-httpx = "^0.35" +pytest-xdist = "^3.6.1" linkify-it-py = "^2.0.3" +setuptools = "^75.2.0" +pydoclint = "^0.6.0" +pytest-sugar = "^1.0.0" +types-deprecated = "^1.2.15.20241117" +sphinx-autobuild = "^2024.10.3" +furo = "^2024.8.6" +myst-parser = "^4.0.0" +sphinx-autoapi = "^3.4.0" +sphinx-markdown-builder = "^0.6.8" [build-system] requires = ["poetry-core"] @@ -80,7 +83,6 @@ lint.select = [ "SLF", # flake8-self "SIM", # flake8-simplify "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib "ERA", # eradicate @@ -92,8 +94,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 @@ -105,6 +105,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 = [ @@ -112,30 +113,44 @@ 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"] +"src/algokit_utils/__init__.py" = ["I001", "RUF022"] # Ignore import sorting for __init__.py [tool.poe.tasks] docs = ["docs-html-only", "docs-md-only"] docs-md-only = "sphinx-build docs/source docs/markdown -b markdown" docs-html-only = "sphinx-build docs/source docs/html" +docstrings-check = "pydoclint src --style sphinx --arg-type-hints-in-docstring false --check-return-types false --exclude src/algokit_utils/_legacy_v2" +docs-dev = "sphinx-autobuild --ignore '**/_build/**' --ignore '**/autoapi/**' --ignore '**/.doctrees/**' docs/source docs/_build" [tool.pytest.ini_options] pythonpath = ["src", "tests"] +norecursedirs = ["src"] # Ignore test collection in source directory, otherwise picks up TestNet* prefixed abstractions +filterwarnings = [ + # Ignore deprecations in utils legacy v2 is removed + "ignore::DeprecationWarning", +] [tool.mypy] files = ["src", "tests"] -exclude = ["dist"] +exclude = ["dist", "tests/artifacts", "src/algokit_utils/_legacy_v2"] python_version = "3.10" warn_unused_ignores = true warn_redundant_casts = true @@ -148,6 +163,18 @@ disallow_any_generics = false implicit_reexport = false show_error_codes = true +untyped_calls_exclude = [ + "algosdk", +] + +[[tool.mypy.overrides]] +module = ["algosdk", "algosdk.*"] +disallow_untyped_calls = false + +[[tool.mypy.overrides]] +module = ["tests.transactions.test_transaction_composer"] +disable_error_code = ["call-overload", "union-attr"] + [tool.semantic_release] version_toml = "pyproject.toml:tool.poetry.version" remove_dist = false diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 02a5e341..399a81cd 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -1,182 +1,24 @@ -from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response -from algokit_utils._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded -from algokit_utils._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset -from algokit_utils.account import ( - create_kmd_wallet_account, - get_account, - get_account_from_mnemonic, - get_dispenser_account, - get_kmd_wallet_account, - get_localnet_default_account, - get_or_create_kmd_wallet_account, -) -from algokit_utils.application_client import ( - ApplicationClient, - execute_atc_with_logic_error, - get_next_version, - get_sender_from_signer, - num_extra_program_pages, -) -from algokit_utils.application_specification import ( - ApplicationSpecification, - AppSpecStateDict, - CallConfig, - DefaultArgumentDict, - DefaultArgumentType, - MethodConfigDict, - MethodHints, - OnCompleteActionName, -) -from algokit_utils.asset import opt_in, opt_out -from algokit_utils.common import Program -from algokit_utils.deploy import ( - DELETABLE_TEMPLATE_NAME, - NOTE_PREFIX, - UPDATABLE_TEMPLATE_NAME, - ABICallArgs, - ABICallArgsDict, - ABICreateCallArgs, - ABICreateCallArgsDict, - AppDeployMetaData, - AppLookup, - AppMetaData, - AppReference, - DeployCallArgs, - DeployCallArgsDict, - DeployCreateCallArgs, - DeployCreateCallArgsDict, - DeploymentFailedError, - DeployResponse, - OnSchemaBreak, - OnUpdate, - OperationPerformed, - TemplateValueDict, - TemplateValueMapping, - get_app_id_from_tx_id, - get_creator_apps, - replace_template_variables, -) -from algokit_utils.dispenser_api import ( - DISPENSER_ACCESS_TOKEN_KEY, - DISPENSER_REQUEST_TIMEOUT, - DispenserFundResponse, - DispenserLimitResponse, - TestNetDispenserApiClient, -) -from algokit_utils.logic_error import LogicError -from algokit_utils.models import ( - ABIArgsDict, - ABIMethod, - ABITransactionResponse, - Account, - CommonCallParameters, # noqa: F401 - CommonCallParametersDict, # noqa: F401 - CreateCallParameters, - CreateCallParametersDict, - CreateTransactionParameters, - OnCompleteCallParameters, - OnCompleteCallParametersDict, - RawTransactionParameters, # noqa: F401 - TransactionParameters, - TransactionParametersDict, - TransactionResponse, -) -from algokit_utils.network_clients import ( - AlgoClientConfig, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client_from_algod_client, - is_localnet, - is_mainnet, - is_testnet, -) +"""AlgoKit Python Utilities - a set of utilities for building solutions on Algorand -__all__ = [ - "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", - "NOTE_PREFIX", - "DeploymentFailedError", - "AppReference", - "AppDeployMetaData", - "AppMetaData", - "AppLookup", - "get_creator_apps", - "replace_template_variables", - "ABIArgsDict", - "ABICallArgs", - "ABICallArgsDict", - "ABICreateCallArgs", - "ABICreateCallArgsDict", - "ABIMethod", - "CreateCallParameters", - "CreateCallParametersDict", - "CreateTransactionParameters", - "DeployCallArgs", - "DeployCreateCallArgs", - "DeployCallArgsDict", - "DeployCreateCallArgsDict", - "OnCompleteCallParameters", - "OnCompleteCallParametersDict", - "TransactionParameters", - "TransactionParametersDict", - "ApplicationClient", - "DeployResponse", - "OnUpdate", - "OnSchemaBreak", - "OperationPerformed", - "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", - "TransactionResponse", - "AlgoClientConfig", - "get_algod_client", - "get_algonode_config", - "get_default_localnet_config", - "get_indexer_client", - "get_kmd_client_from_algod_client", - "is_localnet", - "is_mainnet", - "is_testnet", - "TestNetDispenserApiClient", - "DispenserFundResponse", - "DispenserLimitResponse", - "DISPENSER_ACCESS_TOKEN_KEY", - "DISPENSER_REQUEST_TIMEOUT", - "EnsureBalanceParameters", - "EnsureFundedResponse", - "TransferParameters", - "ensure_funded", - "transfer", - "TransferAssetParameters", - "transfer_asset", - "opt_in", - "opt_out", - "persist_sourcemaps", - "PersistSourceMapInput", - "simulate_and_persist_response", -] +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + + from algokit_utils.accounts import KmdAccountManager + from algokit_utils.applications import AppClient + from algokit_utils.applications.app_spec import Arc52Contract + etc. +""" + +# Core types and utilities that are commonly used +from algokit_utils.applications import * # noqa: F403 +from algokit_utils.assets import * # noqa: F403 +from algokit_utils.protocols import * # noqa: F403 +from algokit_utils.models import * # noqa: F403 +from algokit_utils.accounts import * # noqa: F403 +from algokit_utils.clients import * # noqa: F403 +from algokit_utils.transactions import * # noqa: F403 +from algokit_utils.errors import * # noqa: F403 +from algokit_utils.algorand import * # noqa: F403 + +# Legacy types and utilities +from algokit_utils._legacy_v2 import * # noqa: F403 diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index b10356f2..957b2cbe 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -14,7 +14,8 @@ from algosdk.encoding import checksum from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig -from algokit_utils.common import Program +from algokit_utils._legacy_v2.common import Program +from algokit_utils.models.application import CompiledTeal if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -64,7 +65,11 @@ def to_dict(self) -> dict: @dataclass class PersistSourceMapInput: def __init__( - self, app_name: str, file_name: str, raw_teal: str | None = None, compiled_teal: Program | None = None + self, + app_name: str, + file_name: str, + raw_teal: str | None = None, + compiled_teal: CompiledTeal | Program | None = None, ): self.compiled_teal = compiled_teal self.app_name = app_name @@ -76,7 +81,9 @@ def from_raw_teal(cls, raw_teal: str, app_name: str, file_name: str) -> "Persist return cls(app_name, file_name, raw_teal=raw_teal) @classmethod - def from_compiled_teal(cls, compiled_teal: Program, app_name: str, file_name: str) -> "PersistSourceMapInput": + def from_compiled_teal( + cls, compiled_teal: CompiledTeal | Program, app_name: str, file_name: str + ) -> "PersistSourceMapInput": return cls(app_name, file_name, compiled_teal=compiled_teal) @property @@ -112,24 +119,35 @@ 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, output_path: Path, client: "AlgodClient", raw_teal: str | None = None, - compiled_teal: Program | None = None, + compiled_teal: CompiledTeal | Program | None = None, with_sources: bool = True, ) -> AVMDebuggerSourceMapEntry: if not raw_teal and not compiled_teal: raise ValueError("Either raw teal or compiled teal must be provided") - result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client) - program_hash = base64.b64encode( - checksum(result.raw_binary) # type: ignore[no-untyped-call] - ).decode() - source_map = result.source_map.__dict__ + # Handle both legacy Program and new CompiledTeal + if isinstance(compiled_teal, Program): + program_hash = base64.b64encode(checksum(compiled_teal.raw_binary)).decode() + source_map = compiled_teal.source_map.__dict__ + teal_content = compiled_teal.teal + elif isinstance(compiled_teal, CompiledTeal): + program_hash = base64.b64encode(checksum(compiled_teal.compiled_base64_to_bytes)).decode() + source_map = compiled_teal.source_map.__dict__ if compiled_teal.source_map else {} + teal_content = compiled_teal.teal + else: + # Handle raw TEAL case + result = Program(str(raw_teal), client=client) + program_hash = base64.b64encode(checksum(result.raw_binary)).decode() + source_map = result.source_map.__dict__ + teal_content = result.teal + source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else [] output_dir_path = output_path / ALGOKIT_DIR / SOURCES_DIR / app_name @@ -138,7 +156,7 @@ def _build_avm_sourcemap( # noqa: PLR0913 _write_to_file(source_map_output_path, json.dumps(source_map)) if with_sources: - _write_to_file(teal_output_path, result.teal) + _write_to_file(teal_output_path, teal_content) return AVMDebuggerSourceMapEntry(str(source_map_output_path), program_hash) @@ -169,12 +187,11 @@ def persist_sourcemaps( ) -> None: """ Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts. - Args: - sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects. - project_root (Path): The root directory of the project. - client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. - with_sources (bool): If True, it will dump teal source files along with sourcemaps. - Default is True, as needed by an AlgoKit AVM debugger. + + :param sources: A list of PersistSourceMapInput objects. + :param project_root: The root directory of the project. + :param client: An AlgodClient object for interacting with the Algorand blockchain. + :param with_sources: If True, it will dump teal source files along with sourcemaps. """ for source in sources: @@ -189,17 +206,17 @@ def persist_sourcemaps( ) -def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse: - """ - Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. - - Args: - atc (AtomicTransactionComposer): An AtomicTransactionComposer object. - algod_client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain. - - Returns: - SimulateAtomicTransactionResponse: The simulated response. - """ +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, + simulation_round: int | None = None, +) -> SimulateAtomicTransactionResponse: + """Simulate atomic transaction group execution""" unsigned_txn_groups = atc.build_group() empty_signer = EmptySigner() @@ -209,28 +226,46 @@ 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 if allow_more_logs is not None else True, + round=simulation_round, + extra_opcode_budget=extra_opcode_budget if extra_opcode_budget is not None else 0, + allow_unnamed_resources=allow_unnamed_resources if allow_unnamed_resources is not None else True, + allow_empty_signatures=allow_empty_signatures if allow_empty_signatures is not None else True, + exec_trace_config=exec_trace_config if exec_trace_config is not None else trace_config, ) + return atc.simulate(algod_client, simulate_request) def simulate_and_persist_response( - atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256 + 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, + simulation_round: int | None = None, ) -> SimulateAtomicTransactionResponse: - """ - Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, - and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. - - :param atc: An `AtomicTransactionComposer` object representing the atomic transactions to be - simulated and persisted. - :param project_root: A `Path` object representing the root directory of the project. - :param algod_client: An `AlgodClient` object representing the Algorand client. - :param buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256mb. - :return: None - - Returns: - SimulateAtomicTransactionResponse: The simulated response after persisting it - for AlgoKit AVM Debugger consumption. + """Simulates atomic transactions and persists simulation response to a JSON file. + + Simulates the atomic transactions using the provided AtomicTransactionComposer and AlgodClient, + then persists the simulation response to an AlgoKit AVM Debugger compliant JSON file. + + :param atc: AtomicTransactionComposer containing transactions to simulate and persist + :param project_root: Root directory path of the project + :param algod_client: Algorand client instance + :param buffer_size_mb: Size of trace buffer in megabytes, defaults to 256 + :param allow_more_logs: Flag to allow additional logs, defaults to None + :param allow_empty_signatures: Flag to allow empty signatures, defaults to None + :param allow_unnamed_resources: Flag to allow unnamed resources, defaults to None + :param extra_opcode_budget: Additional opcode budget, defaults to None + :param exec_trace_config: Execution trace configuration, defaults to None + :param simulation_round: Round number for simulation, defa ults to None + :return: Simulated response after persisting for AlgoKit AVM Debugger consumption """ atc_to_simulate = atc.clone() sp = algod_client.suggested_params() @@ -240,7 +275,16 @@ 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, + simulation_round, + ) txn_results = response.simulate_response["txn-groups"] txn_types = [ diff --git a/src/algokit_utils/_legacy_v2/__init__.py b/src/algokit_utils/_legacy_v2/__init__.py new file mode 100644 index 00000000..25c8b08f --- /dev/null +++ b/src/algokit_utils/_legacy_v2/__init__.py @@ -0,0 +1,177 @@ +"""AlgoKit Python Utilities (Legacy V2) - a set of utilities for building solutions on Algorand + +This module provides commonly used utilities and types at the root level for convenience. +For more specific functionality, import directly from the relevant submodules: + + from algokit_utils.accounts import KmdAccountManager + from algokit_utils.applications import AppClient + from algokit_utils.applications.app_spec import Arc52Contract + etc. +""" + +# Debugging utilities +from algokit_utils._legacy_v2._ensure_funded import ( + EnsureBalanceParameters, + EnsureFundedResponse, + ensure_funded, +) +from algokit_utils._legacy_v2._transfer import ( + TransferAssetParameters, + TransferParameters, + transfer, + transfer_asset, +) +from algokit_utils._legacy_v2.account import ( + create_kmd_wallet_account, + get_account, + get_account_from_mnemonic, + get_dispenser_account, + get_kmd_wallet_account, + get_localnet_default_account, + get_or_create_kmd_wallet_account, +) +from algokit_utils._legacy_v2.application_client import ( + ApplicationClient, + execute_atc_with_logic_error, + get_next_version, + get_sender_from_signer, + num_extra_program_pages, +) +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + AppSpecStateDict, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) +from algokit_utils._legacy_v2.asset import opt_in, opt_out +from algokit_utils._legacy_v2.common import Program +from algokit_utils._legacy_v2.deploy import ( + NOTE_PREFIX, + ABICallArgs, + ABICallArgsDict, + ABICreateCallArgs, + ABICreateCallArgsDict, + AppDeployMetaData, + AppLookup, + AppMetaData, + AppReference, + DeployCallArgs, + DeployCallArgsDict, + DeployCreateCallArgs, + DeployCreateCallArgsDict, + DeploymentFailedError, + DeployResponse, + TemplateValueDict, + TemplateValueMapping, + get_app_id_from_tx_id, + get_creator_apps, + replace_template_variables, +) +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIMethod, + ABITransactionResponse, + Account, + CommonCallParameters, + CommonCallParametersDict, + CreateCallParameters, + CreateCallParametersDict, + CreateTransactionParameters, + OnCompleteCallParameters, + OnCompleteCallParametersDict, + TransactionParameters, + TransactionParametersDict, + TransactionResponse, +) +from algokit_utils._legacy_v2.network_clients import ( + AlgoClientConfig, + get_algod_client, + get_algonode_config, + get_default_localnet_config, + get_indexer_client, + get_kmd_client_from_algod_client, + is_localnet, + is_mainnet, + is_testnet, +) + +__all__ = [ + "NOTE_PREFIX", + "ABIArgsDict", + "ABICallArgs", + "ABICallArgsDict", + "ABICreateCallArgs", + "ABICreateCallArgsDict", + "ABIMethod", + "ABITransactionResponse", + "Account", + "AlgoClientConfig", + "AppDeployMetaData", + "AppLookup", + "AppMetaData", + "AppReference", + "AppSpecStateDict", + "ApplicationClient", + "ApplicationSpecification", + "CallConfig", + "CommonCallParameters", + "CommonCallParametersDict", + "CreateCallParameters", + "CreateCallParametersDict", + "CreateTransactionParameters", + "DefaultArgumentDict", + "DefaultArgumentType", + "DeployCallArgs", + "DeployCallArgsDict", + "DeployCreateCallArgs", + "DeployCreateCallArgsDict", + "DeployResponse", + "DeploymentFailedError", + "EnsureBalanceParameters", + "EnsureFundedResponse", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", + "OnCompleteCallParameters", + "OnCompleteCallParametersDict", + "Program", + "TemplateValueDict", + "TemplateValueMapping", + "TransactionParameters", + "TransactionParametersDict", + "TransactionResponse", + "TransferAssetParameters", + "TransferParameters", + # Legacy v2 functions + "create_kmd_wallet_account", + "ensure_funded", + "execute_atc_with_logic_error", + "get_account", + "get_account_from_mnemonic", + "get_algod_client", + "get_algonode_config", + "get_app_id_from_tx_id", + "get_creator_apps", + "get_default_localnet_config", + "get_dispenser_account", + "get_indexer_client", + "get_kmd_client_from_algod_client", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_next_version", + "get_or_create_kmd_wallet_account", + "get_sender_from_signer", + "is_localnet", + "is_mainnet", + "is_testnet", + "num_extra_program_pages", + "opt_in", + "opt_out", + "replace_template_variables", + "transfer", + "transfer_asset", +] diff --git a/src/algokit_utils/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py similarity index 81% rename from src/algokit_utils/_ensure_funded.py rename to src/algokit_utils/_legacy_v2/_ensure_funded.py index b80734e4..1add7522 100644 --- a/src/algokit_utils/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -4,15 +4,16 @@ 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._transfer import TransferParameters, transfer -from algokit_utils.account import get_dispenser_account -from algokit_utils.dispenser_api import ( +from algokit_utils._legacy_v2._transfer import TransferParameters, transfer +from algokit_utils._legacy_v2.account import get_dispenser_account +from algokit_utils._legacy_v2.models import Account +from algokit_utils._legacy_v2.network_clients import is_testnet +from algokit_utils.clients.dispenser_api_client import ( DispenserAssetName, TestNetDispenserApiClient, ) -from algokit_utils.models import Account -from algokit_utils.network_clients import is_testnet @dataclass(kw_only=True) @@ -63,7 +64,7 @@ def _get_address_to_fund(parameters: EnsureBalanceParameters) -> str: if isinstance(parameters.account_to_fund, str): return parameters.account_to_fund else: - return str(address_from_private_key(parameters.account_to_fund.private_key)) # type: ignore[no-untyped-call] + return str(address_from_private_key(parameters.account_to_fund.private_key)) def _get_account_info(client: AlgodClient, address_to_fund: str) -> dict: @@ -111,28 +112,28 @@ def _fund_using_transfer( fee_micro_algos=parameters.fee_micro_algos, ), ) - transaction_id = response.get_txid() # type: ignore[no-untyped-call] + transaction_id = response.get_txid() return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt) +@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, ) -> EnsureFundedResponse | None: """ - Funds a given account using a funding source such that it has a certain amount of algos free to spend - (accounting for ALGOs locked in minimum balance requirement) - see - + Funds a given account using a funding source to ensure it has sufficient spendable ALGOs. - Args: - client (AlgodClient): An instance of the AlgodClient class from the AlgoSDK library. - parameters (EnsureBalanceParameters): An instance of the EnsureBalanceParameters class that - specifies the account to fund and the minimum spending balance. + Ensures the target account has enough ALGOs free to spend after accounting for ALGOs locked in minimum balance + requirements. See https://developer.algorand.org/docs/get-details/accounts/#minimum-balance for details on minimum + balance requirements. - Returns: - PaymentTxn | str | None: If funds are needed, the function returns a payment transaction or a - string indicating that the dispenser API was used. If no funds are needed, the function returns None. + :param client: An instance of the AlgodClient class from the AlgoSDK library + :param parameters: Parameters specifying the account to fund and minimum spending balance requirements + :return: If funds are needed, returns payment transaction details or dispenser API response. Returns None if no funding needed """ address_to_fund = _get_address_to_fund(parameters) diff --git a/src/algokit_utils/_transfer.py b/src/algokit_utils/_legacy_v2/_transfer.py similarity index 72% rename from src/algokit_utils/_transfer.py rename to src/algokit_utils/_legacy_v2/_transfer.py index 0103b172..b5136051 100644 --- a/src/algokit_utils/_transfer.py +++ b/src/algokit_utils/_legacy_v2/_transfer.py @@ -6,30 +6,29 @@ from algosdk.account import address_from_private_key from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams +from typing_extensions import deprecated -from algokit_utils.models import Account +from algokit_utils._legacy_v2.models import Account 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__) @dataclasses.dataclass(kw_only=True) class TransferParametersBase: - """Parameters for transferring µALGOs between accounts - - Args: - from_account (Account | AccountTransactionSigner): The account (with private key) or signer that will send - the µALGOs - to_address (str): The account address that will receive the µALGOs - suggested_params (SuggestedParams | None): (optional) transaction parameters - note (str | bytes | None): (optional) transaction note - fee_micro_algos (int | None): (optional) The flat fee you want to pay, useful for covering extra fees in a - transaction group or app call - max_fee_micro_algos (int | None): (optional) The maximum fee that you are happy to pay (default: unbounded) - - if this is set it's possible the transaction could get rejected during network congestion + """Parameters for transferring µALGOs between accounts. + + This class contains the base parameters needed for transferring µALGOs between Algorand accounts. + + :ivar from_account: The account (with private key) or signer that will send the µALGOs + :ivar to_address: The account address that will receive the µALGOs + :ivar suggested_params: Transaction parameters, defaults to None + :ivar note: Transaction note, defaults to None + :ivar fee_micro_algos: The flat fee you want to pay, useful for covering extra fees in a transaction group or app call, defaults to None + :ivar max_fee_micro_algos: The maximum fee that you are happy to pay - if this is set it's possible the transaction could get rejected during network congestion, defaults to None """ from_account: Account | AccountTransactionSigner @@ -49,13 +48,13 @@ class TransferParameters(TransferParametersBase): @dataclasses.dataclass(kw_only=True) class TransferAssetParameters(TransferParametersBase): - """Parameters for transferring assets between accounts + """Parameters for transferring assets between accounts. + + Defines the parameters needed to transfer Algorand Standard Assets (ASAs) between accounts. - Args: - asset_id (int): The asset id that will be transfered - amount (int): The amount to send - clawback_from (str | None): An address of a target account from which to perform a clawback operation. Please - note, in such cases senderAccount must be equal to clawback field on ASA metadata. + :param asset_id: The asset id that will be transferred + :param amount: The amount of the asset to send + :param clawback_from: An address of a target account from which to perform a clawback operation. Please note, in such cases senderAccount must be equal to clawback field on ASA metadata, defaults to None """ asset_id: int @@ -80,6 +79,7 @@ def _check_fee(transaction: PaymentTxn | AssetTransferTxn, max_fee: int | None) ) +@deprecated("Use the `TransactionComposer` abstraction instead to construct appropriate transfer transactions") def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTxn: """Transfer µALGOs between accounts""" @@ -93,13 +93,14 @@ def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTx amt=params.micro_algos, note=params.note.encode("utf-8") if isinstance(params.note, str) else params.note, sp=params.suggested_params, - ) # type: ignore[no-untyped-call] + ) result = _send_transaction(client=client, transaction=transaction, parameters=params) assert isinstance(result, PaymentTxn) return result +@deprecated("Use the `TransactionComposer` abstraction instead to construct appropriate transfer transactions") def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) -> AssetTransferTxn: """Transfer assets between accounts""" @@ -117,7 +118,7 @@ def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) - note=params.note, index=params.asset_id, rekey_to=None, - ) # type: ignore[no-untyped-call] + ) result = _send_transaction(client=client, transaction=xfer_txn, parameters=params) assert isinstance(result, AssetTransferTxn) @@ -148,5 +149,5 @@ def _get_address(account: Account | AccountTransactionSigner) -> str: if type(account) is Account: return account.address else: - address = address_from_private_key(account.private_key) # type: ignore[no-untyped-call] + address = address_from_private_key(account.private_key) return str(address) diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py new file mode 100644 index 00000000..fa0bfa52 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/account.py @@ -0,0 +1,203 @@ +import logging +import os +from typing import TYPE_CHECKING, Any + +from algosdk.account import address_from_private_key +from algosdk.mnemonic import from_private_key, to_private_key +from algosdk.util import algos_to_microalgos +from typing_extensions import deprecated + +from algokit_utils._legacy_v2._transfer import TransferParameters, transfer +from algokit_utils._legacy_v2.models import Account +from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.kmd import KMDClient + from algosdk.v2client.algod import AlgodClient + +__all__ = [ + "create_kmd_wallet_account", + "get_account", + "get_account_from_mnemonic", + "get_dispenser_account", + "get_kmd_wallet_account", + "get_localnet_default_account", + "get_or_create_kmd_wallet_account", +] + +logger = logging.getLogger(__name__) +_DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 + + +@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 = 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"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + kmd_client.generate_key(wallet_handle) + + key_ids: list[str] = kmd_client.list_keys(wallet_handle) + account_key = key_ids[0] + + private_account_key = kmd_client.export_key(wallet_handle, "", account_key) + return get_account_from_mnemonic(from_private_key(private_account_key)) + + +@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: + """Returns a wallet with specified name, or creates one if not found""" + kmd_client = kmd_client or get_kmd_client_from_algod_client(client) + account = get_kmd_wallet_account(client, kmd_client, name) + + if account: + account_info = client.account_info(account.address) + assert isinstance(account_info, dict) + if account_info["amount"] > 0: + return account + logger.debug(f"Found existing account in LocalNet with name '{name}', but no funds in the account.") + else: + account = create_kmd_wallet_account(kmd_client, name) + + logger.debug( + f"Couldn't find existing account in LocalNet with name '{name}'. " + f"So created account {account.address} with keys stored in KMD." + ) + + logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs") + + if fund_with_algos: + transfer( + client, + TransferParameters( + from_account=get_dispenser_account(client), + to_address=account.address, + micro_algos=algos_to_microalgos(fund_with_algos), + ), + ) + + return account + + +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): + raise Exception("Can't get a default account from non LocalNet network") + + account = get_kmd_wallet_account( + client, get_kmd_client_from_algod_client(client), "unencrypted-default-wallet", _is_default_account + ) + assert account + return account + + +@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): + return get_localnet_default_account(client) + 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", + name: str, + predicate: "Callable[[dict[str, Any]], bool] | None" = None, +) -> Account | None: + """Returns wallet matching specified name and predicate or None if not found""" + wallets: list[dict] = kmd_client.list_wallets() + + wallet = next((w for w in wallets if w["name"] == name), None) + if wallet is None: + return None + + wallet_id = wallet["id"] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") + key_ids: list[str] = kmd_client.list_keys(wallet_handle) + matched_account_key = None + if predicate: + for key in key_ids: + account = client.account_info(key) + assert isinstance(account, dict) + if predicate(account): + matched_account_key = key + else: + matched_account_key = next(key_ids.__iter__(), None) + + if not matched_account_key: + return None + + private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key) + return get_account_from_mnemonic(from_private_key(private_account_key)) + + +@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: + """Returns an Algorand account with private key loaded by convention based on the given name identifier. + Returns an Algorand account with private key loaded by convention based on the given name identifier. + + For non-LocalNet environments, loads the mnemonic secret from environment variable {name}_MNEMONIC. + For LocalNet environments, loads or creates an account from a KMD wallet named {name}. + + :example: + >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call: + >>> account = get_account('ACCOUNT', algod) + >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created + >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + + :param client: The Algorand client to use + :param name: The name identifier to use for loading/creating the account + :param fund_with_algos: Amount of Algos to fund new LocalNet accounts with, defaults to 1000 + :param kmd_client: Optional KMD client to use for LocalNet wallet operations + :raises Exception: If required environment variable is missing in non-LocalNet environment + :return: An Account object with loaded private key + """ + + mnemonic_key = f"{name.upper()}_MNEMONIC" + mnemonic = os.getenv(mnemonic_key) + if mnemonic: + return get_account_from_mnemonic(mnemonic) + + if is_localnet(client): + account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) + os.environ[mnemonic_key] = from_private_key(account.private_key) + return account + + raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'") diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py new file mode 100644 index 00000000..d3a12852 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -0,0 +1,1471 @@ +from __future__ import annotations + +import base64 +import copy +import json +import logging +import re +import typing +from math import ceil +from pathlib import Path +from typing import Any, Literal, cast, overload + +import algosdk +from algosdk import transaction +from algosdk.abi import ABIType, Method, Returns +from algosdk.account import address_from_private_key +from algosdk.atomic_transaction_composer import ( + ABI_RETURN_HASH, + ABIResult, + AccountTransactionSigner, + AtomicTransactionComposer, + AtomicTransactionResponse, + LogicSigTransactionSigner, + MultisigTransactionSigner, + SimulateAtomicTransactionResponse, + TransactionSigner, + TransactionWithSigner, +) +from algosdk.constants import APP_PAGE_MAX_SIZE +from algosdk.logic import get_application_address +from algosdk.source_map import SourceMap +from typing_extensions import deprecated + +import algokit_utils._legacy_v2.application_specification as au_spec +import algokit_utils._legacy_v2.deploy as au_deploy +from algokit_utils._legacy_v2.common import Program +from algokit_utils._legacy_v2.logic_error import LogicError, parse_logic_error +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIArgType, + ABIMethod, + ABITransactionResponse, + Account, + CreateCallParameters, + CreateCallParametersDict, + OnCompleteCallParameters, + OnCompleteCallParametersDict, + SimulationTrace, + TransactionParameters, + TransactionParametersDict, + TransactionResponse, +) +from algokit_utils.config import config + +if typing.TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + +logger = logging.getLogger(__name__) + + +"""A dictionary `dict[str, Any]` representing ABI argument names and values""" + +__all__ = [ + "ApplicationClient", + "execute_atc_with_logic_error", + "get_next_version", + "get_sender_from_signer", + "num_extra_program_pages", +] + +"""Alias for {py:class}`pyteal.ABIReturnSubroutine`, {py:class}`algosdk.abi.method.Method` or a {py:class}`str` +representing an ABI method name or signature""" + + +def num_extra_program_pages(approval: bytes, clear: bytes) -> int: + """Calculate minimum number of extra_pages required for provided approval and clear programs""" + + return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) + + +@deprecated( + "Use AppClient from algokit_utils.applications instead. Example:\n" + "```python\n" + "from algokit_utils import AlgorandClient\n" + "from algokit_utils.models.application import Arc56Contract\n" + "algorand_client = AlgorandClient.from_environment()\n" + "app_client = AppClient.from_network(app_spec=Arc56Contract.from_json(app_spec_json), " + "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 + + ApplicationClient can be created with an app_id to interact with an existing application, alternatively + it can be created with a creator and indexer_client specified to find existing applications by name and creator. + + :param AlgodClient algod_client: AlgoSDK algod client + :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one + :param int app_id: The app_id of an existing application, to instead find the application by creator and name + use the creator and indexer_client parameters + :param str | Account creator: The address or Account of the app creator to resolve the app_id + :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by + creator and app name + :param AppLookup existing_deployments: + :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and + creator was passed as an Account will use that. + :param str sender: Address to use as the sender for all transactions, will use the address associated with the + signer if not specified. + :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should + *NOT* include the TMPL_ prefix + :param str | None app_name: Name of application to use when deploying, defaults to name defined on the + Application Specification + """ + + @overload + def __init__( + self, + algod_client: AlgodClient, + app_spec: au_spec.ApplicationSpecification | Path, + *, + app_id: int = 0, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + ): ... + + @overload + def __init__( + self, + algod_client: AlgodClient, + app_spec: au_spec.ApplicationSpecification | Path, + *, + creator: str | Account, + indexer_client: IndexerClient | None = None, + existing_deployments: au_deploy.AppLookup | None = None, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + app_name: str | None = None, + ): ... + + def __init__( # noqa: PLR0913 + self, + algod_client: AlgodClient, + app_spec: au_spec.ApplicationSpecification | Path, + *, + app_id: int = 0, + creator: str | Account | None = None, + indexer_client: IndexerClient | None = None, + existing_deployments: au_deploy.AppLookup | None = None, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueMapping | None = None, + app_name: str | None = None, + ): + self.algod_client = algod_client + self.app_spec = ( + au_spec.ApplicationSpecification.from_json(app_spec.read_text()) if isinstance(app_spec, Path) else app_spec + ) + self._app_name = app_name + self._approval_program: Program | None = None + self._approval_source_map: SourceMap | None = None + self._clear_program: Program | None = None + + self.template_values: au_deploy.TemplateValueMapping = template_values or {} + self.existing_deployments = existing_deployments + self._indexer_client = indexer_client + if creator is not None: + if not self.existing_deployments and not self._indexer_client: + raise Exception( + "If using the creator parameter either existing_deployments or indexer_client must also be provided" + ) + self._creator: str | None = creator.address if isinstance(creator, Account) else creator + if self.existing_deployments and self.existing_deployments.creator != self._creator: + raise Exception( + "Attempt to create application client with invalid existing_deployments against" + f"a different creator ({self.existing_deployments.creator} instead of " + f"expected creator {self._creator}" + ) + self.app_id = 0 + else: + self.app_id = app_id + self._creator = None + + self.signer: TransactionSigner | None + if signer: + self.signer = ( + signer if isinstance(signer, TransactionSigner) else AccountTransactionSigner(signer.private_key) + ) + elif isinstance(creator, Account): + self.signer = AccountTransactionSigner(creator.private_key) + else: + self.signer = None + + self.sender = sender + self.suggested_params = suggested_params + + @property + def app_name(self) -> str: + return self._app_name or self.app_spec.contract.name + + @app_name.setter + def app_name(self, value: str) -> None: + self._app_name = value + + @property + def app_address(self) -> str: + return get_application_address(self.app_id) + + @property + def approval(self) -> Program | None: + return self._approval_program + + @property + def approval_source_map(self) -> SourceMap | None: + if self._approval_source_map: + return self._approval_source_map + if self._approval_program: + return self._approval_program.source_map + return None + + @approval_source_map.setter + def approval_source_map(self, value: SourceMap) -> None: + self._approval_source_map = value + + @property + def clear(self) -> Program | None: + return self._clear_program + + def prepare( + self, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> ApplicationClient: + """Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. + Will also substitute provided template_values into the associated app_spec in the copy""" + new_client: ApplicationClient = copy.copy(self) + new_client._prepare( # noqa: SLF001 + new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values + ) + return new_client + + def _prepare( + self, + target: ApplicationClient, + *, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> None: + target.app_id = self.app_id if app_id is None else app_id + target.signer, target.sender = target.get_signer_sender( + AccountTransactionSigner(signer.private_key) if isinstance(signer, Account) else signer, sender + ) + target.template_values = {**self.template_values, **(template_values or {})} + + def deploy( # noqa: PLR0913 + self, + version: str | None = None, + *, + signer: TransactionSigner | None = None, + sender: str | None = None, + allow_update: bool | None = None, + allow_delete: bool | None = None, + on_update: au_deploy.OnUpdate = au_deploy.OnUpdate.Fail, + on_schema_break: au_deploy.OnSchemaBreak = au_deploy.OnSchemaBreak.Fail, + template_values: au_deploy.TemplateValueMapping | None = None, + create_args: au_deploy.ABICreateCallArgs + | au_deploy.ABICreateCallArgsDict + | au_deploy.DeployCreateCallArgs + | None = None, + update_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, + delete_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, + ) -> au_deploy.DeployResponse: + """Deploy an application and update client to reference it. + + Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator + account, including deploy-time template placeholder substitutions. + To understand the architecture decisions behind this functionality please see + + + ```{note} + If there is a breaking state schema change to an existing app (and `on_schema_break` is set to + 'ReplaceApp' the existing app will be deleted and re-created. + ``` + + ```{note} + If there is an update (different TEAL code) to an existing app (and `on_update` is set to 'ReplaceApp') + the existing app will be deleted and re-created. + ``` + + :param str version: version to use when creating or updating app, if None version will be auto incremented + :param algosdk.atomic_transaction_composer.TransactionSigner signer: signer to use when deploying app + , if None uses self.signer + :param str sender: sender address to use when deploying app, if None uses self.sender + :param bool allow_delete: Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app + can be deleted + :param bool allow_update: Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app + can be updated + :param OnUpdate on_update: Determines what action to take if an application update is required + :param OnSchemaBreak on_schema_break: Determines what action to take if an application schema requirements + has increased beyond the current allocation + :param dict[str, int|str|bytes] template_values: Values to use for `TMPL_*` template variables, dictionary keys + should *NOT* include the TMPL_ prefix + :param ABICreateCallArgs create_args: Arguments used when creating an application + :param ABICallArgs | ABICallArgsDict update_args: Arguments used when updating an application + :param ABICallArgs | ABICallArgsDict delete_args: Arguments used when deleting an application + :return DeployResponse: details action taken and relevant transactions + :raises DeploymentError: If the deployment failed + """ + # check inputs + if self.app_id: + raise au_deploy.DeploymentFailedError( + f"Attempt to deploy app which already has an app index of {self.app_id}" + ) + try: + resolved_signer, resolved_sender = self.resolve_signer_sender(signer, sender) + except ValueError as ex: + raise au_deploy.DeploymentFailedError(f"{ex}, unable to deploy app") from None + if not self._creator: + raise au_deploy.DeploymentFailedError("No creator provided, unable to deploy app") + if self._creator != resolved_sender: + raise au_deploy.DeploymentFailedError( + f"Attempt to deploy contract with a sender address {resolved_sender} that differs " + f"from the given creator address for this application client: {self._creator}" + ) + + # make a copy and prepare variables + template_values = {**self.template_values, **(template_values or {})} + au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) + + existing_app_metadata_or_reference = self._load_app_reference() + + self._approval_program, self._clear_program = substitute_template_and_compile( + self.algod_client, self.app_spec, template_values + ) + + if config.debug and config.project_root: + from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps + + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" + ), + PersistSourceMapInput( + compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" + ), + ], + project_root=config.project_root, + client=self.algod_client, + with_sources=True, + ) + + deployer = au_deploy.Deployer( + app_client=self, + creator=self._creator, + signer=resolved_signer, + sender=resolved_sender, + new_app_metadata=self._get_app_deploy_metadata(version, allow_update, allow_delete), + existing_app_metadata_or_reference=existing_app_metadata_or_reference, + on_update=on_update, + on_schema_break=on_schema_break, + create_args=create_args, + update_args=update_args, + delete_args=delete_args, + ) + + return deployer.deploy() + + def compose_create( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" + approval_program, clear_program = self._check_is_compiled() + transaction_parameters = _convert_transaction_parameters(transaction_parameters) + + extra_pages = transaction_parameters.extra_pages or num_extra_program_pages( + approval_program.raw_binary, clear_program.raw_binary + ) + + self.add_method_call( + atc, + app_id=0, + abi_method=call_abi_method, + abi_args=abi_kwargs, + on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC, + call_config=au_spec.CallConfig.CREATE, + parameters=transaction_parameters, + approval_program=approval_program.raw_binary, + clear_program=clear_program.raw_binary, + global_schema=self.app_spec.global_state_schema, + local_schema=self.app_spec.local_state_schema, + extra_pages=extra_pages, + ) + + @overload + def create( + self, + call_abi_method: Literal[False], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def create( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def create( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def create( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" + + atc = AtomicTransactionComposer() + + self.compose_create( + atc, + call_abi_method, + transaction_parameters, + **abi_kwargs, + ) + create_result = self._execute_atc_tr(atc) + self.app_id = au_deploy.get_app_id_from_tx_id(self.algod_client, create_result.tx_id) + return create_result + + def compose_update( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=UpdateApplication to atc""" + approval_program, clear_program = self._check_is_compiled() + + self.add_method_call( + atc=atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.UpdateApplicationOC, + approval_program=approval_program.raw_binary, + clear_program=clear_program.raw_binary, + ) + + @overload + def update( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def update( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def update( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def update( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=UpdateApplication""" + + atc = AtomicTransactionComposer() + self.compose_update( + atc, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_delete( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=DeleteApplication to atc""" + + self.add_method_call( + atc, + call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.DeleteApplicationOC, + ) + + @overload + def delete( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def delete( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def delete( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def delete( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=DeleteApplication""" + + atc = AtomicTransactionComposer() + self.compose_delete( + atc, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_call( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with specified parameters to atc""" + _parameters = _convert_transaction_parameters(transaction_parameters) + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=_parameters, + on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC, + ) + + @overload + def call( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def call( + self, + call_abi_method: Literal[False], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def call( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def call( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with specified parameters""" + atc = AtomicTransactionComposer() + _parameters = _convert_transaction_parameters(transaction_parameters) + self.compose_call( + atc, + call_abi_method=call_abi_method, + transaction_parameters=_parameters, + **abi_kwargs, + ) + + method = self._resolve_method( + call_abi_method, abi_kwargs, _parameters.on_complete or transaction.OnComplete.NoOpOC + ) + if method: + hints = self._method_hints(method) + if hints and hints.read_only: + if config.debug and config.project_root and config.trace_all: + from algokit_utils._debugging import simulate_and_persist_response + + simulate_and_persist_response( + atc, config.project_root, self.algod_client, config.trace_buffer_size_mb + ) + + return self._simulate_readonly_call(method, atc) + + return self._execute_atc_tr(atc) + + def compose_opt_in( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=OptIn to atc""" + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.OptInOC, + ) + + @overload + def opt_in( + self, + call_abi_method: ABIMethod | Literal[True] = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def opt_in( + self, + call_abi_method: Literal[False] = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + ) -> TransactionResponse: ... + + @overload + def opt_in( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def opt_in( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=OptIn""" + atc = AtomicTransactionComposer() + self.compose_opt_in( + atc, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_close_out( + self, + atc: AtomicTransactionComposer, + /, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> None: + """Adds a signed transaction with on_complete=CloseOut to ac""" + self.add_method_call( + atc, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.CloseOutOC, + ) + + @overload + def close_out( + self, + call_abi_method: ABIMethod | Literal[True], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: ... + + @overload + def close_out( + self, + call_abi_method: Literal[False], + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + ) -> TransactionResponse: ... + + @overload + def close_out( + self, + call_abi_method: ABIMethod | bool | None = ..., + transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: ... + + def close_out( + self, + call_abi_method: ABIMethod | bool | None = None, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + """Submits a signed transaction with on_complete=CloseOut""" + atc = AtomicTransactionComposer() + self.compose_close_out( + atc, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, + ) + return self._execute_atc_tr(atc) + + def compose_clear_state( + self, + atc: AtomicTransactionComposer, + /, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + app_args: list[bytes] | None = None, + ) -> None: + """Adds a signed transaction with on_complete=ClearState to atc""" + return self.add_method_call( + atc, + parameters=transaction_parameters, + on_complete=transaction.OnComplete.ClearStateOC, + app_args=app_args, + ) + + def clear_state( + self, + transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, + app_args: list[bytes] | None = None, + ) -> TransactionResponse: + """Submits a signed transaction with on_complete=ClearState""" + atc = AtomicTransactionComposer() + self.compose_clear_state( + atc, + transaction_parameters=transaction_parameters, + app_args=app_args, + ) + return self._execute_atc_tr(atc) + + def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: + """Gets the global state info associated with app_id""" + global_state = self.algod_client.application_info(self.app_id) + assert isinstance(global_state, dict) + return cast( + dict[bytes | str, bytes | str | int], + _decode_state(global_state.get("params", {}).get("global-state", {}), raw=raw), + ) + + def get_local_state(self, account: str | None = None, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: + """Gets the local state info for associated app_id and account/sender""" + + if account is None: + _, account = self.resolve_signer_sender(self.signer, self.sender) + + acct_state = self.algod_client.account_application_info(account, self.app_id) + assert isinstance(acct_state, dict) + return cast( + dict[bytes | str, bytes | str | int], + _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), + ) + + def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes: + """Resolves the default value for an ABI method, based on app_spec""" + + def _data_check(value: object) -> int | str | bytes: + if isinstance(value, int | str | bytes): + return value + raise ValueError(f"Unexpected type for constant data: {value}") + + match to_resolve: + case {"source": "constant", "data": data}: + return _data_check(data) + case {"source": "global-state", "data": str() as key}: + global_state = self.get_global_state(raw=True) + return global_state[key.encode()] + case {"source": "local-state", "data": str() as key}: + _, sender = self.resolve_signer_sender(self.signer, self.sender) + acct_state = self.get_local_state(sender, raw=True) + return acct_state[key.encode()] + case {"source": "abi-method", "data": dict() as method_dict}: + method = Method.undictify(method_dict) + response = self.call(method) + assert isinstance(response, ABITransactionResponse) + return _data_check(response.return_value) + + case {"source": source}: + raise ValueError(f"Unrecognized default argument source: {source}") + case _: + raise TypeError("Unable to interpret default argument specification") + + def _get_app_deploy_metadata( + self, version: str | None, allow_update: bool | None, allow_delete: bool | None + ) -> au_deploy.AppDeployMetaData: + updatable = ( + allow_update + if allow_update is not None + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC + ) + ) + deletable = ( + allow_delete + if allow_delete is not None + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC + ) + ) + + app = self._load_app_reference() + + if version is None: + if app.app_id == 0: + version = "v1.0" + else: + assert isinstance(app, au_deploy.AppDeployMetaData) + version = get_next_version(app.version) + return au_deploy.AppDeployMetaData(self.app_name, version, updatable=updatable, deletable=deletable) + + def _check_is_compiled(self) -> tuple[Program, Program]: + if self._approval_program is None or self._clear_program is None: + self._approval_program, self._clear_program = substitute_template_and_compile( + self.algod_client, self.app_spec, self.template_values + ) + + if config.debug and config.project_root: + from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps + + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" + ), + PersistSourceMapInput( + compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" + ), + ], + project_root=config.project_root, + client=self.algod_client, + with_sources=True, + ) + + return self._approval_program, self._clear_program + + def _simulate_readonly_call( + self, method: Method, atc: AtomicTransactionComposer + ) -> ABITransactionResponse | TransactionResponse: + from algokit_utils._debugging import simulate_response + + response = simulate_response(atc, self.algod_client) + traces = None + if config.debug: + traces = _create_simulate_traces(response) + if response.failure_message: + raise _try_convert_to_logic_error( + response.failure_message, + self.app_spec.approval_program, + self._get_approval_source_map, + traces, + ) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}") + + return TransactionResponse.from_atr(response) + + def _load_reference_and_check_app_id(self) -> None: + self._load_app_reference() + self._check_app_id() + + def _load_app_reference(self) -> au_deploy.AppReference | au_deploy.AppMetaData: + if not self.existing_deployments and self._creator: + assert self._indexer_client + self.existing_deployments = au_deploy.get_creator_apps(self._indexer_client, self._creator) + + if self.existing_deployments: + app = self.existing_deployments.apps.get(self.app_name) + if app: + if self.app_id == 0: + self.app_id = app.app_id + return app + + return au_deploy.AppReference(self.app_id, self.app_address) + + def _check_app_id(self) -> None: + if self.app_id == 0: + raise Exception( + "ApplicationClient is not associated with an app instance, to resolve either:\n" + "1.provide an app_id on construction OR\n" + "2.provide a creator address so an app can be searched for OR\n" + "3.create an app first using create or deploy methods" + ) + + def _resolve_method( + self, + abi_method: ABIMethod | bool | None, + args: ABIArgsDict | None, + on_complete: transaction.OnComplete, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, + ) -> Method | None: + matches: list[Method | None] = [] + match abi_method: + case str() | Method(): # abi method specified + return self._resolve_abi_method(abi_method) + case bool() | None: # find abi method + has_bare_config = ( + call_config in au_deploy.get_call_config(self.app_spec.bare_call_config, on_complete) + or on_complete == transaction.OnComplete.ClearStateOC + ) + abi_methods = self._find_abi_methods(args, on_complete, call_config) + if abi_method is not False: + matches += abi_methods + if has_bare_config and abi_method is not True: + matches += [None] + case _: + return abi_method.method_spec() + + if len(matches) == 1: # exact match + return matches[0] + elif len(matches) > 1: # ambiguous match + signatures = ", ".join((m.get_signature() if isinstance(m, Method) else "bare") for m in matches) + raise Exception( + f"Could not find an exact method to use for {on_complete.name} with call_config of {call_config.name}, " + f"specify the exact method using abi_method and args parameters, considered: {signatures}" + ) + else: # no match + raise Exception( + f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" + ) + + def _get_approval_source_map(self) -> SourceMap | None: + if self.approval_source_map: + return self.approval_source_map + + try: + approval, _ = self._check_is_compiled() + except au_deploy.DeploymentFailedError: + return None + return approval.source_map + + def export_source_map(self) -> str | None: + """Export approval source map to JSON, can be later re-imported with `import_source_map`""" + source_map = self._get_approval_source_map() + if source_map: + return json.dumps( + { + "version": source_map.version, + "sources": source_map.sources, + "mappings": source_map.mappings, + } + ) + return None + + def import_source_map(self, source_map_json: str) -> None: + """Import approval source from JSON exported by `export_source_map`""" + source_map = json.loads(source_map_json) + self._approval_source_map = SourceMap(source_map) + + def add_method_call( # noqa: PLR0913 + self, + atc: AtomicTransactionComposer, + abi_method: ABIMethod | bool | None = None, + *, + abi_args: ABIArgsDict | None = None, + app_id: int | None = None, + parameters: TransactionParameters | TransactionParametersDict | None = None, + on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, + local_schema: transaction.StateSchema | None = None, + global_schema: transaction.StateSchema | None = None, + approval_program: bytes | None = None, + clear_program: bytes | None = None, + extra_pages: int | None = None, + app_args: list[bytes] | None = None, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, + ) -> None: + """Adds a transaction to the AtomicTransactionComposer passed""" + if app_id is None: + self._load_reference_and_check_app_id() + app_id = self.app_id + parameters = _convert_transaction_parameters(parameters) + method = self._resolve_method(abi_method, abi_args, on_complete, call_config) + sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() + signer, sender = self.resolve_signer_sender(parameters.signer, parameters.sender) + if parameters.boxes is not None: + # TODO: algosdk actually does this, but it's type hints say otherwise... + encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in parameters.boxes] + else: + encoded_boxes = None + + encoded_lease = parameters.lease.encode("utf-8") if isinstance(parameters.lease, str) else parameters.lease + + if not method: # not an abi method, treat as a regular call + if abi_args: + raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}") + atc.add_transaction( + TransactionWithSigner( + txn=transaction.ApplicationCallTxn( + sender=sender, + sp=sp, + index=app_id, + on_complete=on_complete, + approval_program=approval_program, + clear_program=clear_program, + global_schema=global_schema, + local_schema=local_schema, + extra_pages=extra_pages, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, + boxes=encoded_boxes, + note=parameters.note, + lease=encoded_lease, + rekey_to=parameters.rekey_to, + app_args=app_args, + ), + signer=signer, + ) + ) + return + # resolve ABI method args + args = self._get_abi_method_args(abi_args, method) + atc.add_method_call( + app_id, + method, + sender, + sp, + signer, + method_args=args, + on_complete=on_complete, + local_schema=local_schema, + global_schema=global_schema, + approval_program=approval_program, + clear_program=clear_program, + extra_pages=extra_pages or 0, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, + boxes=encoded_boxes, + note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, + lease=encoded_lease, + rekey_to=parameters.rekey_to, + ) + + def _get_abi_method_args(self, abi_args: ABIArgsDict | None, method: Method) -> list: + args: list = [] + hints = self._method_hints(method) + # copy args so we don't mutate original + abi_args = dict(abi_args or {}) + for method_arg in method.args: + name = method_arg.name + if name in abi_args: + argument = abi_args.pop(name) + if isinstance(argument, dict): + if hints.structs is None or name not in hints.structs: + raise Exception(f"Argument missing struct hint: {name}. Check argument name and type") + + elements = hints.structs[name]["elements"] + + argument_tuple = tuple(argument[field_name] for field_name, field_type in elements) + args.append(argument_tuple) + else: + args.append(argument) + + elif hints.default_arguments is not None and name in hints.default_arguments: + default_arg = hints.default_arguments[name] + if default_arg is not None: + args.append(self.resolve(default_arg)) + else: + raise Exception(f"Unspecified argument: {name}") + if abi_args: + raise Exception(f"Unused arguments specified: {', '.join(abi_args)}") + return args + + def _method_matches( + self, + method: Method, + args: ABIArgsDict | None, + on_complete: transaction.OnComplete, + call_config: au_spec.CallConfig, + ) -> bool: + hints = self._method_hints(method) + if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): + return False + method_args = {m.name for m in method.args} + provided_args = set(args or {}) | set(hints.default_arguments) + + # TODO: also match on types? + return method_args == provided_args + + def _find_abi_methods( + self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: au_spec.CallConfig + ) -> list[Method]: + return [ + method + for method in self.app_spec.contract.methods + if self._method_matches(method, args, on_complete, call_config) + ] + + def _resolve_abi_method(self, method: ABIMethod) -> Method: + if isinstance(method, str): + try: + return next(iter(m for m in self.app_spec.contract.methods if m.get_signature() == method)) + except StopIteration: + pass + return self.app_spec.contract.get_method_by_name(method) + elif hasattr(method, "method_spec"): + return method.method_spec() + else: + return method + + def _method_hints(self, method: Method) -> au_spec.MethodHints: + sig = method.get_signature() + if sig not in self.app_spec.hints: + return au_spec.MethodHints() + return self.app_spec.hints[sig] + + def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: + result = self.execute_atc(atc) + return TransactionResponse.from_atr(result) + + def execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: + return execute_atc_with_logic_error( + atc, + self.algod_client, + approval_program=self.app_spec.approval_program, + approval_source_map=self._get_approval_source_map, + ) + + def get_signer_sender( + self, signer: TransactionSigner | None = None, sender: str | None = None + ) -> tuple[TransactionSigner | None, str | None]: + """Return signer and sender, using default values on client if not specified + + Will use provided values if given, otherwise will fall back to values defined on client. + If no sender is specified then will attempt to obtain sender from signer""" + resolved_signer = signer or self.signer + resolved_sender = sender or get_sender_from_signer(signer) or self.sender or get_sender_from_signer(self.signer) + return resolved_signer, resolved_sender + + def resolve_signer_sender( + self, signer: TransactionSigner | None = None, sender: str | None = None + ) -> tuple[TransactionSigner, str]: + """Return signer and sender, using default values on client if not specified + + Will use provided values if given, otherwise will fall back to values defined on client. + If no sender is specified then will attempt to obtain sender from signer + + :raises ValueError: Raised if a signer or sender is not provided. See `get_signer_sender` + for variant with no exception""" + resolved_signer, resolved_sender = self.get_signer_sender(signer, sender) + if not resolved_signer: + raise ValueError("No signer provided") + if not resolved_sender: + raise ValueError("No sender provided") + return resolved_signer, resolved_sender + + # TODO: remove private implementation, kept in the 1.0.2 release to not impact existing beaker 1.0 installs + _resolve_signer_sender = resolve_signer_sender + + +def substitute_template_and_compile( + algod_client: AlgodClient, + app_spec: au_spec.ApplicationSpecification, + template_values: au_deploy.TemplateValueMapping, +) -> tuple[Program, Program]: + """Substitutes the provided template_values into app_spec and compiles""" + template_values = dict(template_values or {}) + clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values) + + au_deploy.check_template_variables(app_spec.approval_program, template_values) + approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values) + + approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client) + + return approval_app, clear_app + + +def get_next_version(current_version: str) -> str: + """Calculates the next version from `current_version` + + Next version is calculated by finding a semver like + version string and incrementing the lower. This function is used by {py:meth}`ApplicationClient.deploy` when + a version is not specified, and is intended mostly for convenience during local development. + + :params str current_version: An existing version string with a semver like version contained within it, + some valid inputs and incremented outputs: + `1` -> `2` + `1.0` -> `1.1` + `v1.1` -> `v1.2` + `v1.1-beta1` -> `v1.2-beta1` + `v1.2.3.4567` -> `v1.2.3.4568` + `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` + :raises DeploymentFailedError: If `current_version` cannot be parsed""" + pattern = re.compile(r"(?P\w*)(?P(?:\d+\.)*\d+)(?P\w*)") + match = pattern.match(current_version) + if match: + version = match.group("version") + new_version = _increment_version(version) + + def replacement(m: re.Match) -> str: + return f"{m.group('prefix')}{new_version}{m.group('suffix')}" + + return re.sub(pattern, replacement, current_version) + raise au_deploy.DeploymentFailedError( + f"Could not auto increment {current_version}, please specify the next version using the version parameter" + ) + + +def _try_convert_to_logic_error( + source_ex: Exception | str, + approval_program: str, + approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, + simulate_traces: list[SimulationTrace] | None = None, +) -> Exception | None: + source_ex_str = str(source_ex) + logic_error_data = parse_logic_error(source_ex_str) + if logic_error_data: + return LogicError( + logic_error_str=source_ex_str, + logic_error=source_ex if isinstance(source_ex, Exception) else None, + program=approval_program, + source_map=approval_source_map() if callable(approval_source_map) else approval_source_map, + **logic_error_data, + traces=simulate_traces, + ) + + return None + + +@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, + approval_program: str, + wait_rounds: int = 4, + approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, +) -> AtomicTransactionResponse: + """Calls {py:meth}`AtomicTransactionComposer.execute` on provided `atc`, but will parse any errors + and raise a {py:class}`LogicError` if possible + + ```{note} + `approval_program` and `approval_source_map` are required to be able to parse any errors into a + {py:class}`LogicError` + ``` + """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response + + try: + if config.debug and config.project_root and config.trace_all: + simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb) + + return atc.execute(algod_client, wait_rounds=wait_rounds) + except Exception as ex: + if config.debug: + simulate = None + if config.project_root and not config.trace_all: + # if trace_all is enabled, we already have the traces executed above + # hence we only need to simulate if trace_all is disabled and + # project_root is set + simulate = simulate_and_persist_response( + atc, config.project_root, algod_client, config.trace_buffer_size_mb + ) + else: + simulate = simulate_response(atc, algod_client) + traces = _create_simulate_traces(simulate) + else: + traces = None + logger.info("An error occurred while executing the transaction.") + logger.info("To see more details, enable debug mode by setting config.debug = True ") + + logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces) + if logic_error: + raise logic_error from ex + raise ex + + +def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: + traces = [] + if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget_added = txn_group.get("app-budget-added", None) + app_budget_consumed = txn_group.get("app-budget-consumed", None) + failure_message = txn_group.get("failure-message", None) + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + traces.append( + SimulationTrace( + app_budget_added=app_budget_added, + app_budget_consumed=app_budget_consumed, + failure_message=failure_message, + exec_trace=exec_trace, + ) + ) + return traces + + +def _convert_transaction_parameters( + args: TransactionParameters | TransactionParametersDict | None, +) -> CreateCallParameters: + _args = args.__dict__ if isinstance(args, TransactionParameters) else (args or {}) + return CreateCallParameters(**_args) + + +def get_sender_from_signer(signer: TransactionSigner | None) -> str | None: + """Returns the associated address of a signer, return None if no address found""" + + if isinstance(signer, AccountTransactionSigner): + sender = address_from_private_key(signer.private_key) + assert isinstance(sender, str) + return sender + elif isinstance(signer, MultisigTransactionSigner): + sender = signer.msig.address() + assert isinstance(sender, str) + return sender + elif isinstance(signer, LogicSigTransactionSigner): + return signer.lsig.address() + return None + + +# TEMPORARY, use SDK one when available +def _parse_result( + methods: dict[int, Method], + txns: list[dict[str, Any]], + txids: list[str], +) -> list[ABIResult]: + method_results = [] + for i, tx_info in enumerate(txns): + raw_value = b"" + return_value = None + decode_error = None + + if i not in methods: + continue + + # Parse log for ABI method return value + try: + if methods[i].returns.type == Returns.VOID: + method_results.append( + ABIResult( + tx_id=txids[i], + raw_value=raw_value, + return_value=return_value, + decode_error=decode_error, + tx_info=tx_info, + method=methods[i], + ) + ) + continue + + logs = tx_info.get("logs", []) + + # Look for the last returned value in the log + if not logs: + raise Exception("No logs") + + result = logs[-1] + # Check that the first four bytes is the hash of "return" + result_bytes = base64.b64decode(result) + if len(result_bytes) < len(ABI_RETURN_HASH) or result_bytes[: len(ABI_RETURN_HASH)] != ABI_RETURN_HASH: + raise Exception("no logs") + + raw_value = result_bytes[4:] + abi_return_type = methods[i].returns.type + if isinstance(abi_return_type, ABIType): + return_value = abi_return_type.decode(raw_value) + else: + return_value = raw_value + + except Exception as e: + decode_error = e + + method_results.append( + ABIResult( + tx_id=txids[i], + raw_value=raw_value, + return_value=return_value, + decode_error=decode_error, + tx_info=tx_info, + method=methods[i], + ) + ) + + return method_results + + +def _increment_version(version: str) -> str: + split = list(map(int, version.split("."))) + split[-1] = split[-1] + 1 + return ".".join(str(x) for x in split) + + +def _str_or_hex(v: bytes) -> str: + decoded: str + try: + decoded = v.decode("utf-8") + except UnicodeDecodeError: + decoded = v.hex() + + return decoded + + +def _decode_state(state: list[dict[str, Any]], *, raw: bool = False) -> dict[str | bytes, bytes | str | int | None]: + decoded_state: dict[str | bytes, bytes | str | int | None] = {} + + for state_value in state: + raw_key = base64.b64decode(state_value["key"]) + + key: str | bytes = raw_key if raw else _str_or_hex(raw_key) + val: str | bytes | int | None + + action = state_value["value"]["action"] if "action" in state_value["value"] else state_value["value"]["type"] + + match action: + case 1: + raw_val = base64.b64decode(state_value["value"]["bytes"]) + val = raw_val if raw else _str_or_hex(raw_val) + case 2: + val = state_value["value"]["uint"] + case 3: + val = None + case _: + raise NotImplementedError + + decoded_state[key] = val + return decoded_state diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py new file mode 100644 index 00000000..93001f82 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -0,0 +1,21 @@ +from algokit_utils.applications.app_spec.arc32 import ( + AppSpecStateDict, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) +from algokit_utils.applications.app_spec.arc32 import Arc32Contract as ApplicationSpecification + +__all__ = [ + "AppSpecStateDict", + "ApplicationSpecification", + "CallConfig", + "DefaultArgumentDict", + "DefaultArgumentType", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", +] diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py new file mode 100644 index 00000000..b16b266b --- /dev/null +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -0,0 +1,168 @@ +import logging +from enum import Enum, auto + +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner +from algosdk.constants import TX_GROUP_LIMIT +from algosdk.transaction import AssetTransferTxn +from algosdk.v2client.algod import AlgodClient +from typing_extensions import deprecated + +from algokit_utils._legacy_v2.models import Account + +__all__ = ["opt_in", "opt_out"] +logger = logging.getLogger(__name__) + + +class ValidationType(Enum): + OPTIN = auto() + OPTOUT = auto() + + +def _ensure_account_is_valid(algod_client: "AlgodClient", account: Account) -> None: + try: + algod_client.account_info(account.address) + except Exception as err: + error_message = f"Account address{account.address} does not exist" + logger.debug(error_message) + raise err + + +def _ensure_asset_balance_conditions( + algod_client: "AlgodClient", account: Account, asset_ids: list, validation_type: ValidationType +) -> None: + invalid_asset_ids = [] + account_info = algod_client.account_info(account.address) + account_assets = account_info.get("assets", []) # type: ignore # noqa: PGH003 + for asset_id in asset_ids: + asset_exists_in_account_info = any(asset["asset-id"] == asset_id for asset in account_assets) + if validation_type == ValidationType.OPTIN: + if asset_exists_in_account_info: + logger.debug(f"Asset {asset_id} is already opted in for account {account.address}") + invalid_asset_ids.append(asset_id) + + elif validation_type == ValidationType.OPTOUT: + if not account_assets or not asset_exists_in_account_info: + logger.debug(f"Account {account.address} does not have asset {asset_id}") + invalid_asset_ids.append(asset_id) + else: + asset_balance = next((asset["amount"] for asset in account_assets if asset["asset-id"] == asset_id), 0) + if asset_balance != 0: + logger.debug(f"Asset {asset_id} balance is not zero") + invalid_asset_ids.append(asset_id) + + if len(invalid_asset_ids) > 0: + action = "opted out" if validation_type == ValidationType.OPTOUT else "opted in" + condition_message = ( + "their amount is zero and that the account has" + if validation_type == ValidationType.OPTOUT + else "they are valid and that the account has not" + ) + + error_message = ( + f"Assets {invalid_asset_ids} cannot be {action}. Ensure that " + f"{condition_message} previously opted into them." + ) + raise ValueError(error_message) + + +@deprecated( + "Use TransactionComposer.add_asset_opt_in() or AlgorandClient.asset.bulk_opt_in() instead. " + "Example: composer.add_asset_opt_in(AssetOptInParams(sender=account.address, asset_id=123))" +) +def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: + """ + Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, + it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases + its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). + + :param algod_client: An instance of the AlgodClient class from the algosdk library. + :param account: An instance of the Account class representing the account that wants to opt-in to the assets. + :param asset_ids: A list of integers representing the asset IDs to opt-in to. + :return: A dictionary where the keys are the asset IDs and the values are the transaction IDs for opting-in to each asset. + :rtype: dict[int, str] + """ + _ensure_account_is_valid(algod_client, account) + _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN) + suggested_params = algod_client.suggested_params() + result = {} + for i in range(0, len(asset_ids), TX_GROUP_LIMIT): + atc = AtomicTransactionComposer() + chunk = asset_ids[i : i + TX_GROUP_LIMIT] + for asset_id in chunk: + asset = algod_client.asset_info(asset_id) + xfer_txn = AssetTransferTxn( + sp=suggested_params, + sender=account.address, + receiver=account.address, + close_assets_to=None, + revocation_target=None, + amt=0, + note=f"opt in asset id ${asset_id}", + index=asset["index"], # type: ignore # noqa: PGH003 + rekey_to=None, + ) + + transaction_with_signer = TransactionWithSigner( + txn=xfer_txn, + signer=account.signer, + ) + atc.add_transaction(transaction_with_signer) + atc.execute(algod_client, 4) + + for index, asset_id in enumerate(chunk): + result[asset_id] = atc.tx_ids[index] + + return result + + +@deprecated( + "Use TransactionComposer.add_asset_opt_out() or AlgorandClient.asset.bulk_opt_out() instead. " + "Example: composer.add_asset_opt_out(AssetOptOutParams(sender=account.address, asset_id=123, creator=creator_address))" +) +def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: + """ + Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. + The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) + The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. + + It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero. + + :param AlgodClient algod_client: An instance of the AlgodClient class from the algosdk library. + :param Account account: An instance of the Account class representing the account that wants to opt-out from the assets. + :param list[int] asset_ids: A list of integers representing the asset IDs to opt-out from. + :return dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of + the executed transactions. + """ + _ensure_account_is_valid(algod_client, account) + _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT) + suggested_params = algod_client.suggested_params() + result = {} + for i in range(0, len(asset_ids), TX_GROUP_LIMIT): + atc = AtomicTransactionComposer() + chunk = asset_ids[i : i + TX_GROUP_LIMIT] + for asset_id in chunk: + asset = algod_client.asset_info(asset_id) + asset_creator = asset["params"]["creator"] # type: ignore # noqa: PGH003 + xfer_txn = AssetTransferTxn( + sp=suggested_params, + sender=account.address, + receiver=account.address, + close_assets_to=asset_creator, + revocation_target=None, + amt=0, + note=f"opt out asset id ${asset_id}", + index=asset["index"], # type: ignore # noqa: PGH003 + rekey_to=None, + ) + + transaction_with_signer = TransactionWithSigner( + txn=xfer_txn, + signer=account.signer, + ) + atc.add_transaction(transaction_with_signer) + atc.execute(algod_client, 4) + + for index, asset_id in enumerate(chunk): + result[asset_id] = atc.tx_ids[index] + + return result diff --git a/src/algokit_utils/_legacy_v2/common.py b/src/algokit_utils/_legacy_v2/common.py new file mode 100644 index 00000000..65051a60 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/common.py @@ -0,0 +1,28 @@ +""" +This module contains common classes and methods that are reused in more than one file. +""" + +import base64 +import typing + +from algosdk.source_map import SourceMap + +from algokit_utils._legacy_v2.deploy import strip_comments + +if typing.TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + + +class Program: + """A compiled TEAL program + + :param program: The TEAL program source code + :param client: The AlgodClient instance to use for compiling the program + """ + + def __init__(self, program: str, client: "AlgodClient"): + self.teal = program + result: dict = client.compile(strip_comments(self.teal), source_map=True) + self.raw_binary = base64.b64decode(result["result"]) + self.binary_hash: str = result["hash"] + self.source_map = SourceMap(result["sourcemap"]) diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py new file mode 100644 index 00000000..91d6eb14 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -0,0 +1,822 @@ +import base64 +import dataclasses +import json +import logging +import re +from collections.abc import Iterable, Mapping, Sequence +from typing import TYPE_CHECKING, TypeAlias, TypedDict + +import algosdk +from algosdk import transaction +from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner +from algosdk.transaction import StateSchema +from typing_extensions import deprecated + +from algokit_utils._legacy_v2.application_specification import ( + ApplicationSpecification, + CallConfig, + MethodConfigDict, + OnCompleteActionName, +) +from algokit_utils._legacy_v2.models import ( + ABIArgsDict, + ABIMethod, + Account, + CreateCallParameters, + TransactionResponse, +) +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed + +if TYPE_CHECKING: + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.indexer import IndexerClient + + from algokit_utils._legacy_v2.application_client import ApplicationClient + + +__all__ = [ + "DELETABLE_TEMPLATE_NAME", + "NOTE_PREFIX", + "UPDATABLE_TEMPLATE_NAME", + "ABICallArgs", + "ABICallArgsDict", + "ABICreateCallArgs", + "ABICreateCallArgsDict", + "AppDeployMetaData", + "AppLookup", + "AppMetaData", + "AppReference", + "DeployCallArgs", + "DeployCallArgsDict", + "DeployCreateCallArgs", + "DeployCreateCallArgsDict", + "DeployResponse", + "Deployer", + "DeploymentFailedError", + "OnSchemaBreak", + "OnUpdate", + "OperationPerformed", + "TemplateValueDict", + "TemplateValueMapping", + "get_app_id_from_tx_id", + "get_creator_apps", + "replace_template_variables", +] + +logger = logging.getLogger(__name__) + +DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT = 1000 +_UPDATABLE = "UPDATABLE" +_DELETABLE = "DELETABLE" +UPDATABLE_TEMPLATE_NAME = f"TMPL_{_UPDATABLE}" +"""Template variable name used to control if a smart contract is updatable or not at deployment""" +DELETABLE_TEMPLATE_NAME = f"TMPL_{_DELETABLE}" +"""Template variable name used to control if a smart contract is deletable or not at deployment""" +_TOKEN_PATTERN = re.compile(r"TMPL_[A-Z_]+") +TemplateValue: TypeAlias = int | str | bytes +TemplateValueDict: TypeAlias = dict[str, TemplateValue] +"""Dictionary of `dict[str, int | str | bytes]` representing template variable names and values""" +TemplateValueMapping: TypeAlias = Mapping[str, TemplateValue] +"""Mapping of `str` to `int | str | bytes` representing template variable names and values""" + +NOTE_PREFIX = "ALGOKIT_DEPLOYER:j" +"""ARC-0002 compliant note prefix for algokit_utils deployed applications""" +# This prefix is also used to filter for parsable transaction notes in get_creator_apps. +# However, as the note is base64 encoded first we need to consider it's base64 representation. +# When base64 encoding bytes, 3 bytes are stored in every 4 characters. +# So then we don't need to worry about the padding/changing characters of the prefix if it was followed by +# additional characters, assert the NOTE_PREFIX length is a multiple of 3. +assert len(NOTE_PREFIX) % 3 == 0 + + +class DeploymentFailedError(Exception): + pass + + +@dataclasses.dataclass +class AppReference: + """Information about an Algorand app""" + + app_id: int + app_address: str + + +@dataclasses.dataclass +class AppDeployMetaData: + """Metadata about an application stored in a transaction note during creation. + + The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field + as part of {py:meth}`ApplicationClient.deploy` + """ + + name: str + version: str + deletable: bool | None + updatable: bool | None + + @staticmethod + def from_json(value: str) -> "AppDeployMetaData": + json_value: dict = json.loads(value) + json_value.setdefault("deletable", None) + json_value.setdefault("updatable", None) + return AppDeployMetaData(**json_value) + + @classmethod + def from_b64(cls: type["AppDeployMetaData"], b64: str) -> "AppDeployMetaData": + return cls.decode(base64.b64decode(b64)) + + @classmethod + def decode(cls: type["AppDeployMetaData"], value: bytes) -> "AppDeployMetaData": + note = value.decode("utf-8") + assert note.startswith(NOTE_PREFIX) + return cls.from_json(note[len(NOTE_PREFIX) :]) + + def encode(self) -> bytes: + json_str = json.dumps(self.__dict__) + return f"{NOTE_PREFIX}{json_str}".encode() + + +@dataclasses.dataclass +class AppMetaData(AppReference, AppDeployMetaData): + """Metadata about a deployed app""" + + created_round: int + updated_round: int + created_metadata: AppDeployMetaData + deleted: bool + + +@dataclasses.dataclass +class AppLookup: + """Cache of {py:class}`AppMetaData` for a specific `creator` + + Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple + apps or discovering multiple app_ids + """ + + creator: str + apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) + + +def _sort_by_round(txn: dict) -> tuple[int, int]: + confirmed = txn["confirmed-round"] + offset = txn["intra-round-offset"] + return confirmed, offset + + +def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: + if not metadata_b64: + return None + # noinspection PyBroadException + try: + return AppDeployMetaData.from_b64(metadata_b64) + except Exception: + return None + + +@deprecated("Use algorand.appDeployer.get_creator_apps_by_name() instead. ") +def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: + """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified + creator that have a transaction note containing {py:class}`AppDeployMetaData` + """ + apps: dict[str, AppMetaData] = {} + + creator_address = creator_account if isinstance(creator_account, str) else creator_account.address + token = None + # TODO: paginated indexer call instead of N + 1 calls + while True: + response = indexer.lookup_account_application_by_creator( + creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token + ) + if "message" in response: # an error occurred + raise Exception(f"Error querying applications for {creator_address}: {response}") + for app in response["applications"]: + app_id = app["id"] + app_created_at_round = app["created-at-round"] + app_deleted = app.get("deleted", False) + search_transactions_response = indexer.search_transactions( + min_round=app_created_at_round, + txn_type="appl", + application_id=app_id, + address=creator_address, + address_role="sender", + note_prefix=NOTE_PREFIX.encode("utf-8"), + ) + transactions: list[dict] = search_transactions_response["transactions"] + if not transactions: + continue + + created_transaction = next( + t + for t in transactions + if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address + ) + + transactions.sort(key=_sort_by_round, reverse=True) + latest_transaction = transactions[0] + app_updated_at_round = latest_transaction["confirmed-round"] + + create_metadata = _parse_note(created_transaction.get("note")) + update_metadata = _parse_note(latest_transaction.get("note")) + + if create_metadata and create_metadata.name: + apps[create_metadata.name] = AppMetaData( + app_id=app_id, + app_address=algosdk.logic.get_application_address(app_id), + created_metadata=create_metadata, + created_round=app_created_at_round, + **(update_metadata or create_metadata).__dict__, + updated_round=app_updated_at_round, + deleted=app_deleted, + ) + + token = response.get("next-token") + if not token: + break + + return AppLookup(creator_address, apps) + + +def _state_schema(schema: dict[str, int]) -> StateSchema: + return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) + + +def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: + if to_schema.num_uints > from_schema.num_uints: + yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}" + if to_schema.num_byte_slices > from_schema.num_byte_slices: + yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}" + + +@dataclasses.dataclass(kw_only=True) +class AppChanges: + app_updated: bool + schema_breaking_change: bool + schema_change_description: str | None + + +@deprecated("The algokit_utils.AppDeployer now handles checking for app changes implicitly as part of `deploy` method") +def check_for_app_changes( + algod_client: "AlgodClient", + *, + new_approval: bytes, + new_clear: bytes, + new_global_schema: StateSchema, + new_local_schema: StateSchema, + app_id: int, +) -> AppChanges: + application_info = algod_client.application_info(app_id) + assert isinstance(application_info, dict) + application_create_params = application_info["params"] + + current_approval = base64.b64decode(application_create_params["approval-program"]) + current_clear = base64.b64decode(application_create_params["clear-state-program"]) + current_global_schema = _state_schema(application_create_params["global-state-schema"]) + current_local_schema = _state_schema(application_create_params["local-state-schema"]) + + app_updated = current_approval != new_approval or current_clear != new_clear + + schema_changes: list[str] = [] + schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema)) + schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema)) + + return AppChanges( + app_updated=app_updated, + schema_breaking_change=bool(schema_changes), + schema_change_description=", ".join(schema_changes), + ) + + +def _is_valid_token_character(char: str) -> bool: + return char.isalnum() or char == "_" + + +def add_deploy_template_variables( + template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None +) -> None: + if allow_update is not None: + template_values[_UPDATABLE] = int(allow_update) + if allow_delete is not None: + template_values[_DELETABLE] = int(allow_delete) + + +def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. + Returns None if not found""" + + if end < 0: + end = len(line) + idx = start + in_quotes = in_base64 = False + while idx < end: + current_char = line[idx] + match current_char: + # enter base64 + case " " | "(" if not in_quotes and _last_token_base64(line, idx): + in_base64 = True + # exit base64 + case " " | ")" if not in_quotes and in_base64: + in_base64 = False + # escaped char + case "\\" if in_quotes: + # skip next character + idx += 1 + # quote boundary + case '"': + in_quotes = not in_quotes + # can test for match + case _ if not in_quotes and not in_base64 and line.startswith(token, idx): + # only match if not in quotes and string matches + return idx + idx += 1 + return None + + +def _last_token_base64(line: str, idx: int) -> bool: + try: + *_, last = line[:idx].split() + except ValueError: + return False + return last in ("base64", "b64") + + +def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. + Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING + Returns None if not found""" + if end < 0: + end = len(line) + + idx = start + while idx < end: + token_idx = _find_unquoted_string(line, token, idx, end) + if token_idx is None: + break + trailing_idx = token_idx + len(token) + if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start + trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end + ): + return token_idx + idx = trailing_idx + return None + + +def _strip_comment(line: str) -> str: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + return line + return line[:comment_idx].rstrip() + + +def strip_comments(program: str) -> str: + return "\n".join(_strip_comment(line) for line in program.splitlines()) + + +def _has_token(program_without_comments: str, token: str) -> bool: + for line in program_without_comments.splitlines(): + token_idx = _find_template_token(line, token) + if token_idx is not None: + return True + return False + + +def _find_tokens(stripped_approval_program: str) -> list[str]: + return _TOKEN_PATTERN.findall(stripped_approval_program) + + +def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: + approval_program = strip_comments(approval_program) + if _has_token(approval_program, UPDATABLE_TEMPLATE_NAME) and _UPDATABLE not in template_values: + raise DeploymentFailedError( + "allow_update must be specified if deploy time configuration of update is being used" + ) + if _has_token(approval_program, DELETABLE_TEMPLATE_NAME) and _DELETABLE not in template_values: + raise DeploymentFailedError( + "allow_delete must be specified if deploy time configuration of delete is being used" + ) + all_tokens = _find_tokens(approval_program) + missing_values = [token for token in all_tokens if token[len("TMPL_") :] not in template_values] + if missing_values: + raise DeploymentFailedError(f"The following template values were not provided: {', '.join(missing_values)}") + + for template_variable_name in template_values: + tmpl_variable = f"TMPL_{template_variable_name}" + if not _has_token(approval_program, tmpl_variable): + if template_variable_name == _UPDATABLE: + raise DeploymentFailedError( + "allow_update must only be specified if deploy time configuration of update is being used" + ) + if template_variable_name == _DELETABLE: + raise DeploymentFailedError( + "allow_delete must only be specified if deploy time configuration of delete is being used" + ) + logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") + + +@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` + + ```{note} + `template_values` keys should *NOT* be prefixed with `TMPL_` + ``` + """ + return AppManager.replace_template_variables(program, template_values) + + +def has_template_vars(app_spec: ApplicationSpecification) -> bool: + return "TMPL_" in strip_comments(app_spec.approval_program) or "TMPL_" in strip_comments(app_spec.clear_program) + + +def get_deploy_control( + app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete +) -> bool | None: + if template_var not in strip_comments(app_spec.approval_program): + return None + return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( + h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER + ) + + +def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: + def get(key: OnCompleteActionName) -> CallConfig: + return method_config.get(key, CallConfig.NEVER) + + match on_complete: + case transaction.OnComplete.NoOpOC: + return get("no_op") + case transaction.OnComplete.UpdateApplicationOC: + return get("update_application") + case transaction.OnComplete.DeleteApplicationOC: + return get("delete_application") + case transaction.OnComplete.OptInOC: + return get("opt_in") + case transaction.OnComplete.CloseOutOC: + return get("close_out") + case transaction.OnComplete.ClearStateOC: + return get("clear_state") + + +@dataclasses.dataclass(kw_only=True) +class DeployResponse: + """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`""" + + app: AppMetaData + create_response: TransactionResponse | None = None + delete_response: TransactionResponse | None = None + update_response: TransactionResponse | None = None + action_taken: OperationPerformed = OperationPerformed.Nothing + + +@dataclasses.dataclass(kw_only=True) +class DeployCallArgs: + """Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + suggested_params: transaction.SuggestedParams | None = None + lease: bytes | str | None = None + accounts: list[str] | None = None + foreign_apps: list[int] | None = None + foreign_assets: list[int] | None = None + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None + rekey_to: str | None = None + + +@dataclasses.dataclass(kw_only=True) +class ABICall: + method: ABIMethod | bool | None = None + args: ABIArgsDict = dataclasses.field(default_factory=dict) + + +@dataclasses.dataclass(kw_only=True) +class DeployCreateCallArgs(DeployCallArgs): + """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + extra_pages: int | None = None + on_complete: transaction.OnComplete | None = None + + +@dataclasses.dataclass(kw_only=True) +class ABICallArgs(DeployCallArgs, ABICall): + """ABI Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + +@dataclasses.dataclass(kw_only=True) +class ABICreateCallArgs(DeployCreateCallArgs, ABICall): + """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + +class DeployCallArgsDict(TypedDict, total=False): + """Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + suggested_params: transaction.SuggestedParams + lease: bytes | str + accounts: list[str] + foreign_apps: list[int] + foreign_assets: list[int] + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] + rekey_to: str + + +class ABICallArgsDict(DeployCallArgsDict, TypedDict, total=False): + """ABI Parameters used to update or delete an application when calling + {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + method: ABIMethod | bool + args: ABIArgsDict + + +class DeployCreateCallArgsDict(DeployCallArgsDict, TypedDict, total=False): + """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + extra_pages: int | None + on_complete: transaction.OnComplete + + +class ABICreateCallArgsDict(DeployCreateCallArgsDict, TypedDict, total=False): + """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" + + method: ABIMethod | bool + args: ABIArgsDict + + +@dataclasses.dataclass(kw_only=True) +class Deployer: + app_client: "ApplicationClient" + creator: str + signer: TransactionSigner + sender: str + existing_app_metadata_or_reference: AppReference | AppMetaData + new_app_metadata: AppDeployMetaData + on_update: OnUpdate + on_schema_break: OnSchemaBreak + create_args: ABICreateCallArgs | ABICreateCallArgsDict | DeployCreateCallArgs | None + update_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None + delete_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None + + def deploy(self) -> DeployResponse: + """Ensures app associated with app client's creator is present and up to date""" + assert self.app_client.approval + assert self.app_client.clear + + if self.existing_app_metadata_or_reference.app_id == 0: + logger.info(f"{self.new_app_metadata.name} not found in {self.creator} account, deploying app.") + return self._create_app() + + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + logger.debug( + f"{self.existing_app_metadata_or_reference.name} found in {self.creator} account, " + f"with app id {self.existing_app_metadata_or_reference.app_id}, " + f"version={self.existing_app_metadata_or_reference.version}." + ) + + app_changes = check_for_app_changes( + self.app_client.algod_client, + new_approval=self.app_client.approval.raw_binary, + new_clear=self.app_client.clear.raw_binary, + new_global_schema=self.app_client.app_spec.global_state_schema, + new_local_schema=self.app_client.app_spec.local_state_schema, + app_id=self.existing_app_metadata_or_reference.app_id, + ) + + if app_changes.schema_breaking_change: + logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") + return self._deploy_breaking_change() + + if app_changes.app_updated: + logger.info(f"Detected a TEAL update in app id {self.existing_app_metadata_or_reference.app_id}") + return self._deploy_update() + + logger.info("No detected changes in app, nothing to do.") + return DeployResponse(app=self.existing_app_metadata_or_reference) + + def _deploy_breaking_change(self) -> DeployResponse: + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + if self.on_schema_break == OnSchemaBreak.Fail: + raise DeploymentFailedError( + "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" + ) + if self.on_schema_break == OnSchemaBreak.AppendApp: + logger.info("Schema break detected and on_schema_break=AppendApp, will attempt to create new app") + return self._create_app() + + if self.existing_app_metadata_or_reference.deletable: + logger.info( + "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" + ) + elif self.existing_app_metadata_or_reference.deletable is False: + logger.warning( + "App is not deletable but on_schema_break=ReplaceApp, " + "will attempt to delete app, delete will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is deletable but on_schema_break=ReplaceApp, will attempt to delete app" + ) + return self._create_and_delete_app() + + def _deploy_update(self) -> DeployResponse: + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + if self.on_update == OnUpdate.Fail: + raise DeploymentFailedError( + "Update detected and on_update=Fail, stopping deployment. " + "If you want to try updating the app then re-run with on_update=UpdateApp" + ) + if self.on_update == OnUpdate.AppendApp: + logger.info("Update detected and on_update=AppendApp, will attempt to create new app") + return self._create_app() + elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.UpdateApp: + logger.info("App is updatable and on_update=UpdateApp, will update app") + return self._update_app() + elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.ReplaceApp: + logger.warning( + "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" + ) + return self._create_and_delete_app() + elif self.on_update == OnUpdate.ReplaceApp: + if self.existing_app_metadata_or_reference.updatable is False: + logger.warning( + "App is not updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + return self._create_and_delete_app() + else: + if self.existing_app_metadata_or_reference.updatable is False: + logger.warning( + "App is not updatable but on_update=UpdateApp, " + "will attempt to update app, update will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=UpdateApp, will attempt to update app" + ) + return self._update_app() + + def _create_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + + method, abi_args, parameters = _convert_deploy_args( + self.create_args, self.new_app_metadata, self.signer, self.sender + ) + create_response = self.app_client.create( + method, + parameters, + **abi_args, + ) + logger.info( + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " + f"with app id {self.app_client.app_id}." + ) + assert create_response.confirmed_round is not None + app_metadata = _create_metadata(self.new_app_metadata, self.app_client.app_id, create_response.confirmed_round) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + return DeployResponse(app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create) + + def _create_and_delete_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + + logger.info( + f"Replacing {self.existing_app_metadata_or_reference.name} " + f"({self.existing_app_metadata_or_reference.version}) with " + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) in {self.creator} account." + ) + atc = AtomicTransactionComposer() + create_method, create_abi_args, create_parameters = _convert_deploy_args( + self.create_args, self.new_app_metadata, self.signer, self.sender + ) + self.app_client.compose_create( + atc, + create_method, + create_parameters, + **create_abi_args, + ) + create_txn_index = len(atc.txn_list) - 1 + delete_method, delete_abi_args, delete_parameters = _convert_deploy_args( + self.delete_args, self.new_app_metadata, self.signer, self.sender + ) + self.app_client.compose_delete( + atc, + delete_method, + delete_parameters, + **delete_abi_args, + ) + delete_txn_index = len(atc.txn_list) - 1 + create_delete_response = self.app_client.execute_atc(atc) + create_response = TransactionResponse.from_atr(create_delete_response, create_txn_index) + delete_response = TransactionResponse.from_atr(create_delete_response, delete_txn_index) + self.app_client.app_id = get_app_id_from_tx_id(self.app_client.algod_client, create_response.tx_id) + logger.info( + f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " + f"with app id {self.app_client.app_id}." + ) + logger.info( + f"{self.existing_app_metadata_or_reference.name} " + f"({self.existing_app_metadata_or_reference.version}) with app id " + f"{self.existing_app_metadata_or_reference.app_id}, deleted successfully." + ) + + app_metadata = _create_metadata( + self.new_app_metadata, self.app_client.app_id, create_delete_response.confirmed_round + ) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + + return DeployResponse( + app=app_metadata, + create_response=create_response, + delete_response=delete_response, + action_taken=OperationPerformed.Replace, + ) + + def _update_app(self) -> DeployResponse: + assert self.app_client.existing_deployments + assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) + + logger.info( + f"Updating {self.existing_app_metadata_or_reference.name} to {self.new_app_metadata.version} in " + f"{self.creator} account, with app id {self.existing_app_metadata_or_reference.app_id}" + ) + method, abi_args, parameters = _convert_deploy_args( + self.update_args, self.new_app_metadata, self.signer, self.sender + ) + update_response = self.app_client.update( + method, + parameters, + **abi_args, + ) + app_metadata = _create_metadata( + self.new_app_metadata, + self.app_client.app_id, + self.existing_app_metadata_or_reference.created_round, + updated_round=update_response.confirmed_round, + original_metadata=self.existing_app_metadata_or_reference.created_metadata, + ) + self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata + return DeployResponse(app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update) + + +def _create_metadata( + app_spec_note: AppDeployMetaData, + app_id: int, + created_round: int, + updated_round: int | None = None, + original_metadata: AppDeployMetaData | None = None, +) -> AppMetaData: + return AppMetaData( + app_id=app_id, + app_address=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, + name=app_spec_note.name, + version=app_spec_note.version, + deletable=app_spec_note.deletable, + updatable=app_spec_note.updatable, + deleted=False, + ) + + +def _convert_deploy_args( + _args: DeployCallArgs | DeployCallArgsDict | None, + note: AppDeployMetaData, + signer: TransactionSigner | None, + sender: str | None, +) -> tuple[ABIMethod | bool | None, ABIArgsDict, CreateCallParameters]: + args = _args.__dict__ if isinstance(_args, DeployCallArgs) else dict(_args or {}) + + # return most derived type, unused parameters are ignored + parameters = CreateCallParameters( + note=note.encode(), + signer=signer, + sender=sender, + suggested_params=args.get("suggested_params"), + lease=args.get("lease"), + accounts=args.get("accounts"), + foreign_assets=args.get("foreign_assets"), + foreign_apps=args.get("foreign_apps"), + boxes=args.get("boxes"), + rekey_to=args.get("rekey_to"), + extra_pages=args.get("extra_pages"), + on_complete=args.get("on_complete"), + ) + + return args.get("method"), args.get("args") or {}, parameters + + +def get_app_id_from_tx_id(algod_client: "AlgodClient", tx_id: str) -> int: + """Finds the app_id for provided transaction id""" + result = algod_client.pending_transaction_info(tx_id) + assert isinstance(result, dict) + app_id = result["application-index"] + assert isinstance(app_id, int) + return app_id diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py new file mode 100644 index 00000000..0c171cb7 --- /dev/null +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -0,0 +1,14 @@ +from typing_extensions import deprecated + +from algokit_utils.errors.logic_error import LogicError as NewLogicError +from algokit_utils.errors.logic_error import parse_logic_error + +__all__ = [ + "LogicError", + "parse_logic_error", +] + + +@deprecated("Use algokit_utils.models.error.LogicError instead") +class LogicError(NewLogicError): + pass diff --git a/src/algokit_utils/models.py b/src/algokit_utils/_legacy_v2/models.py similarity index 79% rename from src/algokit_utils/models.py rename to src/algokit_utils/_legacy_v2/models.py index e1030088..da9d129e 100644 --- a/src/algokit_utils/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -2,17 +2,20 @@ from collections.abc import Sequence from typing import Any, Generic, Protocol, TypeAlias, TypedDict, TypeVar -import algosdk.account from algosdk import transaction from algosdk.abi import Method from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, AtomicTransactionResponse, SimulateAtomicTransactionResponse, TransactionSigner, ) -from algosdk.encoding import decode_address -from deprecated import deprecated +from typing_extensions import deprecated + +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.simulate import SimulationTrace + +# Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) + __all__ = [ "ABIArgsDict", @@ -24,6 +27,7 @@ "CreateTransactionParameters", "OnCompleteCallParameters", "OnCompleteCallParametersDict", + "SimulationTrace", "TransactionParameters", "TransactionResponse", ] @@ -31,35 +35,10 @@ ReturnType = TypeVar("ReturnType") +@deprecated("Use 'SigningAccount' instead") @dataclasses.dataclass(kw_only=True) -class Account: - """Holds the private_key and address for an account""" - - private_key: str - """Base64 encoded private key""" - address: str = dataclasses.field(default="") - """Address for this account""" - - def __post_init__(self) -> None: - if not self.address: - self.address = algosdk.account.address_from_private_key(self.private_key) # type: ignore[no-untyped-call] - - @property - def public_key(self) -> bytes: - """The public key for this account""" - public_key = decode_address(self.address) # type: ignore[no-untyped-call] - assert isinstance(public_key, bytes) - return public_key - - @property - def signer(self) -> AccountTransactionSigner: - """An AccountTransactionSigner for this account""" - return AccountTransactionSigner(self.private_key) - - @staticmethod - def new_account() -> "Account": - private_key, address = algosdk.account.generate_account() # type: ignore[no-untyped-call] - return Account(private_key=private_key) +class Account(SigningAccount): + """An account that can be used to sign transactions""" @dataclasses.dataclass(kw_only=True) @@ -202,14 +181,14 @@ class TransactionParametersDict(TypedDict, total=False): """Address to rekey to""" -class OnCompleteCallParametersDict(TypedDict, TransactionParametersDict, total=False): +class OnCompleteCallParametersDict(TransactionParametersDict, total=False): """Additional parameters that can be included in a transaction when using the ApplicationClient.call/compose_call methods""" on_complete: transaction.OnComplete -class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=False): +class CreateCallParametersDict(OnCompleteCallParametersDict, total=False): """Additional parameters that can be included in a transaction when using the ApplicationClient.create/compose_create methods""" @@ -217,24 +196,16 @@ class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=Fa # 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""" - - -@dataclasses.dataclass -class SimulationTrace: - app_budget_added: int | None - app_budget_consumed: int | None - failure_message: str | None - exec_trace: dict[str, object] diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py new file mode 100644 index 00000000..9a5bc9aa --- /dev/null +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -0,0 +1,140 @@ +import dataclasses +import os +from typing import Literal +from urllib import parse + +from algosdk.kmd import KMDClient +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient +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", +] + + +@dataclasses.dataclass +class AlgoClientConfig: + """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or + {py:class}`algosdk.v2client.indexer.IndexerClient`""" + + server: str + """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + token: str + """API Token to authenticate with the service""" + + +@dataclasses.dataclass +class AlgoClientConfigs: + algod_config: AlgoClientConfig + indexer_config: AlgoClientConfig + kmd_config: AlgoClientConfig | None + + +@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.testnet() or AlgorandClient.mainnet() instead") +def get_algonode_config( + network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str +) -> AlgoClientConfig: + client = "api" if config == "algod" else "idx" + return AlgoClientConfig( + server=f"https://{network}-{client}.algonode.cloud", + token=token, + ) + + +@deprecated("Use AlgorandClient.from_environment() instead.") +def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: + """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment + + If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" + config = config or _get_config_from_environment("ALGOD") + headers = {"X-Algo-API-Token": config.token} + return AlgodClient(config.token, config.server, headers) + + +@deprecated("Use AlgorandClient.default_localnet().kmd instead") +def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment + + 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) + + +@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. + + If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`""" + config = config or _get_config_from_environment("INDEXER") + headers = {"X-Indexer-API-Token": config.token} + return IndexerClient(config.token, config.server, headers) + + +@deprecated("Use AlgorandClient.client.is_localnet() instead") +def is_localnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" + params = client.suggested_params() + return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + + +@deprecated("Use AlgorandClient.client.is_mainnet() instead") +def is_mainnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `mainnet-v1`""" + params = client.suggested_params() + return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] + + +@deprecated("Use AlgorandClient.client.is_testnet() instead") +def is_testnet(client: AlgodClient) -> bool: + """Returns True if client genesis is `testnet-v1`""" + params = client.suggested_params() + return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] + + +@deprecated("Use AlgorandClient.default_localnet().kmd instead") +def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: + """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` + + Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, + or 4002 by default""" + # We can only use Kmd on the LocalNet otherwise it's not exposed so this makes some assumptions + # (e.g. same token and server as algod and port 4002 by default) + port = os.getenv("KMD_PORT", "4002") + server = _replace_kmd_port(client.algod_address, port) + return KMDClient(client.algod_token, server) + + +def _replace_kmd_port(address: str, port: str) -> str: + parsed_algod = parse.urlparse(address) + kmd_host = parsed_algod.netloc.split(":", maxsplit=1)[0] + f":{port}" + kmd_parsed = parsed_algod._replace(netloc=kmd_host) + return parse.urlunparse(kmd_parsed) + + +def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: + server = os.getenv(f"{environment_prefix}_SERVER") + if server is None: + raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") + port = os.getenv(f"{environment_prefix}_PORT") + if port: + parsed = parse.urlparse(server) + server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() + return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index a0eb7d53..1a049e5e 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -1,183 +1,12 @@ -import logging -import os -from typing import TYPE_CHECKING, Any - -from algosdk.account import address_from_private_key -from algosdk.mnemonic import from_private_key, to_private_key -from algosdk.util import algos_to_microalgos - -from algokit_utils._transfer import TransferParameters, transfer -from algokit_utils.models import Account -from algokit_utils.network_clients import get_kmd_client_from_algod_client, is_localnet - -if TYPE_CHECKING: - from collections.abc import Callable - - from algosdk.kmd import KMDClient - from algosdk.v2client.algod import AlgodClient - -__all__ = [ - "create_kmd_wallet_account", - "get_account", - "get_account_from_mnemonic", - "get_dispenser_account", - "get_kmd_wallet_account", - "get_localnet_default_account", - "get_or_create_kmd_wallet_account", -] - -logger = logging.getLogger(__name__) -_DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 - - -def get_account_from_mnemonic(mnemonic: str) -> Account: - """Convert a mnemonic (25 word passphrase) into an Account""" - private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call] - address = address_from_private_key(private_key) # type: ignore[no-untyped-call] - return Account(private_key=private_key, address=address) - - -def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: - """Creates a wallet with specified name""" - wallet_id = kmd_client.create_wallet(name, "")["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - kmd_client.generate_key(wallet_handle) - - key_ids: list[str] = kmd_client.list_keys(wallet_handle) - account_key = key_ids[0] - - private_account_key = kmd_client.export_key(wallet_handle, "", account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] - - -def get_or_create_kmd_wallet_account( - client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None -) -> Account: - """Returns a wallet with specified name, or creates one if not found""" - kmd_client = kmd_client or get_kmd_client_from_algod_client(client) - account = get_kmd_wallet_account(client, kmd_client, name) - - if account: - account_info = client.account_info(account.address) - assert isinstance(account_info, dict) - if account_info["amount"] > 0: - return account - logger.debug(f"Found existing account in LocalNet with name '{name}', but no funds in the account.") - else: - account = create_kmd_wallet_account(kmd_client, name) - - logger.debug( - f"Couldn't find existing account in LocalNet with name '{name}'. " - f"So created account {account.address} with keys stored in KMD." - ) - - logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs") - - if fund_with_algos: - transfer( - client, - TransferParameters( - from_account=get_dispenser_account(client), - to_address=account.address, - micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call] - ), - ) - - return account - - -def _is_default_account(account: dict[str, Any]) -> bool: - return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) - - -def get_localnet_default_account(client: "AlgodClient") -> Account: - """Returns the default Account in a LocalNet instance""" - if not is_localnet(client): - raise Exception("Can't get a default account from non LocalNet network") - - account = get_kmd_wallet_account( - client, get_kmd_client_from_algod_client(client), "unencrypted-default-wallet", _is_default_account - ) - assert account - return account - - -def get_dispenser_account(client: "AlgodClient") -> Account: - """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" - if is_localnet(client): - return get_localnet_default_account(client) - return get_account(client, "DISPENSER") - - -def get_kmd_wallet_account( - client: "AlgodClient", - kmd_client: "KMDClient", - name: str, - predicate: "Callable[[dict[str, Any]], bool] | None" = None, -) -> Account | None: - """Returns wallet matching specified name and predicate or None if not found""" - wallets: list[dict] = kmd_client.list_wallets() - - wallet = next((w for w in wallets if w["name"] == name), None) - if wallet is None: - return None - - wallet_id = wallet["id"] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") - key_ids: list[str] = kmd_client.list_keys(wallet_handle) - matched_account_key = None - if predicate: - for key in key_ids: - account = client.account_info(key) - assert isinstance(account, dict) - if predicate(account): - matched_account_key = key - else: - matched_account_key = next(key_ids.__iter__(), None) - - if not matched_account_key: - return None - - private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key) - return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] - - -def get_account( - client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None -) -> Account: - """Returns an Algorand account with private key loaded by convention based on the given name identifier. - - # Convention - - **Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret - Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a - secret storage service rather than the file system. - - **LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will - create it and fund the account for you - - This allows you to write code that will work seamlessly in production and local development (LocalNet) without - manual config locally (including when you reset the LocalNet). - - # Example - If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get - that private key loaded into an account object: - ```python - account = get_account('ACCOUNT', algod) - ``` - - If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account - that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. - """ - - mnemonic_key = f"{name.upper()}_MNEMONIC" - mnemonic = os.getenv(mnemonic_key) - if mnemonic: - return get_account_from_mnemonic(mnemonic) - - if is_localnet(client): - account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) - os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call] - return account - - raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'") +import warnings + +warnings.warn( + """The legacy v2 account module is deprecated and will be removed in a future version. + Use `SigningAccount` abstraction from `algokit_utils.models` instead or + classes compliant with `TransactionSignerAccountProtocol` obtained from `AccountManager`. +""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.account import * # noqa: F403, E402 diff --git a/src/algokit_utils/accounts/__init__.py b/src/algokit_utils/accounts/__init__.py new file mode 100644 index 00000000..87da256b --- /dev/null +++ b/src/algokit_utils/accounts/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.accounts.account_manager import * # noqa: F403 +from algokit_utils.accounts.kmd_account_manager import * # noqa: F403 diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py new file mode 100644 index 00000000..45a04f3f --- /dev/null +++ b/src/algokit_utils/accounts/account_manager.py @@ -0,0 +1,909 @@ +import os +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +import algosdk +from algosdk import mnemonic +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.mnemonic import to_private_key +from algosdk.transaction import LogicSigAccount as AlgosdkLogicSigAccount +from algosdk.transaction import SuggestedParams +from typing_extensions import Self + +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager +from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.clients.dispenser_api_client import DispenserAssetName, TestNetDispenserApiClient +from algokit_utils.config import config +from algokit_utils.models.account import ( + DISPENSER_ACCOUNT_NAME, + LogicSigAccount, + MultiSigAccount, + MultisigMetadata, + SigningAccount, + TransactionSignerAccount, +) +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.account import TransactionSignerAccountProtocol +from algokit_utils.transactions.transaction_composer import ( + PaymentParams, + SendAtomicTransactionComposerResults, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import SendSingleTransactionResult + +logger = config.logger + +__all__ = [ + "AccountInformation", + "AccountManager", + "EnsureFundedFromTestnetDispenserApiResult", + "EnsureFundedResult", +] + + +@dataclass(frozen=True, kw_only=True) +class _CommonEnsureFundedParams: + """ + Common parameters for ensure funded responses. + """ + + transaction_id: str + amount_funded: AlgoAmount + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedResult(SendSingleTransactionResult, _CommonEnsureFundedParams): + """ + Result from performing an ensure funded call. + """ + + +@dataclass(frozen=True, kw_only=True) +class EnsureFundedFromTestnetDispenserApiResult(_CommonEnsureFundedParams): + """ + Result from performing an ensure funded call using TestNet dispenser API. + """ + + +@dataclass(frozen=True, kw_only=True) +class AccountInformation: + """ + Information about an Algorand account's current status, balance and other properties. + + See `https://developer.algorand.org/docs/rest-apis/algod/#account` for detailed field descriptions. + + :ivar str address: The account's address + :ivar AlgoAmount amount: The account's current balance + :ivar AlgoAmount amount_without_pending_rewards: The account's balance without the pending rewards + :ivar AlgoAmount min_balance: The account's minimum required balance + :ivar AlgoAmount pending_rewards: The amount of pending rewards + :ivar AlgoAmount rewards: The amount of rewards earned + :ivar int round: The round for which this information is relevant + :ivar str status: The account's status (e.g., 'Offline', 'Online') + :ivar int|None total_apps_opted_in: Number of applications this account has opted into + :ivar int|None total_assets_opted_in: Number of assets this account has opted into + :ivar int|None total_box_bytes: Total number of box bytes used by this account + :ivar int|None total_boxes: Total number of boxes used by this account + :ivar int|None total_created_apps: Number of applications created by this account + :ivar int|None total_created_assets: Number of assets created by this account + :ivar list[dict]|None apps_local_state: Local state of applications this account has opted into + :ivar int|None apps_total_extra_pages: Number of extra pages allocated to applications + :ivar dict|None apps_total_schema: Total schema for all applications + :ivar list[dict]|None assets: Assets held by this account + :ivar str|None auth_addr: If rekeyed, the authorized address + :ivar int|None closed_at_round: Round when this account was closed + :ivar list[dict]|None created_apps: Applications created by this account + :ivar list[dict]|None created_assets: Assets created by this account + :ivar int|None created_at_round: Round when this account was created + :ivar bool|None deleted: Whether this account is deleted + :ivar bool|None incentive_eligible: Whether this account is eligible for incentives + :ivar int|None last_heartbeat: Last heartbeat round for this account + :ivar int|None last_proposed: Last round this account proposed a block + :ivar dict|None participation: Participation information for this account + :ivar int|None reward_base: Base reward for this account + :ivar str|None sig_type: Signature type for this account + """ + + address: str + amount: AlgoAmount + amount_without_pending_rewards: AlgoAmount + min_balance: AlgoAmount + pending_rewards: AlgoAmount + rewards: AlgoAmount + round: int + status: str + total_apps_opted_in: int | None = None + total_assets_opted_in: int | None = None + total_box_bytes: int | None = None + total_boxes: int | None = None + total_created_apps: int | None = None + total_created_assets: int | None = None + apps_local_state: list[dict] | None = None + apps_total_extra_pages: int | None = None + apps_total_schema: dict | None = None + assets: list[dict] | None = None + auth_addr: str | None = None + closed_at_round: int | None = None + created_apps: list[dict] | None = None + created_assets: list[dict] | None = None + created_at_round: int | None = None + deleted: bool | None = None + incentive_eligible: bool | None = None + last_heartbeat: int | None = None + last_proposed: int | None = None + participation: dict | None = None + reward_base: int | None = None + sig_type: str | None = None + + +class AccountManager: + """ + Creates and keeps track of signing accounts that can sign transactions for a sending address. + + This class provides functionality to create, track, and manage various types of accounts including + mnemonic-based, rekeyed, multisig, and logic signature accounts. + + :param client_manager: The ClientManager client to use for algod and kmd clients + + :example: + >>> account_manager = AccountManager(client_manager) + """ + + def __init__(self, client_manager: ClientManager): + self._client_manager = client_manager + self._kmd_account_manager = KmdAccountManager(client_manager) + self._accounts = dict[str, TransactionSignerAccountProtocol]() + self._default_signer: TransactionSigner | None = None + + @property + def kmd(self) -> KmdAccountManager: + return self._kmd_account_manager + + def set_default_signer(self, signer: TransactionSigner | TransactionSignerAccountProtocol) -> Self: + """ + Sets the default signer to use if no other signer is specified. + + If this isn't set and a transaction needs signing for a given sender + then an error will be thrown from `get_signer` / `get_account`. + + :param signer: A `TransactionSigner` signer to use. + :returns: The `AccountManager` so method calls can be chained + + :example: + >>> signer_account = account_manager.random() + >>> account_manager.set_default_signer(signer_account.signer) + >>> # When signing a transaction, if there is no signer registered for the sender + >>> # then the default signer will be used + >>> signer = account_manager.get_signer("{SENDERADDRESS}") + """ + self._default_signer = signer if isinstance(signer, TransactionSigner) else signer.signer + return self + + def set_signer(self, sender: str, signer: TransactionSigner) -> Self: + """ + Tracks the given `TransactionSigner` against the given sender address for later signing. + + :param sender: The sender address to use this signer for + :param signer: The `TransactionSigner` to sign transactions with for the given sender + :returns: The `AccountManager` instance for method chaining + + :example: + >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) + """ + self._accounts[sender] = TransactionSignerAccount(address=sender, signer=signer) + return self + + def set_signers(self, *, another_account_manager: "AccountManager", overwrite_existing: bool = True) -> Self: + """ + Merges the given `AccountManager` into this one. + + :param another_account_manager: The `AccountManager` to merge into this one + :param overwrite_existing: Whether to overwrite existing signers in this manager + :returns: The `AccountManager` instance for method chaining + """ + self._accounts = ( + {**self._accounts, **another_account_manager._accounts} # noqa: SLF001 + if overwrite_existing + else {**another_account_manager._accounts, **self._accounts} # noqa: SLF001 + ) + return self + + def set_signer_from_account(self, account: TransactionSignerAccountProtocol) -> Self: + """ + Tracks the given account for later signing. + + Note: If you are generating accounts via the various methods on `AccountManager` + (like `random`, `from_mnemonic`, `logic_sig`, etc.) then they automatically get tracked. + + :param account: The account to register + :returns: The `AccountManager` instance for method chaining + + :example: + >>> account_manager = AccountManager(client_manager) + >>> account_manager.set_signer_from_account(SigningAccount.new_account()) + >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) + >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) + """ + self._accounts[account.address] = account + return self + + def get_signer(self, sender: str | TransactionSignerAccountProtocol) -> TransactionSigner: + """ + Returns the `TransactionSigner` for the given sender address. + + If no signer has been registered for that address then the default signer is used if registered. + + :param sender: The sender address or account + :returns: The `TransactionSigner` + :raises ValueError: If no signer is found and no default signer is set + + :example: + >>> signer = account_manager.get_signer("SENDERADDRESS") + """ + signer = self._accounts.get(self._get_address(sender)) or self._default_signer + if not signer: + raise ValueError(f"No signer found for address {sender}") + return signer if isinstance(signer, TransactionSigner) else signer.signer + + def get_account(self, sender: str) -> TransactionSignerAccountProtocol: + """ + Returns the `TransactionSignerAccountProtocol` for the given sender address. + + :param sender: The sender address + :returns: The `TransactionSignerAccountProtocol` + :raises ValueError: If no account is found or if the account is not a regular account + + :example: + >>> sender = account_manager.random() + >>> # ... + >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered + >>> account = account_manager.get_account(sender) + """ + account = self._accounts.get(sender) + if not account: + raise ValueError(f"No account found for address {sender}") + if not isinstance(account, SigningAccount): + raise ValueError(f"Account {sender} is not a regular account") + return account + + def get_information(self, sender: str | TransactionSignerAccountProtocol) -> AccountInformation: + """ + Returns the given sender account's current status, balance and spendable amounts. + + See ``_ + for response data schema details. + + :param sender: The address or account compliant with `TransactionSignerAccountProtocol` protocol to look up + :returns: The account information + + :example: + >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" + >>> account_info = account_manager.get_information(address) + """ + info = self._client_manager.algod.account_info(self._get_address(sender)) + assert isinstance(info, dict) + info = {k.replace("-", "_"): v for k, v in info.items()} + for key, value in info.items(): + if key in ("amount", "amount_without_pending_rewards", "min_balance", "pending_rewards", "rewards"): + info[key] = AlgoAmount.from_micro_algo(value) + return AccountInformation(**info) + + def _register_account(self, private_key: str, address: str | None = None) -> SigningAccount: + """ + Helper method to create and register an account with its signer. + + :param private_key: The private key for the account + :param address: The address for the account + :returns: The registered Account instance + """ + address = address or str(algosdk.account.address_from_private_key(private_key)) + account = SigningAccount(private_key=private_key, address=address) + self._accounts[address or account.address] = TransactionSignerAccount( + address=account.address, signer=account.signer + ) + return account + + def _register_logicsig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + """ + Helper method to create and register a logic signature account. + + :param program: The bytes that make up the compiled logic signature + :param args: The (binary) arguments to pass into the logic signature + :returns: The registered AlgosdkLogicSigAccount instance + """ + logic_sig = LogicSigAccount(AlgosdkLogicSigAccount(program, args)) + self._accounts[logic_sig.address] = logic_sig + return logic_sig + + def _register_multisig(self, metadata: MultisigMetadata, signing_accounts: list[SigningAccount]) -> MultiSigAccount: + """ + Helper method to create and register a multisig account. + + :param metadata: The metadata for the multisig account + :param signing_accounts: The list of accounts that are present to sign + :returns: The registered MultisigAccount instance + """ + msig_account = MultiSigAccount(metadata, signing_accounts) + self._accounts[str(msig_account.address)] = MultiSigAccount(metadata, signing_accounts) + return msig_account + + def from_mnemonic(self, *, mnemonic: str, sender: str | None = None) -> SigningAccount: + """ + Tracks and returns an Algorand account with secret key loaded by taking the mnemonic secret. + + :param mnemonic: The mnemonic secret representing the private key of an account + :param sender: Optional address to use as the sender + :returns: The account + + .. warning:: + Be careful how the mnemonic is handled. Never commit it into source control and ideally load it + from the environment (ideally via a secret storage service) rather than the file system. + + :example: + >>> account = account_manager.from_mnemonic("mnemonic secret ...") + """ + return self._register_account(to_private_key(mnemonic), sender) + + def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> SigningAccount: + """ + Tracks and returns an Algorand account with private key loaded by convention from environment variables. + + This allows you to write code that will work seamlessly in production and local development (LocalNet) + without manual config locally (including when you reset the LocalNet). + + :param name: The name identifier of the account + :param fund_with: Optional amount to fund the account with when it gets created + (when targeting LocalNet) + :returns: The account + :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} + + .. note:: + Convention: + * **Non-LocalNet:** will load `{NAME}_MNEMONIC` as a mnemonic secret. + If `{NAME}_SENDER` is defined then it will use that for the sender address + (i.e. to support rekeyed accounts) + * **LocalNet:** will load the account from a KMD wallet called {NAME} and if that wallet doesn't exist + it will create it and fund the account for you + + :example: + >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: + >>> account = account_manager.from_environment('MY_ACCOUNT') + >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created + >>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser + """ + account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") + + if account_mnemonic: + private_key = mnemonic.to_private_key(account_mnemonic) + return self._register_account(private_key) + + if self._client_manager.is_localnet(): + kmd_account = self._kmd_account_manager.get_or_create_wallet_account(name, fund_with) + return self._register_account(kmd_account.private_key) + + 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, sender: str | None = None + ) -> SigningAccount: + """ + Tracks and returns an Algorand account with private key loaded from the given KMD wallet. + + :param name: The name of the wallet to retrieve an account from + :param predicate: Optional filter to use to find the account + :param sender: Optional sender address to use this signer for (aka a rekeyed account) + :returns: The account + :raises ValueError: If unable to find KMD account with given name and predicate + + :example: + >>> # Get default funded account in a LocalNet: + >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', + ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 + ... ) + """ + kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender) + if not kmd_account: + raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}") + + return self._register_account(kmd_account.private_key) + + def logicsig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigAccount: + """ + Tracks and returns an account that represents a logic signature. + + :param program: The bytes that make up the compiled logic signature + :param args: Optional (binary) arguments to pass into the logic signature + :returns: A logic signature account wrapper + + :example: + >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) + """ + return self._register_logicsig(program, args) + + def multisig(self, metadata: MultisigMetadata, signing_accounts: list[SigningAccount]) -> MultiSigAccount: + """ + Tracks and returns an account that supports partial or full multisig signing. + + :param metadata: The metadata for the multisig account + :param signing_accounts: The signers that are currently present + :returns: A multisig account wrapper + + :example: + >>> account = account_manager.multi_sig( + ... version=1, + ... threshold=1, + ... addrs=["ADDRESS1...", "ADDRESS2..."], + ... signing_accounts=[account1, account2] + ... ) + """ + return self._register_multisig(metadata, signing_accounts) + + def random(self) -> SigningAccount: + """ + Tracks and returns a new, random Algorand account. + + :returns: The account + + :example: + >>> account = account_manager.random() + """ + account = SigningAccount.new_account() + return self._register_account(account.private_key) + + def localnet_dispenser(self) -> SigningAccount: + """ + Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + + This account can be used to fund other accounts. + + :returns: The account + + :example: + >>> account = account_manager.localnet_dispenser() + """ + kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() + return self._register_account(kmd_account.private_key) + + def dispenser_from_environment(self) -> SigningAccount: + """ + Returns an account (with private key loaded) that can act as a dispenser from environment variables. + + If environment variables are not present, returns the default LocalNet dispenser account. + + :returns: The account + + :example: + >>> account = account_manager.dispenser_from_environment() + """ + name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") + if name: + return self.from_environment(DISPENSER_ACCOUNT_NAME) + return self.localnet_dispenser() + + def rekeyed( + self, *, sender: str, account: TransactionSignerAccountProtocol + ) -> TransactionSignerAccount | SigningAccount: + """ + Tracks and returns an Algorand account that is a rekeyed version of the given account to a new sender. + + :param sender: The account or address to use as the sender + :param account: The account to use as the signer for this new rekeyed account + :returns: The rekeyed account + + :example: + >>> account = account.from_mnemonic("mnemonic secret ...") + >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") + """ + sender_address = sender.address if isinstance(sender, SigningAccount) else sender + self._accounts[sender_address] = TransactionSignerAccount(address=sender_address, signer=account.signer) + if isinstance(account, SigningAccount): + return SigningAccount(address=sender_address, private_key=account.private_key) + return TransactionSignerAccount(address=sender_address, signer=account.signer) + + def rekey_account( # noqa: PLR0913 + self, + account: str, + rekey_to: str | TransactionSignerAccountProtocol, + *, # 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. + + :param account: The account to rekey + :param rekey_to: The address or account to rekey to + :param signer: Optional transaction signer + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional max fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round + :param suppress_log: Optional flag to suppress logging + :returns: The result of the transaction and the transaction that was sent + + .. warning:: + Please be careful with this function and be sure to read the + `official rekey guidance `_. + + :example: + >>> # Basic example (with string addresses): + >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) + >>> # Basic example (with signer accounts): + >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) + >>> # Advanced example: + >>> algorand.account.rekey_account({ + ... account: "ACCOUNTADDRESS", + ... rekey_to: "NEWADDRESS", + ... lease: 'lease', + ... note: 'note', + ... first_valid_round: 1000, + ... validity_window: 10, + ... extra_fee: AlgoAmount.from_micro_algo(1000), + ... static_fee: AlgoAmount.from_micro_algo(1000), + ... max_fee: AlgoAmount.from_micro_algo(3000), + ... suppress_log: True, + ... }) + """ + sender_address = self._get_address(account) + rekey_address = self._get_address(rekey_to) + + 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, + ) + ) + .send() + ) + + # If rekey_to is a signing account, set it as the signer for this account + if isinstance(rekey_to, SigningAccount): + self.rekeyed(sender=account, account=rekey_to) + + if not suppress_log: + logger.info(f"Rekeyed {sender_address} to {rekey_address} via transaction {result.tx_ids[-1]}") + + return result + + def ensure_funded( # noqa: PLR0913 + self, + account_to_fund: str | SigningAccount, + dispenser_account: str | SigningAccount, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + # Sender params + send_params: SendParams | 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, + ) -> EnsureFundedResult | None: + """ + Funds a given account using a dispenser account as a funding source. + + Ensures the given account has a certain amount of Algo free to spend (accounting for + Algo locked in minimum balance requirement). + + See ``_ for details. + + :param account_to_fund: The account to fund + :param dispenser_account: The account to use as a dispenser funding source + :param min_spending_balance: The minimum balance of Algo that the account + should have available to spend + :param min_funding_increment: Optional minimum funding increment + :param send_params: Parameters for the send operation, defaults to None + :param signer: Optional transaction signer + :param rekey_to: Optional rekey address + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional maximum fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, + or None if no funds were needed + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded( + ... "ACCOUNTADDRESS", + ... "DISPENSERADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) + """ + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self._get_address(dispenser_account) + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + 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(send_params) + ) + + return EnsureFundedResult( + 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 | SigningAccount, + min_spending_balance: AlgoAmount, + *, # Force remaining params to be keyword-only + min_funding_increment: AlgoAmount | None = None, + # SendParams + send_params: SendParams | 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, + ) -> EnsureFundedResult | None: + """ + Ensure an account is funded from a dispenser account configured in environment. + + Uses a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, + as a funding source such that the given account has a certain amount of Algo free to spend + (accounting for Algo locked in minimum balance requirement). + + See ``_ for details. + + :param account_to_fund: The account to fund + :param min_spending_balance: The minimum balance of Algo that the account should have available to + spend + :param min_funding_increment: Optional minimum funding increment + :param send_params: Parameters for the send operation, defaults to None + :param signer: Optional transaction signer + :param rekey_to: Optional rekey address + :param note: Optional transaction note + :param lease: Optional transaction lease + :param static_fee: Optional static fee + :param extra_fee: Optional extra fee + :param max_fee: Optional maximum fee + :param validity_window: Optional validity window + :param first_valid_round: Optional first valid round + :param last_valid_round: Optional last valid round + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or + None if no funds were needed + + .. note:: + The dispenser account is retrieved from the account mnemonic stored in + process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER + if it's a rekeyed account, or against default LocalNet if no environment variables present. + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_environment( + ... "ACCOUNTADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) + """ + account_to_fund = self._get_address(account_to_fund) + dispenser_account = self.dispenser_from_environment() + + 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(send_params) + ) + + return EnsureFundedResult( + 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_testnet_dispenser_api( + self, + account_to_fund: str | SigningAccount, + dispenser_client: TestNetDispenserApiClient, + min_spending_balance: AlgoAmount, + *, + min_funding_increment: AlgoAmount | None = None, + ) -> EnsureFundedFromTestnetDispenserApiResult | None: + """ + Ensure an account is funded using the TestNet Dispenser API. + + Uses the TestNet Dispenser API as a funding source such that the account has a certain amount + of Algo free to spend (accounting for Algo locked in minimum balance requirement). + + See ``_ for details. + + :param account_to_fund: The account to fund + :param dispenser_client: The TestNet dispenser funding client + :param min_spending_balance: The minimum balance of Algo that the account should have + available to spend + :param min_funding_increment: Optional minimum funding increment + :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or + None if no funds were needed + :raises ValueError: If attempting to fund on non-TestNet network + + :example: + >>> # Basic example: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1) + ... ) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2) + ... ) + """ + account_to_fund = self._get_address(account_to_fund) + + if not self._client_manager.is_testnet(): + raise ValueError("Attempt to fund using TestNet dispenser API on non TestNet network.") + + amount_funded = self._get_ensure_funded_amount(account_to_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = dispenser_client.fund( + address=account_to_fund, + amount=amount_funded.micro_algo, + asset_id=DispenserAssetName.ALGO, + ) + + return EnsureFundedFromTestnetDispenserApiResult( + transaction_id=result.tx_id, + amount_funded=AlgoAmount.from_micro_algo(result.amount), + ) + + def _get_address(self, sender: str | TransactionSignerAccountProtocol) -> str: + match sender: + case TransactionSignerAccountProtocol(): + return sender.address + case str(): + return sender + case _: + raise ValueError(f"Unknown sender type: {type(sender)}") + + def _get_composer(self, get_suggested_params: Callable[[], SuggestedParams] | None = None) -> TransactionComposer: + if get_suggested_params is None: + + def _get_suggested_params() -> SuggestedParams: + return self._client_manager.algod.suggested_params() + + 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: AlgoAmount, + min_funding_increment: int, + ) -> int | None: + if min_spending_balance > current_spending_balance: + min_fund_amount = (min_spending_balance - current_spending_balance).micro_algo + return max(min_fund_amount, min_funding_increment) + return None + + 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 + ) + + 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..ae3c8c7b --- /dev/null +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -0,0 +1,159 @@ +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 SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer + +__all__ = ["KmdAccount", "KmdAccountManager"] + +logger = config.logger + + +class KmdAccount(SigningAccount): + """Account retrieved from KMD with signing capabilities, extending base Account. + + Provides an account implementation that can be used to sign transactions using keys stored in KMD. + + :param private_key: Base64 encoded private key + :param address: Optional address override for rekeyed accounts, defaults to None + """ + + def __init__(self, private_key: str, address: str | None = None) -> None: + super().__init__(private_key=private_key, address=address or "") + + +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: + self._client_manager = client_manager + try: + self._kmd = client_manager.kmd + except ValueError: + self._kmd = None + + def kmd(self) -> KMDClient: + """Returns the KMD client, initializing it if needed. + + :raises Exception: If KMD client is not configured and not running against LocalNet + :return: The KMD client + """ + if self._kmd is None: + if self._client_manager.is_localnet(): + 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. + + Retrieves an account from a KMD wallet that matches the given predicate, or a random account + if no predicate is provided. + + :param wallet_name: The name of the wallet to retrieve an account from + :param predicate: Optional filter to use to find the account (otherwise gets a random account from the wallet) + :param sender: Optional sender address to use this signer for (aka a rekeyed account) + :return: The signing account or None if no matching wallet or account was found + """ + + kmd_client = self.kmd() + wallets = kmd_client.list_wallets() + wallet = next((w for w in wallets if w["name"] == wallet_name), None) + 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. + + Provides idempotent access to accounts from LocalNet without specifying the private key. + + :param name: The name of the wallet to retrieve / create + :param fund_with: The number of Algos to fund the account with when created + :return: An Algorand account with private key loaded + """ + fund_with = fund_with or AlgoAmount.from_algo(1000) + + 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, + ) + ).send() + return account + + def get_localnet_dispenser_account(self) -> KmdAccount: + """Returns an Algorand account with private key loaded for the default LocalNet dispenser account. + + Retrieves the default funded account from LocalNet that can be used to fund other accounts. + + :raises Exception: If not running against LocalNet or dispenser account not found + :return: The default LocalNet dispenser account + """ + if not self._client_manager.is_localnet(): + raise Exception("Can't get LocalNet dispenser account from non LocalNet network") + + 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/algorand.py b/src/algokit_utils/algorand.py new file mode 100644 index 00000000..83a8c56d --- /dev/null +++ b/src/algokit_utils/algorand.py @@ -0,0 +1,265 @@ +import copy +import time + +import typing_extensions +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.kmd import KMDClient +from algosdk.transaction import SuggestedParams +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from algokit_utils.accounts.account_manager import AccountManager +from algokit_utils.applications.app_deployer import AppDeployer +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager +from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig +from algokit_utils.protocols.account import TransactionSignerAccountProtocol +from algokit_utils.transactions.transaction_composer import ( + TransactionComposer, +) +from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator +from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender + +__all__ = [ + "AlgorandClient", +] + + +class AlgorandClient: + """A client that brokers easy access to Algorand functionality.""" + + def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): + 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) + self._transaction_sender = AlgorandClientTransactionSender( + new_group=lambda: self.new_group(), + asset_manager=self._asset_manager, + app_manager=self._app_manager, + algod_client=self._client_manager.algod, + ) + self._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(), + ) + + self._cached_suggested_params: SuggestedParams | None = None + self._cached_suggested_params_expiry: float | None = None + self._cached_suggested_params_timeout: int = 3_000 # three seconds + self._default_validity_window: int | None = None + + def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self: + """ + Sets the default validity window for transactions. + + :param validity_window: The number of rounds between the first and last valid rounds + :return: The `AlgorandClient` so method calls can be chained + """ + self._default_validity_window = validity_window + return self + + def set_default_signer( + self, signer: TransactionSigner | TransactionSignerAccountProtocol + ) -> typing_extensions.Self: + """ + Sets the default signer to use if no other signer is specified. + + :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol` + :return: The `AlgorandClient` so method calls can be chained + """ + self._account_manager.set_default_signer(signer) + return self + + def set_signer(self, sender: str, signer: TransactionSigner) -> typing_extensions.Self: + """ + Tracks the given account for later signing. + + :param sender: The sender address to use this signer for + :param signer: The signer to sign transactions with for the given sender + :return: The `AlgorandClient` so method calls can be chained + """ + self._account_manager.set_signer(sender, signer) + return self + + def set_signer_account(self, signer: TransactionSignerAccountProtocol) -> typing_extensions.Self: + """ + Sets the default signer to use if no other signer is specified. + + :param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccountProtocol` + :return: The `AlgorandClient` so method calls can be chained + """ + self._account_manager.set_default_signer(signer) + return self + + def set_suggested_params( + self, suggested_params: SuggestedParams, until: float | None = None + ) -> typing_extensions.Self: + """ + Sets a cache value to use for suggested params. + + :param suggested_params: The suggested params to use + :param until: A timestamp until which to cache, or if not specified then the timeout is used + :return: The `AlgorandClient` so method calls can be chained + """ + self._cached_suggested_params = suggested_params + self._cached_suggested_params_expiry = until or time.time() + self._cached_suggested_params_timeout + return self + + def set_suggested_params_timeout(self, timeout: int) -> typing_extensions.Self: + """ + Sets the timeout for caching suggested params. + + :param timeout: The timeout in milliseconds + :return: The `AlgorandClient` so method calls can be chained + """ + self._cached_suggested_params_timeout = timeout + return self + + def get_suggested_params(self) -> SuggestedParams: + """Get suggested params for a transaction (either cached or from algod if the cache is stale or empty)""" + if self._cached_suggested_params and ( + self._cached_suggested_params_expiry is None or self._cached_suggested_params_expiry > time.time() + ): + return copy.deepcopy(self._cached_suggested_params) + + self._cached_suggested_params = self._client_manager.algod.suggested_params() + self._cached_suggested_params_expiry = time.time() + self._cached_suggested_params_timeout + + return copy.deepcopy(self._cached_suggested_params) + + def new_group(self) -> TransactionComposer: + """Start a new `TransactionComposer` transaction group""" + return TransactionComposer( + algod=self.client.algod, + get_signer=lambda addr: self.account.get_signer(addr), + get_suggested_params=self.get_suggested_params, + default_validity_window=self._default_validity_window, + ) + + @property + def client(self) -> ClientManager: + """Get clients, including algosdk clients and app clients.""" + return self._client_manager + + @property + def account(self) -> AccountManager: + """Get or create accounts that can sign transactions.""" + return self._account_manager + + @property + def asset(self) -> AssetManager: + """Get or create assets.""" + return self._asset_manager + + @property + 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""" + return self._transaction_sender + + @property + def create_transaction(self) -> AlgorandClientTransactionCreator: + """Methods for building transactions""" + return self._transaction_creator + + @staticmethod + def default_localnet() -> "AlgorandClient": + """ + Returns an `AlgorandClient` pointing at default LocalNet ports and API token. + + :return: The `AlgorandClient` + """ + return AlgorandClient( + AlgoClientConfigs( + algod_config=ClientManager.get_default_localnet_config("algod"), + indexer_config=ClientManager.get_default_localnet_config("indexer"), + kmd_config=ClientManager.get_default_localnet_config("kmd"), + ) + ) + + @staticmethod + def testnet() -> "AlgorandClient": + """ + Returns an `AlgorandClient` pointing at TestNet using AlgoNode. + + :return: The `AlgorandClient` + """ + return AlgorandClient( + AlgoClientConfigs( + algod_config=ClientManager.get_algonode_config("testnet", "algod"), + indexer_config=ClientManager.get_algonode_config("testnet", "indexer"), + kmd_config=None, + ) + ) + + @staticmethod + def mainnet() -> "AlgorandClient": + """ + Returns an `AlgorandClient` pointing at MainNet using AlgoNode. + + :return: The `AlgorandClient` + """ + return AlgorandClient( + AlgoClientConfigs( + algod_config=ClientManager.get_algonode_config("mainnet", "algod"), + indexer_config=ClientManager.get_algonode_config("mainnet", "indexer"), + kmd_config=None, + ) + ) + + @staticmethod + def from_clients( + algod: AlgodClient, indexer: IndexerClient | None = None, kmd: KMDClient | None = None + ) -> "AlgorandClient": + """ + Returns an `AlgorandClient` pointing to the given client(s). + + :param algod: The algod client to use + :param indexer: The indexer client to use + :param kmd: The kmd client to use + :return: The `AlgorandClient` + """ + return AlgorandClient(AlgoSdkClients(algod=algod, indexer=indexer, kmd=kmd)) + + @staticmethod + def from_environment() -> "AlgorandClient": + """ + Returns an `AlgorandClient` loading the configuration from environment variables. + + Retrieve configurations from environment variables when defined or get defaults. + + Expects to be called from a Python environment. + + :return: The `AlgorandClient` + """ + return AlgorandClient(ClientManager.get_config_from_environment_or_localnet()) + + @staticmethod + def from_config( + algod_config: AlgoClientNetworkConfig, + indexer_config: AlgoClientNetworkConfig | None = None, + kmd_config: AlgoClientNetworkConfig | None = None, + ) -> "AlgorandClient": + """ + Returns an `AlgorandClient` from the given config. + + :param algod_config: The config to use for the algod client + :param indexer_config: The config to use for the indexer client + :param kmd_config: The config to use for the kmd client + :return: The `AlgorandClient` + """ + return AlgorandClient( + AlgoClientConfigs(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) + ) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 008ac32f..a81118bd 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -1,1449 +1,11 @@ -import base64 -import copy -import json -import logging -import re -import typing -from math import ceil -from pathlib import Path -from typing import Any, Literal, cast, overload - -import algosdk -from algosdk import transaction -from algosdk.abi import ABIType, Method, Returns -from algosdk.account import address_from_private_key -from algosdk.atomic_transaction_composer import ( - ABI_RETURN_HASH, - ABIResult, - AccountTransactionSigner, - AtomicTransactionComposer, - AtomicTransactionResponse, - LogicSigTransactionSigner, - MultisigTransactionSigner, - SimulateAtomicTransactionResponse, - TransactionSigner, - TransactionWithSigner, -) -from algosdk.constants import APP_PAGE_MAX_SIZE -from algosdk.logic import get_application_address -from algosdk.source_map import SourceMap - -import algokit_utils.application_specification as au_spec -import algokit_utils.deploy as au_deploy -from algokit_utils._debugging import ( - PersistSourceMapInput, - persist_sourcemaps, - simulate_and_persist_response, - simulate_response, +import warnings + +warnings.warn( + """The legacy v2 application_client module is deprecated and will be removed in a future version. + Use `AppClient` abstraction from `algokit_utils.applications` instead. +""", + DeprecationWarning, + stacklevel=2, ) -from algokit_utils.common import Program -from algokit_utils.config import config -from algokit_utils.logic_error import LogicError, parse_logic_error -from algokit_utils.models import ( - ABIArgsDict, - ABIArgType, - ABIMethod, - ABITransactionResponse, - Account, - CreateCallParameters, - CreateCallParametersDict, - OnCompleteCallParameters, - OnCompleteCallParametersDict, - SimulationTrace, - TransactionParameters, - TransactionParametersDict, - TransactionResponse, -) - -if typing.TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient - - -logger = logging.getLogger(__name__) - - -"""A dictionary `dict[str, Any]` representing ABI argument names and values""" - -__all__ = [ - "ApplicationClient", - "execute_atc_with_logic_error", - "get_next_version", - "get_sender_from_signer", - "num_extra_program_pages", -] - -"""Alias for {py:class}`pyteal.ABIReturnSubroutine`, {py:class}`algosdk.abi.method.Method` or a {py:class}`str` -representing an ABI method name or signature""" - - -def num_extra_program_pages(approval: bytes, clear: bytes) -> int: - """Calculate minimum number of extra_pages required for provided approval and clear programs""" - - return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) - - -class ApplicationClient: - """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" - - @overload - def __init__( - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - app_id: int = 0, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - ): ... - - @overload - def __init__( - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - creator: str | Account, - indexer_client: "IndexerClient | None" = None, - existing_deployments: au_deploy.AppLookup | None = None, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - app_name: str | None = None, - ): ... - - def __init__( # noqa: PLR0913 - self, - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification | Path, - *, - app_id: int = 0, - creator: str | Account | None = None, - indexer_client: "IndexerClient | None" = None, - existing_deployments: au_deploy.AppLookup | None = None, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - template_values: au_deploy.TemplateValueMapping | None = None, - app_name: str | None = None, - ): - """ApplicationClient can be created with an app_id to interact with an existing application, alternatively - it can be created with a creator and indexer_client specified to find existing applications by name and creator. - - :param AlgodClient algod_client: AlgoSDK algod client - :param ApplicationSpecification | Path app_spec: An Application Specification or the path to one - :param int app_id: The app_id of an existing application, to instead find the application by creator and name - use the creator and indexer_client parameters - :param str | Account creator: The address or Account of the app creator to resolve the app_id - :param IndexerClient indexer_client: AlgoSDK indexer client, only required if deploying or finding app_id by - creator and app name - :param AppLookup existing_deployments: - :param TransactionSigner | Account signer: Account or signer to use to sign transactions, if not specified and - creator was passed as an Account will use that. - :param str sender: Address to use as the sender for all transactions, will use the address associated with the - signer if not specified. - :param TemplateValueMapping template_values: Values to use for TMPL_* template variables, dictionary keys should - *NOT* include the TMPL_ prefix - :param str | None app_name: Name of application to use when deploying, defaults to name defined on the - Application Specification - """ - self.algod_client = algod_client - self.app_spec = ( - au_spec.ApplicationSpecification.from_json(app_spec.read_text()) if isinstance(app_spec, Path) else app_spec - ) - self._app_name = app_name - self._approval_program: Program | None = None - self._approval_source_map: SourceMap | None = None - self._clear_program: Program | None = None - - self.template_values: au_deploy.TemplateValueMapping = template_values or {} - self.existing_deployments = existing_deployments - self._indexer_client = indexer_client - if creator is not None: - if not self.existing_deployments and not self._indexer_client: - raise Exception( - "If using the creator parameter either existing_deployments or indexer_client must also be provided" - ) - self._creator: str | None = creator.address if isinstance(creator, Account) else creator - if self.existing_deployments and self.existing_deployments.creator != self._creator: - raise Exception( - "Attempt to create application client with invalid existing_deployments against" - f"a different creator ({self.existing_deployments.creator} instead of " - f"expected creator {self._creator}" - ) - self.app_id = 0 - else: - self.app_id = app_id - self._creator = None - - self.signer: TransactionSigner | None - if signer: - self.signer = ( - signer if isinstance(signer, TransactionSigner) else AccountTransactionSigner(signer.private_key) - ) - elif isinstance(creator, Account): - self.signer = AccountTransactionSigner(creator.private_key) - else: - self.signer = None - - self.sender = sender - self.suggested_params = suggested_params - - @property - def app_name(self) -> str: - return self._app_name or self.app_spec.contract.name - - @app_name.setter - def app_name(self, value: str) -> None: - self._app_name = value - - @property - def app_address(self) -> str: - return get_application_address(self.app_id) - - @property - def approval(self) -> Program | None: - return self._approval_program - - @property - def approval_source_map(self) -> SourceMap | None: - if self._approval_source_map: - return self._approval_source_map - if self._approval_program: - return self._approval_program.source_map - return None - - @approval_source_map.setter - def approval_source_map(self, value: SourceMap) -> None: - self._approval_source_map = value - - @property - def clear(self) -> Program | None: - return self._clear_program - - def prepare( - self, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - app_id: int | None = None, - template_values: au_deploy.TemplateValueDict | None = None, - ) -> "ApplicationClient": - """Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided. - Will also substitute provided template_values into the associated app_spec in the copy""" - new_client: ApplicationClient = copy.copy(self) - new_client._prepare( # noqa: SLF001 - new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values - ) - return new_client - - def _prepare( # noqa: PLR0913 - self, - target: "ApplicationClient", - *, - signer: TransactionSigner | Account | None = None, - sender: str | None = None, - app_id: int | None = None, - template_values: au_deploy.TemplateValueDict | None = None, - ) -> None: - target.app_id = self.app_id if app_id is None else app_id - target.signer, target.sender = target.get_signer_sender( - AccountTransactionSigner(signer.private_key) if isinstance(signer, Account) else signer, sender - ) - target.template_values = {**self.template_values, **(template_values or {})} - - def deploy( # noqa: PLR0913 - self, - version: str | None = None, - *, - signer: TransactionSigner | None = None, - sender: str | None = None, - allow_update: bool | None = None, - allow_delete: bool | None = None, - on_update: au_deploy.OnUpdate = au_deploy.OnUpdate.Fail, - on_schema_break: au_deploy.OnSchemaBreak = au_deploy.OnSchemaBreak.Fail, - template_values: au_deploy.TemplateValueMapping | None = None, - create_args: au_deploy.ABICreateCallArgs - | au_deploy.ABICreateCallArgsDict - | au_deploy.DeployCreateCallArgs - | None = None, - update_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, - delete_args: au_deploy.ABICallArgs | au_deploy.ABICallArgsDict | au_deploy.DeployCallArgs | None = None, - ) -> au_deploy.DeployResponse: - """Deploy an application and update client to reference it. - - Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator - account, including deploy-time template placeholder substitutions. - To understand the architecture decisions behind this functionality please see - - - ```{note} - If there is a breaking state schema change to an existing app (and `on_schema_break` is set to - 'ReplaceApp' the existing app will be deleted and re-created. - ``` - - ```{note} - If there is an update (different TEAL code) to an existing app (and `on_update` is set to 'ReplaceApp') - the existing app will be deleted and re-created. - ``` - - :param str version: version to use when creating or updating app, if None version will be auto incremented - :param algosdk.atomic_transaction_composer.TransactionSigner signer: signer to use when deploying app - , if None uses self.signer - :param str sender: sender address to use when deploying app, if None uses self.sender - :param bool allow_delete: Used to set the `TMPL_DELETABLE` template variable to conditionally control if an app - can be deleted - :param bool allow_update: Used to set the `TMPL_UPDATABLE` template variable to conditionally control if an app - can be updated - :param OnUpdate on_update: Determines what action to take if an application update is required - :param OnSchemaBreak on_schema_break: Determines what action to take if an application schema requirements - has increased beyond the current allocation - :param dict[str, int|str|bytes] template_values: Values to use for `TMPL_*` template variables, dictionary keys - should *NOT* include the TMPL_ prefix - :param ABICreateCallArgs create_args: Arguments used when creating an application - :param ABICallArgs | ABICallArgsDict update_args: Arguments used when updating an application - :param ABICallArgs | ABICallArgsDict delete_args: Arguments used when deleting an application - :return DeployResponse: details action taken and relevant transactions - :raises DeploymentError: If the deployment failed - """ - # check inputs - if self.app_id: - raise au_deploy.DeploymentFailedError( - f"Attempt to deploy app which already has an app index of {self.app_id}" - ) - try: - resolved_signer, resolved_sender = self.resolve_signer_sender(signer, sender) - except ValueError as ex: - raise au_deploy.DeploymentFailedError(f"{ex}, unable to deploy app") from None - if not self._creator: - raise au_deploy.DeploymentFailedError("No creator provided, unable to deploy app") - if self._creator != resolved_sender: - raise au_deploy.DeploymentFailedError( - f"Attempt to deploy contract with a sender address {resolved_sender} that differs " - f"from the given creator address for this application client: {self._creator}" - ) - - # make a copy and prepare variables - template_values = {**self.template_values, **(template_values or {})} - au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) - - existing_app_metadata_or_reference = self._load_app_reference() - - self._approval_program, self._clear_program = substitute_template_and_compile( - self.algod_client, self.app_spec, template_values - ) - - if config.debug and config.project_root: - persist_sourcemaps( - sources=[ - PersistSourceMapInput( - compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" - ), - PersistSourceMapInput( - compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" - ), - ], - project_root=config.project_root, - client=self.algod_client, - with_sources=True, - ) - - deployer = au_deploy.Deployer( - app_client=self, - creator=self._creator, - signer=resolved_signer, - sender=resolved_sender, - new_app_metadata=self._get_app_deploy_metadata(version, allow_update, allow_delete), - existing_app_metadata_or_reference=existing_app_metadata_or_reference, - on_update=on_update, - on_schema_break=on_schema_break, - create_args=create_args, - update_args=update_args, - delete_args=delete_args, - ) - - return deployer.deploy() - - def compose_create( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" - approval_program, clear_program = self._check_is_compiled() - transaction_parameters = _convert_transaction_parameters(transaction_parameters) - - extra_pages = transaction_parameters.extra_pages or num_extra_program_pages( - approval_program.raw_binary, clear_program.raw_binary - ) - - self.add_method_call( - atc, - app_id=0, - abi_method=call_abi_method, - abi_args=abi_kwargs, - on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC, - call_config=au_spec.CallConfig.CREATE, - parameters=transaction_parameters, - approval_program=approval_program.raw_binary, - clear_program=clear_program.raw_binary, - global_schema=self.app_spec.global_state_schema, - local_schema=self.app_spec.local_state_schema, - extra_pages=extra_pages, - ) - - @overload - def create( - self, - call_abi_method: Literal[False], - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def create( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def create( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def create( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" - - atc = AtomicTransactionComposer() - - self.compose_create( - atc, - call_abi_method, - transaction_parameters, - **abi_kwargs, - ) - create_result = self._execute_atc_tr(atc) - self.app_id = au_deploy.get_app_id_from_tx_id(self.algod_client, create_result.tx_id) - return create_result - - def compose_update( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=UpdateApplication to atc""" - approval_program, clear_program = self._check_is_compiled() - - self.add_method_call( - atc=atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.UpdateApplicationOC, - approval_program=approval_program.raw_binary, - clear_program=clear_program.raw_binary, - ) - - @overload - def update( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def update( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def update( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def update( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=UpdateApplication""" - - atc = AtomicTransactionComposer() - self.compose_update( - atc, - call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_delete( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=DeleteApplication to atc""" - - self.add_method_call( - atc, - call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.DeleteApplicationOC, - ) - - @overload - def delete( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def delete( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def delete( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def delete( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=DeleteApplication""" - - atc = AtomicTransactionComposer() - self.compose_delete( - atc, - call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_call( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with specified parameters to atc""" - _parameters = _convert_transaction_parameters(transaction_parameters) - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=_parameters, - on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC, - ) - - @overload - def call( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def call( - self, - call_abi_method: Literal[False], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def call( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def call( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with specified parameters""" - atc = AtomicTransactionComposer() - _parameters = _convert_transaction_parameters(transaction_parameters) - self.compose_call( - atc, - call_abi_method=call_abi_method, - transaction_parameters=_parameters, - **abi_kwargs, - ) - - method = self._resolve_method( - call_abi_method, abi_kwargs, _parameters.on_complete or transaction.OnComplete.NoOpOC - ) - if method: - hints = self._method_hints(method) - if hints and hints.read_only: - if config.debug and config.project_root and config.trace_all: - simulate_and_persist_response( - atc, config.project_root, self.algod_client, config.trace_buffer_size_mb - ) - - return self._simulate_readonly_call(method, atc) - - return self._execute_atc_tr(atc) - - def compose_opt_in( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=OptIn to atc""" - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.OptInOC, - ) - - @overload - def opt_in( - self, - call_abi_method: ABIMethod | Literal[True] = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def opt_in( - self, - call_abi_method: Literal[False] = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - ) -> TransactionResponse: ... - - @overload - def opt_in( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def opt_in( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=OptIn""" - atc = AtomicTransactionComposer() - self.compose_opt_in( - atc, - call_abi_method=call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_close_out( - self, - atc: AtomicTransactionComposer, - /, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> None: - """Adds a signed transaction with on_complete=CloseOut to ac""" - self.add_method_call( - atc, - abi_method=call_abi_method, - abi_args=abi_kwargs, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.CloseOutOC, - ) - - @overload - def close_out( - self, - call_abi_method: ABIMethod | Literal[True], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> ABITransactionResponse: ... - - @overload - def close_out( - self, - call_abi_method: Literal[False], - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - ) -> TransactionResponse: ... - - @overload - def close_out( - self, - call_abi_method: ABIMethod | bool | None = ..., - transaction_parameters: TransactionParameters | TransactionParametersDict | None = ..., - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: ... - - def close_out( - self, - call_abi_method: ABIMethod | bool | None = None, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - **abi_kwargs: ABIArgType, - ) -> TransactionResponse | ABITransactionResponse: - """Submits a signed transaction with on_complete=CloseOut""" - atc = AtomicTransactionComposer() - self.compose_close_out( - atc, - call_abi_method=call_abi_method, - transaction_parameters=transaction_parameters, - **abi_kwargs, - ) - return self._execute_atc_tr(atc) - - def compose_clear_state( - self, - atc: AtomicTransactionComposer, - /, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - app_args: list[bytes] | None = None, - ) -> None: - """Adds a signed transaction with on_complete=ClearState to atc""" - return self.add_method_call( - atc, - parameters=transaction_parameters, - on_complete=transaction.OnComplete.ClearStateOC, - app_args=app_args, - ) - - def clear_state( - self, - transaction_parameters: TransactionParameters | TransactionParametersDict | None = None, - app_args: list[bytes] | None = None, - ) -> TransactionResponse: - """Submits a signed transaction with on_complete=ClearState""" - atc = AtomicTransactionComposer() - self.compose_clear_state( - atc, - transaction_parameters=transaction_parameters, - app_args=app_args, - ) - return self._execute_atc_tr(atc) - - def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: - """Gets the global state info associated with app_id""" - global_state = self.algod_client.application_info(self.app_id) - assert isinstance(global_state, dict) - return cast( - dict[bytes | str, bytes | str | int], - _decode_state(global_state.get("params", {}).get("global-state", {}), raw=raw), - ) - - def get_local_state(self, account: str | None = None, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: - """Gets the local state info for associated app_id and account/sender""" - - if account is None: - _, account = self.resolve_signer_sender(self.signer, self.sender) - - acct_state = self.algod_client.account_application_info(account, self.app_id) - assert isinstance(acct_state, dict) - return cast( - dict[bytes | str, bytes | str | int], - _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), - ) - - def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes: - """Resolves the default value for an ABI method, based on app_spec""" - - def _data_check(value: object) -> int | str | bytes: - if isinstance(value, int | str | bytes): - return value - raise ValueError(f"Unexpected type for constant data: {value}") - - match to_resolve: - case {"source": "constant", "data": data}: - return _data_check(data) - case {"source": "global-state", "data": str() as key}: - global_state = self.get_global_state(raw=True) - return global_state[key.encode()] - case {"source": "local-state", "data": str() as key}: - _, sender = self.resolve_signer_sender(self.signer, self.sender) - acct_state = self.get_local_state(sender, raw=True) - return acct_state[key.encode()] - case {"source": "abi-method", "data": dict() as method_dict}: - method = Method.undictify(method_dict) - response = self.call(method) - assert isinstance(response, ABITransactionResponse) - return _data_check(response.return_value) - - case {"source": source}: - raise ValueError(f"Unrecognized default argument source: {source}") - case _: - raise TypeError("Unable to interpret default argument specification") - - def _get_app_deploy_metadata( - self, version: str | None, allow_update: bool | None, allow_delete: bool | None - ) -> au_deploy.AppDeployMetaData: - updatable = ( - allow_update - if allow_update is not None - else au_deploy.get_deploy_control( - self.app_spec, au_deploy.UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC - ) - ) - deletable = ( - allow_delete - if allow_delete is not None - else au_deploy.get_deploy_control( - self.app_spec, au_deploy.DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC - ) - ) - - app = self._load_app_reference() - - if version is None: - if app.app_id == 0: - version = "v1.0" - else: - assert isinstance(app, au_deploy.AppDeployMetaData) - version = get_next_version(app.version) - return au_deploy.AppDeployMetaData(self.app_name, version, updatable=updatable, deletable=deletable) - - def _check_is_compiled(self) -> tuple[Program, Program]: - if self._approval_program is None or self._clear_program is None: - self._approval_program, self._clear_program = substitute_template_and_compile( - self.algod_client, self.app_spec, self.template_values - ) - - if config.debug and config.project_root: - persist_sourcemaps( - sources=[ - PersistSourceMapInput( - compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal" - ), - PersistSourceMapInput( - compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal" - ), - ], - project_root=config.project_root, - client=self.algod_client, - with_sources=True, - ) - - return self._approval_program, self._clear_program - - def _simulate_readonly_call( - self, method: Method, atc: AtomicTransactionComposer - ) -> ABITransactionResponse | TransactionResponse: - response = simulate_response(atc, self.algod_client) - traces = None - if config.debug: - traces = _create_simulate_traces(response) - if response.failure_message: - raise _try_convert_to_logic_error( - response.failure_message, - self.app_spec.approval_program, - self._get_approval_source_map, - traces, - ) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}") - - return TransactionResponse.from_atr(response) - - def _load_reference_and_check_app_id(self) -> None: - self._load_app_reference() - self._check_app_id() - - def _load_app_reference(self) -> au_deploy.AppReference | au_deploy.AppMetaData: - if not self.existing_deployments and self._creator: - assert self._indexer_client - self.existing_deployments = au_deploy.get_creator_apps(self._indexer_client, self._creator) - - if self.existing_deployments: - app = self.existing_deployments.apps.get(self.app_name) - if app: - if self.app_id == 0: - self.app_id = app.app_id - return app - - return au_deploy.AppReference(self.app_id, self.app_address) - - def _check_app_id(self) -> None: - if self.app_id == 0: - raise Exception( - "ApplicationClient is not associated with an app instance, to resolve either:\n" - "1.) provide an app_id on construction OR\n" - "2.) provide a creator address so an app can be searched for OR\n" - "3.) create an app first using create or deploy methods" - ) - - def _resolve_method( - self, - abi_method: ABIMethod | bool | None, - args: ABIArgsDict | None, - on_complete: transaction.OnComplete, - call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, - ) -> Method | None: - matches: list[Method | None] = [] - match abi_method: - case str() | Method(): # abi method specified - return self._resolve_abi_method(abi_method) - case bool() | None: # find abi method - has_bare_config = ( - call_config in au_deploy.get_call_config(self.app_spec.bare_call_config, on_complete) - or on_complete == transaction.OnComplete.ClearStateOC - ) - abi_methods = self._find_abi_methods(args, on_complete, call_config) - if abi_method is not False: - matches += abi_methods - if has_bare_config and abi_method is not True: - matches += [None] - case _: - return abi_method.method_spec() - - if len(matches) == 1: # exact match - return matches[0] - elif len(matches) > 1: # ambiguous match - signatures = ", ".join((m.get_signature() if isinstance(m, Method) else "bare") for m in matches) - raise Exception( - f"Could not find an exact method to use for {on_complete.name} with call_config of {call_config.name}, " - f"specify the exact method using abi_method and args parameters, considered: {signatures}" - ) - else: # no match - raise Exception( - f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" - ) - - def _get_approval_source_map(self) -> SourceMap | None: - if self.approval_source_map: - return self.approval_source_map - - try: - approval, _ = self._check_is_compiled() - except au_deploy.DeploymentFailedError: - return None - return approval.source_map - - def export_source_map(self) -> str | None: - """Export approval source map to JSON, can be later re-imported with `import_source_map`""" - source_map = self._get_approval_source_map() - if source_map: - return json.dumps( - { - "version": source_map.version, - "sources": source_map.sources, - "mappings": source_map.mappings, - } - ) - return None - - def import_source_map(self, source_map_json: str) -> None: - """Import approval source from JSON exported by `export_source_map`""" - source_map = json.loads(source_map_json) - self._approval_source_map = SourceMap(source_map) - - def add_method_call( # noqa: PLR0913 - self, - atc: AtomicTransactionComposer, - abi_method: ABIMethod | bool | None = None, - *, - abi_args: ABIArgsDict | None = None, - app_id: int | None = None, - parameters: TransactionParameters | TransactionParametersDict | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - local_schema: transaction.StateSchema | None = None, - global_schema: transaction.StateSchema | None = None, - approval_program: bytes | None = None, - clear_program: bytes | None = None, - extra_pages: int | None = None, - app_args: list[bytes] | None = None, - call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, - ) -> None: - """Adds a transaction to the AtomicTransactionComposer passed""" - if app_id is None: - self._load_reference_and_check_app_id() - app_id = self.app_id - parameters = _convert_transaction_parameters(parameters) - method = self._resolve_method(abi_method, abi_args, on_complete, call_config) - sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() - signer, sender = self.resolve_signer_sender(parameters.signer, parameters.sender) - if parameters.boxes is not None: - # TODO: algosdk actually does this, but it's type hints say otherwise... - encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in parameters.boxes] - else: - encoded_boxes = None - - encoded_lease = parameters.lease.encode("utf-8") if isinstance(parameters.lease, str) else parameters.lease - - if not method: # not an abi method, treat as a regular call - if abi_args: - raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}") - atc.add_transaction( - TransactionWithSigner( - txn=transaction.ApplicationCallTxn( # type: ignore[no-untyped-call] - sender=sender, - sp=sp, - index=app_id, - on_complete=on_complete, - approval_program=approval_program, - clear_program=clear_program, - global_schema=global_schema, - local_schema=local_schema, - extra_pages=extra_pages, - accounts=parameters.accounts, - foreign_apps=parameters.foreign_apps, - foreign_assets=parameters.foreign_assets, - boxes=encoded_boxes, - note=parameters.note, - lease=encoded_lease, - rekey_to=parameters.rekey_to, - app_args=app_args, - ), - signer=signer, - ) - ) - return - # resolve ABI method args - args = self._get_abi_method_args(abi_args, method) - atc.add_method_call( - app_id, - method, - sender, - sp, - signer, - method_args=args, - on_complete=on_complete, - local_schema=local_schema, - global_schema=global_schema, - approval_program=approval_program, - clear_program=clear_program, - extra_pages=extra_pages or 0, - accounts=parameters.accounts, - foreign_apps=parameters.foreign_apps, - foreign_assets=parameters.foreign_assets, - boxes=encoded_boxes, - note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, - lease=encoded_lease, - rekey_to=parameters.rekey_to, - ) - - def _get_abi_method_args(self, abi_args: ABIArgsDict | None, method: Method) -> list: - args: list = [] - hints = self._method_hints(method) - # copy args so we don't mutate original - abi_args = dict(abi_args or {}) - for method_arg in method.args: - name = method_arg.name - if name in abi_args: - argument = abi_args.pop(name) - if isinstance(argument, dict): - if hints.structs is None or name not in hints.structs: - raise Exception(f"Argument missing struct hint: {name}. Check argument name and type") - - elements = hints.structs[name]["elements"] - - argument_tuple = tuple(argument[field_name] for field_name, field_type in elements) - args.append(argument_tuple) - else: - args.append(argument) - - elif hints.default_arguments is not None and name in hints.default_arguments: - default_arg = hints.default_arguments[name] - if default_arg is not None: - args.append(self.resolve(default_arg)) - else: - raise Exception(f"Unspecified argument: {name}") - if abi_args: - raise Exception(f"Unused arguments specified: {', '.join(abi_args)}") - return args - - def _method_matches( - self, - method: Method, - args: ABIArgsDict | None, - on_complete: transaction.OnComplete, - call_config: au_spec.CallConfig, - ) -> bool: - hints = self._method_hints(method) - if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): - return False - method_args = {m.name for m in method.args} - provided_args = set(args or {}) | set(hints.default_arguments) - - # TODO: also match on types? - return method_args == provided_args - - def _find_abi_methods( - self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: au_spec.CallConfig - ) -> list[Method]: - return [ - method - for method in self.app_spec.contract.methods - if self._method_matches(method, args, on_complete, call_config) - ] - - def _resolve_abi_method(self, method: ABIMethod) -> Method: - if isinstance(method, str): - try: - return next(iter(m for m in self.app_spec.contract.methods if m.get_signature() == method)) - except StopIteration: - pass - return self.app_spec.contract.get_method_by_name(method) - elif hasattr(method, "method_spec"): - return method.method_spec() - else: - return method - - def _method_hints(self, method: Method) -> au_spec.MethodHints: - sig = method.get_signature() - if sig not in self.app_spec.hints: - return au_spec.MethodHints() - return self.app_spec.hints[sig] - - def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: - result = self.execute_atc(atc) - return TransactionResponse.from_atr(result) - - def execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: - return execute_atc_with_logic_error( - atc, - self.algod_client, - approval_program=self.app_spec.approval_program, - approval_source_map=self._get_approval_source_map, - ) - - def get_signer_sender( - self, signer: TransactionSigner | None = None, sender: str | None = None - ) -> tuple[TransactionSigner | None, str | None]: - """Return signer and sender, using default values on client if not specified - - Will use provided values if given, otherwise will fall back to values defined on client. - If no sender is specified then will attempt to obtain sender from signer""" - resolved_signer = signer or self.signer - resolved_sender = sender or get_sender_from_signer(signer) or self.sender or get_sender_from_signer(self.signer) - return resolved_signer, resolved_sender - - def resolve_signer_sender( - self, signer: TransactionSigner | None = None, sender: str | None = None - ) -> tuple[TransactionSigner, str]: - """Return signer and sender, using default values on client if not specified - - Will use provided values if given, otherwise will fall back to values defined on client. - If no sender is specified then will attempt to obtain sender from signer - - :raises ValueError: Raised if a signer or sender is not provided. See `get_signer_sender` - for variant with no exception""" - resolved_signer, resolved_sender = self.get_signer_sender(signer, sender) - if not resolved_signer: - raise ValueError("No signer provided") - if not resolved_sender: - raise ValueError("No sender provided") - return resolved_signer, resolved_sender - - # TODO: remove private implementation, kept in the 1.0.2 release to not impact existing beaker 1.0 installs - _resolve_signer_sender = resolve_signer_sender - - -def substitute_template_and_compile( - algod_client: "AlgodClient", - app_spec: au_spec.ApplicationSpecification, - template_values: au_deploy.TemplateValueMapping, -) -> tuple[Program, Program]: - """Substitutes the provided template_values into app_spec and compiles""" - template_values = dict(template_values or {}) - clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values) - - au_deploy.check_template_variables(app_spec.approval_program, template_values) - approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values) - - approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client) - - return approval_app, clear_app - - -def get_next_version(current_version: str) -> str: - """Calculates the next version from `current_version` - - Next version is calculated by finding a semver like - version string and incrementing the lower. This function is used by {py:meth}`ApplicationClient.deploy` when - a version is not specified, and is intended mostly for convenience during local development. - - :params str current_version: An existing version string with a semver like version contained within it, - some valid inputs and incremented outputs: - `1` -> `2` - `1.0` -> `1.1` - `v1.1` -> `v1.2` - `v1.1-beta1` -> `v1.2-beta1` - `v1.2.3.4567` -> `v1.2.3.4568` - `v1.2.3.4567-alpha` -> `v1.2.3.4568-alpha` - :raises DeploymentFailedError: If `current_version` cannot be parsed""" - pattern = re.compile(r"(?P\w*)(?P(?:\d+\.)*\d+)(?P\w*)") - match = pattern.match(current_version) - if match: - version = match.group("version") - new_version = _increment_version(version) - - def replacement(m: re.Match) -> str: - return f"{m.group('prefix')}{new_version}{m.group('suffix')}" - - return re.sub(pattern, replacement, current_version) - raise au_deploy.DeploymentFailedError( - f"Could not auto increment {current_version}, please specify the next version using the version parameter" - ) - - -def _try_convert_to_logic_error( - source_ex: Exception | str, - approval_program: str, - approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, - simulate_traces: list[SimulationTrace] | None = None, -) -> Exception | None: - source_ex_str = str(source_ex) - logic_error_data = parse_logic_error(source_ex_str) - if logic_error_data: - return LogicError( - logic_error_str=source_ex_str, - logic_error=source_ex if isinstance(source_ex, Exception) else None, - program=approval_program, - source_map=approval_source_map() if callable(approval_source_map) else approval_source_map, - **logic_error_data, - traces=simulate_traces, - ) - - return None - - -def execute_atc_with_logic_error( - atc: AtomicTransactionComposer, - algod_client: "AlgodClient", - approval_program: str, - wait_rounds: int = 4, - approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, -) -> AtomicTransactionResponse: - """Calls {py:meth}`AtomicTransactionComposer.execute` on provided `atc`, but will parse any errors - and raise a {py:class}`LogicError` if possible - - ```{note} - `approval_program` and `approval_source_map` are required to be able to parse any errors into a - {py:class}`LogicError` - ``` - """ - try: - if config.debug and config.project_root and config.trace_all: - simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb) - - return atc.execute(algod_client, wait_rounds=wait_rounds) - except Exception as ex: - if config.debug: - simulate = None - if config.project_root and not config.trace_all: - # if trace_all is enabled, we already have the traces executed above - # hence we only need to simulate if trace_all is disabled and - # project_root is set - simulate = simulate_and_persist_response( - atc, config.project_root, algod_client, config.trace_buffer_size_mb - ) - else: - simulate = simulate_response(atc, algod_client) - traces = _create_simulate_traces(simulate) - else: - traces = None - logger.info("An error occurred while executing the transaction.") - logger.info("To see more details, enable debug mode by setting config.debug = True ") - - logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces) - if logic_error: - raise logic_error from ex - raise ex - - -def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[SimulationTrace]: - traces = [] - if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: - for txn_group in simulate.simulate_response["txn-groups"]: - app_budget_added = txn_group.get("app-budget-added", None) - app_budget_consumed = txn_group.get("app-budget-consumed", None) - failure_message = txn_group.get("failure-message", None) - txn_result = txn_group.get("txn-results", [{}])[0] - exec_trace = txn_result.get("exec-trace", {}) - traces.append( - SimulationTrace( - app_budget_added=app_budget_added, - app_budget_consumed=app_budget_consumed, - failure_message=failure_message, - exec_trace=exec_trace, - ) - ) - return traces - - -def _convert_transaction_parameters( - args: TransactionParameters | TransactionParametersDict | None, -) -> CreateCallParameters: - _args = args.__dict__ if isinstance(args, TransactionParameters) else (args or {}) - return CreateCallParameters(**_args) - - -def get_sender_from_signer(signer: TransactionSigner | None) -> str | None: - """Returns the associated address of a signer, return None if no address found""" - - if isinstance(signer, AccountTransactionSigner): - sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] - assert isinstance(sender, str) - return sender - elif isinstance(signer, MultisigTransactionSigner): - sender = signer.msig.address() # type: ignore[no-untyped-call] - assert isinstance(sender, str) - return sender - elif isinstance(signer, LogicSigTransactionSigner): - return signer.lsig.address() - return None - - -# TEMPORARY, use SDK one when available -def _parse_result( - methods: dict[int, Method], - txns: list[dict[str, Any]], - txids: list[str], -) -> list[ABIResult]: - method_results = [] - for i, tx_info in enumerate(txns): - raw_value = b"" - return_value = None - decode_error = None - - if i not in methods: - continue - - # Parse log for ABI method return value - try: - if methods[i].returns.type == Returns.VOID: - method_results.append( - ABIResult( - tx_id=txids[i], - raw_value=raw_value, - return_value=return_value, - decode_error=decode_error, - tx_info=tx_info, - method=methods[i], - ) - ) - continue - - logs = tx_info.get("logs", []) - - # Look for the last returned value in the log - if not logs: - raise Exception("No logs") - - result = logs[-1] - # Check that the first four bytes is the hash of "return" - result_bytes = base64.b64decode(result) - if len(result_bytes) < len(ABI_RETURN_HASH) or result_bytes[: len(ABI_RETURN_HASH)] != ABI_RETURN_HASH: - raise Exception("no logs") - - raw_value = result_bytes[4:] - abi_return_type = methods[i].returns.type - if isinstance(abi_return_type, ABIType): - return_value = abi_return_type.decode(raw_value) - else: - return_value = raw_value - - except Exception as e: - decode_error = e - - method_results.append( - ABIResult( - tx_id=txids[i], - raw_value=raw_value, - return_value=return_value, - decode_error=decode_error, - tx_info=tx_info, - method=methods[i], - ) - ) - - return method_results - - -def _increment_version(version: str) -> str: - split = list(map(int, version.split("."))) - split[-1] = split[-1] + 1 - return ".".join(str(x) for x in split) - - -def _str_or_hex(v: bytes) -> str: - decoded: str - try: - decoded = v.decode("utf-8") - except UnicodeDecodeError: - decoded = v.hex() - - return decoded - - -def _decode_state(state: list[dict[str, Any]], *, raw: bool = False) -> dict[str | bytes, bytes | str | int | None]: - decoded_state: dict[str | bytes, bytes | str | int | None] = {} - - for state_value in state: - raw_key = base64.b64decode(state_value["key"]) - - key: str | bytes = raw_key if raw else _str_or_hex(raw_key) - val: str | bytes | int | None - - action = state_value["value"]["action"] if "action" in state_value["value"] else state_value["value"]["type"] - - match action: - case 1: - raw_val = base64.b64decode(state_value["value"]["bytes"]) - val = raw_val if raw else _str_or_hex(raw_val) - case 2: - val = state_value["value"]["uint"] - case 3: - val = None - case _: - raise NotImplementedError - decoded_state[key] = val - return decoded_state +from algokit_utils._legacy_v2.application_client import * # noqa: F403, E402 diff --git a/src/algokit_utils/application_specification.py b/src/algokit_utils/application_specification.py index 392fce8d..8b38160d 100644 --- a/src/algokit_utils/application_specification.py +++ b/src/algokit_utils/application_specification.py @@ -1,206 +1,48 @@ -import base64 -import dataclasses -import json -from enum import IntFlag -from pathlib import Path -from typing import Any, Literal, TypeAlias, TypedDict +import warnings + +from deprecated import deprecated + +warnings.warn( + """The legacy v2 application_specification module is deprecated and will be removed in a future version. + Use `from algokit_utils.applications.app_spec.arc32 import ...` to access Arc32 app spec instead. + By default, the ARC52Contract is a recommended app spec to use, serving as a replacement + for legacy 'ApplicationSpecification' class. + To convert legacy app specs to ARC52, use `arc32_to_arc52` function from algokit_utils.applications.utils. +""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 # noqa: E402 + AppSpecStateDict, + Arc32Contract, + CallConfig, + DefaultArgumentDict, + DefaultArgumentType, + MethodConfigDict, + MethodHints, + OnCompleteActionName, +) + + +@deprecated( + "Use `Arc32Contract` from algokit_utils.applications instead. Example:\n" + "```python\n" + "from algokit_utils.applications import Arc32Contract\n" + "app_spec = Arc32Contract.from_json(app_spec_json)\n" + "```" +) +class ApplicationSpecification(Arc32Contract): + """Deprecated class for ARC-0032 application specification""" -from algosdk.abi import Contract -from algosdk.abi.method import MethodDict -from algosdk.transaction import StateSchema __all__ = [ + "AppSpecStateDict", + "ApplicationSpecification", "CallConfig", "DefaultArgumentDict", "DefaultArgumentType", "MethodConfigDict", - "OnCompleteActionName", "MethodHints", - "ApplicationSpecification", - "AppSpecStateDict", -] - - -AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] -"""Type defining Application Specification state entries""" - - -class CallConfig(IntFlag): - """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" - - NEVER = 0 - """Never handle the specified on completion type""" - CALL = 1 - """Only handle the specified on completion type for application calls""" - CREATE = 2 - """Only handle the specified on completion type for application create calls""" - ALL = 3 - """Handle the specified on completion type for both create and normal application calls""" - - -class StructArgDict(TypedDict): - name: str - elements: list[list[str]] - - -OnCompleteActionName: TypeAlias = Literal[ - "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" + "OnCompleteActionName", ] -"""String literals representing on completion transaction types""" -MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] -"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" -DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] -"""Literal values describing the types of default argument sources""" - - -class DefaultArgumentDict(TypedDict): - """ - DefaultArgument is a container for any arguments that may - be resolved prior to calling some target method - """ - - source: DefaultArgumentType - data: int | str | bytes | MethodDict - - -StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword - "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} -) - - -@dataclasses.dataclass(kw_only=True) -class MethodHints: - """MethodHints provides hints to the caller about how to call the method""" - - #: hint to indicate this method can be called through Dryrun - read_only: bool = False - #: hint to provide names for tuple argument indices - #: method_name=>param_name=>{name:str, elements:[str,str]} - structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) - #: defaults - default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) - call_config: MethodConfigDict = dataclasses.field(default_factory=dict) - - def empty(self) -> bool: - return not self.dictify() - - def dictify(self) -> dict[str, Any]: - d: dict[str, Any] = {} - if self.read_only: - d["read_only"] = True - if self.default_arguments: - d["default_arguments"] = self.default_arguments - if self.structs: - d["structs"] = self.structs - if any(v for v in self.call_config.values() if v != CallConfig.NEVER): - d["call_config"] = _encode_method_config(self.call_config) - return d - - @staticmethod - def undictify(data: dict[str, Any]) -> "MethodHints": - return MethodHints( - read_only=data.get("read_only", False), - default_arguments=data.get("default_arguments", {}), - structs=data.get("structs", {}), - call_config=_decode_method_config(data.get("call_config", {})), - ) - - -def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: - return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} - - -def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: - return {k: CallConfig[v] for k, v in data.items()} - - -def _encode_source(teal_text: str) -> str: - return base64.b64encode(teal_text.encode()).decode("utf-8") - - -def _decode_source(b64_text: str) -> str: - return base64.b64decode(b64_text).decode("utf-8") - - -def _encode_state_schema(schema: StateSchema) -> dict[str, int]: - return { - "num_byte_slices": schema.num_byte_slices, - "num_uints": schema.num_uints, - } - - -def _decode_state_schema(data: dict[str, int]) -> StateSchema: - return StateSchema( # type: ignore[no-untyped-call] - num_byte_slices=data.get("num_byte_slices", 0), - num_uints=data.get("num_uints", 0), - ) - - -@dataclasses.dataclass(kw_only=True) -class ApplicationSpecification: - """ARC-0032 application specification - - See """ - - approval_program: str - clear_program: str - contract: Contract - hints: dict[str, MethodHints] - schema: StateDict - global_state_schema: StateSchema - local_state_schema: StateSchema - bare_call_config: MethodConfigDict - - def dictify(self) -> dict: - return { - "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, - "source": { - "approval": _encode_source(self.approval_program), - "clear": _encode_source(self.clear_program), - }, - "state": { - "global": _encode_state_schema(self.global_state_schema), - "local": _encode_state_schema(self.local_state_schema), - }, - "schema": self.schema, - "contract": self.contract.dictify(), - "bare_call_config": _encode_method_config(self.bare_call_config), - } - - def to_json(self) -> str: - return json.dumps(self.dictify(), indent=4) - - @staticmethod - def from_json(application_spec: str) -> "ApplicationSpecification": - json_spec = json.loads(application_spec) - return ApplicationSpecification( - approval_program=_decode_source(json_spec["source"]["approval"]), - clear_program=_decode_source(json_spec["source"]["clear"]), - schema=json_spec["schema"], - global_state_schema=_decode_state_schema(json_spec["state"]["global"]), - local_state_schema=_decode_state_schema(json_spec["state"]["local"]), - contract=Contract.undictify(json_spec["contract"]), - hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, - bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), - ) - - def export(self, directory: Path | str | None = None) -> None: - """write out the artifacts generated by the application to disk - - Args: - directory(optional): path to the directory where the artifacts should be written - """ - if directory is None: - output_dir = Path.cwd() - else: - output_dir = Path(directory) - output_dir.mkdir(exist_ok=True, parents=True) - - (output_dir / "approval.teal").write_text(self.approval_program) - (output_dir / "clear.teal").write_text(self.clear_program) - (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) - (output_dir / "application.json").write_text(self.to_json()) - - -def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] diff --git a/src/algokit_utils/applications/__init__.py b/src/algokit_utils/applications/__init__.py new file mode 100644 index 00000000..e872ef12 --- /dev/null +++ b/src/algokit_utils/applications/__init__.py @@ -0,0 +1,7 @@ +from algokit_utils.applications.abi import * # noqa: F403 +from algokit_utils.applications.app_client import * # noqa: F403 +from algokit_utils.applications.app_deployer import * # noqa: F403 +from algokit_utils.applications.app_factory import * # noqa: F403 +from algokit_utils.applications.app_manager import * # noqa: F403 +from algokit_utils.applications.app_spec import * # noqa: F403 +from algokit_utils.applications.enums import * # noqa: F403 diff --git a/src/algokit_utils/applications/abi.py b/src/algokit_utils/applications/abi.py new file mode 100644 index 00000000..7c0ef42f --- /dev/null +++ b/src/algokit_utils/applications/abi.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, TypeAlias + +import algosdk +from algosdk.abi.method import Method as AlgorandABIMethod +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, StructField +from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method + +if TYPE_CHECKING: + from algokit_utils.models.state import BoxName + +ABIValue: TypeAlias = ( + bool | int | str | bytes | bytearray | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"] +) +ABIStruct: TypeAlias = dict[str, list[dict[str, "ABIValue"]]] +Arc56ReturnValueType: TypeAlias = ABIValue | ABIStruct | None + + +ABIType: TypeAlias = algosdk.abi.ABIType +ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType + +__all__ = [ + "ABIArgumentType", + "ABIReturn", + "ABIStruct", + "ABIType", + "ABIValue", + "Arc56ReturnValueType", + "BoxABIValue", + "get_abi_decoded_value", + "get_abi_encoded_value", + "get_abi_struct_from_abi_tuple", + "get_abi_tuple_from_abi_struct", + "get_abi_tuple_type_from_abi_struct_definition", + "get_arc56_value", +] + + +@dataclass(kw_only=True) +class ABIReturn: + """Represents the return value from an ABI method call. + + Wraps the raw return value and decoded value along with any decode errors. + + :ivar result: The ABIResult object containing the method call results + :ivar raw_value: The raw return value from the method call + :ivar value: The decoded return value from the method call + :ivar method: The ABI method definition + :ivar decode_error: The exception that occurred during decoding, if any + """ + + raw_value: bytes | None = None + value: ABIValue | None = None + method: AlgorandABIMethod | None = None + decode_error: Exception | None = None + + def __init__(self, result: ABIResult) -> None: + self.decode_error = result.decode_error + if not self.decode_error: + self.raw_value = result.raw_value + self.value = result.return_value + self.method = result.method + + @property + def is_success(self) -> bool: + """Returns True if the ABI call was successful (no decode error) + + :return: True if no decode error occurred, False otherwise + """ + return self.decode_error is None + + def get_arc56_value( + self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] + ) -> Arc56ReturnValueType: + """Gets the ARC-56 formatted return value. + + :param method: The ABI method definition + :param structs: Dictionary of struct definitions + :return: The decoded return value in ARC-56 format + """ + return get_arc56_value(self, method, structs) + + +def get_arc56_value( + abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]] +) -> Arc56ReturnValueType: + """Gets the ARC-56 formatted return value from an ABI return. + + :param abi_return: The ABI return value to decode + :param method: The ABI method definition + :param structs: Dictionary of struct definitions + :raises ValueError: If there was an error decoding the return value + :return: The decoded return value in ARC-56 format + """ + if isinstance(method, AlgorandABIMethod): + type_str = method.returns.type + struct = None # AlgorandABIMethod doesn't have struct info + else: + type_str = method.returns.type + struct = method.returns.struct + + if type_str == "void" or abi_return.value is None: + return None + + if abi_return.decode_error: + raise ValueError(abi_return.decode_error) + + raw_value = abi_return.raw_value + + # Handle AVM types + if type_str == "AVMBytes": + return raw_value + if type_str == "AVMString" and raw_value: + return raw_value.decode("utf-8") + if type_str == "AVMUint64" and raw_value: + return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return] + + # Handle structs + if struct and struct in structs: + return_tuple = abi_return.value + return Arc56Contract.get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs) + + # Return as-is + return abi_return.value + + +def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: PLR0911, ANN401 + """Encodes a value according to its ABI type. + + :param value: The value to encode + :param type_str: The ABI type string + :param structs: Dictionary of struct definitions + :raises ValueError: If the value cannot be encoded for the given type + :return: The ABI encoded bytes + """ + if isinstance(value, (bytes | bytearray)): + return value + if type_str == "AVMUint64": + return ABIType.from_string("uint64").encode(value) + if type_str in ("AVMBytes", "AVMString"): + if isinstance(value, str): + return value.encode("utf-8") + if not isinstance(value, (bytes | bytearray)): + raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}") + return value + if type_str in structs: + tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs) + if isinstance(value, (list | tuple)): + return tuple_type.encode(value) # type: ignore[arg-type] + else: + tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs) + return tuple_type.encode(tuple_values) + else: + abi_type = ABIType.from_string(type_str) + return abi_type.encode(value) + + +def get_abi_decoded_value( + value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[StructField]] +) -> ABIValue: + """Decodes a value according to its ABI type. + + :param value: The value to decode + :param type_str: The ABI type string or type object + :param structs: Dictionary of struct definitions + :return: The decoded ABI value + """ + type_value = str(type_str) + + if type_value == "AVMBytes" or not isinstance(value, bytes): + 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]: + """Converts an ABI struct to a tuple representation. + + :param struct_value: The struct value as a dictionary + :param struct_fields: List of struct field definitions + :param structs: Dictionary of struct definitions + :raises ValueError: If a required field is missing from the struct + :return: The struct as a tuple + """ + result = [] + for field in struct_fields: + key = field.name + if key not in struct_value: + raise ValueError(f"Missing value for field '{key}'") + value = struct_value[key] + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_tuple_from_abi_struct(value, field_type, structs) + result.append(value) + return result + + +def get_abi_tuple_type_from_abi_struct_definition( + struct_def: list[StructField], structs: dict[str, list[StructField]] +) -> algosdk.abi.TupleType: + """Creates a TupleType from a struct definition. + + :param struct_def: The struct field definitions + :param structs: Dictionary of struct definitions + :raises ValueError: If a field type is invalid + :return: The TupleType representing the struct + """ + types = [] + for field in struct_def: + field_type = field.type + if isinstance(field_type, str): + if field_type in structs: + types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs)) + else: + types.append(ABIType.from_string(field_type)) # type: ignore[arg-type] + elif isinstance(field_type, list): + types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs)) + else: + raise ValueError(f"Invalid field type: {field_type}") + return algosdk.abi.TupleType(types) + + +def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], +) -> dict[str, Any]: + """Converts a decoded tuple to an ABI struct. + + :param decoded_tuple: The tuple to convert + :param struct_fields: List of struct field definitions + :param structs: Dictionary of struct definitions + :return: The tuple as a struct dictionary + """ + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + +@dataclass(kw_only=True, frozen=True) +class BoxABIValue: + """Represents an ABI value stored in a box. + + :ivar name: The name of the box + :ivar value: The ABI value stored in the box + """ + + name: BoxName + value: ABIValue diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py new file mode 100644 index 00000000..406da6a8 --- /dev/null +++ b/src/algokit_utils/applications/app_client.py @@ -0,0 +1,2056 @@ +from __future__ import annotations + +import base64 +import copy +import json +import os +from collections.abc import Sequence +from dataclasses import dataclass, fields +from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar + +import algosdk +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction + +from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps +from algokit_utils.applications.abi import ( + ABIReturn, + ABIStruct, + ABIType, + ABIValue, + Arc56ReturnValueType, + BoxABIValue, + get_abi_decoded_value, + get_abi_encoded_value, + get_abi_tuple_from_abi_struct, +) +from algokit_utils.applications.app_spec.arc32 import Arc32Contract +from algokit_utils.applications.app_spec.arc56 import ( + Arc56Contract, + Method, + PcOffsetMethod, + ProgramSourceInfo, + SourceInfo, + StorageKey, + StorageMap, +) +from algokit_utils.config import config +from algokit_utils.errors.logic_error import LogicError, parse_logic_error +from algokit_utils.models.application import ( + AppSourceMaps, + AppState, + CompiledTeal, +) +from algokit_utils.models.state import BoxName, BoxValue +from algokit_utils.models.transaction import SendParams +from algokit_utils.protocols.account import TransactionSignerAccountProtocol +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCallParams, + AppCreateSchema, + AppDeleteMethodCallParams, + AppMethodCallTransactionArgument, + AppUpdateMethodCallParams, + AppUpdateParams, + BuiltTransactions, + PaymentParams, +) +from algokit_utils.transactions.transaction_sender import ( + SendAppTransactionResult, + SendAppUpdateTransactionResult, + SendSingleTransactionResult, +) + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.atomic_transaction_composer import TransactionSigner + + from algokit_utils.algorand import AlgorandClient + from algokit_utils.applications.app_deployer import ApplicationLookup + from algokit_utils.applications.app_manager import AppManager + from algokit_utils.models.amount import AlgoAmount + from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams + +__all__ = [ + "AppClient", + "AppClientBareCallCreateParams", + "AppClientBareCallParams", + "AppClientCallParams", + "AppClientCompilationParams", + "AppClientCompilationResult", + "AppClientCreateSchema", + "AppClientMethodCallCreateParams", + "AppClientMethodCallParams", + "AppClientParams", + "AppSourceMaps", + "BaseAppClientMethodCallParams", + "CreateOnComplete", + "FundAppAccountParams", + "get_constant_block_offset", +] + +# 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 + +# Sentinel to detect missing arguments in clone() method of AppClient +_MISSING = object() + + +def get_constant_block_offset(program: bytes) -> int: # noqa: C901 + """Calculate the offset after constant blocks in TEAL program. + + Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations. + + :param program: The compiled TEAL program as bytes + :return: The maximum offset position after any constant block operations + """ + bytes_list = list(program) + program_size = len(bytes_list) + + # 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) + + +CreateOnComplete = Literal[ + OnComplete.NoOpOC, + OnComplete.UpdateApplicationOC, + OnComplete.DeleteApplicationOC, + OnComplete.OptInOC, + OnComplete.CloseOutOC, +] + + +@dataclass(kw_only=True, frozen=True) +class AppClientCompilationResult: + """Result of compiling an application's TEAL code. + + Contains the compiled approval and clear state programs along with optional compilation artifacts. + + :ivar approval_program: The compiled approval program bytes + :ivar clear_state_program: The compiled clear state program bytes + :ivar compiled_approval: Optional compilation artifacts for approval program + :ivar compiled_clear: Optional compilation artifacts for clear state program + """ + + approval_program: bytes + clear_state_program: bytes + compiled_approval: CompiledTeal | None = None + compiled_clear: CompiledTeal | None = None + + +class AppClientCompilationParams(TypedDict, total=False): + """Parameters for compiling an application's TEAL code. + + :ivar deploy_time_params: Optional template parameters to use during compilation + :ivar updatable: Optional flag indicating if app should be updatable + :ivar deletable: Optional flag indicating if app should be deletable + """ + + deploy_time_params: TealTemplateParams | None + updatable: bool | None + deletable: bool | None + + +@dataclass(kw_only=True) +class FundAppAccountParams: + """Parameters for funding an application's account. + + :ivar sender: Optional sender address + :ivar signer: Optional transaction signer + :ivar rekey_to: Optional address to rekey to + :ivar note: Optional transaction note + :ivar lease: Optional lease + :ivar static_fee: Optional static fee + :ivar extra_fee: Optional extra fee + :ivar max_fee: Optional maximum fee + :ivar validity_window: Optional validity window in rounds + :ivar first_valid_round: Optional first valid round + :ivar last_valid_round: Optional last valid round + :ivar amount: Amount to fund + :ivar close_remainder_to: Optional address to close remainder to + :ivar on_complete: Optional on complete action + """ + + 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 + on_complete: algosdk.transaction.OnComplete | None = None + + +@dataclass(kw_only=True) +class AppClientCallParams: + """Parameters for calling an application. + + :ivar method: Optional ABI method name or signature + :ivar args: Optional arguments to pass to method + :ivar boxes: Optional box references to load + :ivar accounts: Optional account addresses to load + :ivar apps: Optional app IDs to load + :ivar assets: Optional asset IDs to load + :ivar lease: Optional lease + :ivar sender: Optional sender address + :ivar note: Optional transaction note + :ivar send_params: Optional parameters to control transaction sending + """ + + method: str | None = None + args: list | None = None + boxes: list | None = None + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + lease: (str | bytes) | None = None + sender: str | None = None + note: (bytes | dict | str) | None = None + send_params: dict | None = None + + +ArgsT = TypeVar("ArgsT") +MethodT = TypeVar("MethodT") + + +@dataclass(kw_only=True, frozen=True) +class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]): + """Base parameters for application method calls. + + :ivar method: Method to call + :ivar args: Optional arguments to pass to method + :ivar account_references: Optional account references + :ivar app_references: Optional application references + :ivar asset_references: Optional asset references + :ivar box_references: Optional box references + :ivar extra_fee: Optional extra fee + :ivar first_valid_round: Optional first valid round + :ivar lease: Optional lease + :ivar max_fee: Optional maximum fee + :ivar note: Optional note + :ivar rekey_to: Optional rekey to address + :ivar sender: Optional sender address + :ivar signer: Optional transaction signer + :ivar static_fee: Optional static fee + :ivar validity_window: Optional validity window + :ivar last_valid_round: Optional last valid round + :ivar on_complete: Optional on complete action + """ + + method: MethodT + args: ArgsT | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: Sequence[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 AppClientMethodCallParams( + BaseAppClientMethodCallParams[ + Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None], + str, + ] +): + """Parameters for application method calls.""" + + +@dataclass(kw_only=True, frozen=True) +class AppClientBareCallParams: + """Parameters for bare application calls. + + :ivar signer: Optional transaction signer + :ivar rekey_to: Optional rekey to address + :ivar lease: Optional lease + :ivar static_fee: Optional static fee + :ivar extra_fee: Optional extra fee + :ivar max_fee: Optional maximum fee + :ivar validity_window: Optional validity window + :ivar first_valid_round: Optional first valid round + :ivar last_valid_round: Optional last valid round + :ivar sender: Optional sender address + :ivar note: Optional note + :ivar args: Optional arguments + :ivar account_references: Optional account references + :ivar app_references: Optional application references + :ivar asset_references: Optional asset references + :ivar box_references: Optional box references + """ + + signer: TransactionSigner | None = None + rekey_to: str | None = None + lease: bytes | None = None + 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(frozen=True) +class AppClientCreateSchema: + """Schema for application creation. + + :ivar extra_program_pages: Optional number of extra program pages + :ivar schema: Optional application creation schema + """ + + extra_program_pages: int | None = None + schema: AppCreateSchema | None = None + + +@dataclass(frozen=True) +class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallParams): + """Parameters for creating application with bare call.""" + + on_complete: OnComplete | None = None + + +@dataclass(frozen=True) +class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams): + """Parameters for creating application with method call.""" + + on_complete: CreateOnComplete | None = None + + +class _AppClientStateMethods: + def __init__( + 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 _AppClientBoxMethods: + def __init__( + self, + *, + get_all: Callable[[], dict[str, Any]], + get_value: Callable[[str], ABIValue | None], + get_map_value: Callable[[str, bytes | Any], Any], + get_map: Callable[[str], dict[str, ABIValue]], + ) -> None: + self._get_all = get_all + self._get_value = get_value + self._get_map_value = get_map_value + self._get_map = get_map + + def get_all(self) -> dict[str, Any]: + return self._get_all() + + def get_value(self, name: str) -> ABIValue | None: + return self._get_value(name) + + def get_map_value(self, map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 + return self._get_map_value(map_name, key) + + def get_map(self, map_name: str) -> dict[str, ABIValue]: + return self._get_map(map_name) + + +class _StateAccessor: + 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) -> _AppClientStateMethods: + """Methods to access local state for the current app for a given address""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address), + key_getter=lambda: self._app_spec.state.keys.local_state, + map_getter=lambda: self._app_spec.state.maps.local_state, + ) + + @property + def global_state(self) -> _AppClientStateMethods: + """Methods to access global state for the current app""" + return self._get_state_methods( + state_getter=lambda: self._algorand.app.get_global_state(self._app_id), + key_getter=lambda: self._app_spec.state.keys.global_state, + map_getter=lambda: self._app_spec.state.maps.global_state, + ) + + @property + def box(self) -> _AppClientBoxMethods: + """Methods to access box storage for the current app""" + return self._get_box_methods() + + def _get_box_methods(self) -> _AppClientBoxMethods: + def get_all() -> dict[str, Any]: + """Returns all single-key box values in a dict keyed by the key name.""" + return {key: get_value(key) for key in self._app_spec.state.keys.box} + + def get_value(name: str) -> ABIValue | None: + """Returns a single box value for the current app with the value a decoded ABI value. + + :param name: The name of the box value to retrieve + :return: The decoded ABI value from the box storage, or None if not found + """ + metadata = self._app_spec.state.keys.box[name] + value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(metadata.key)) + return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs) + + def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401 + """Get a value from a box map. + + Retrieves a value from a box map storage using the provided map name and key. + + :param map_name: The name of the map to read from + :param key: The key within the map (without any map prefix) as either bytes or a value + that will be converted to bytes by encoding it using the specified ABI key type + :return: The decoded value from the box map storage + """ + metadata = self._app_spec.state.maps.box[map_name] + prefix = base64.b64decode(metadata.prefix or "") + encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) + full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") + value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(full_key)) + return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs) + + def get_map(map_name: str) -> dict[str, ABIValue]: + """Get all key-value pairs from a box map. + + Retrieves all key-value pairs stored in a box map for the current app. + + :param map_name: The name of the map to read from + :return: A dictionary mapping string keys to their corresponding ABI-decoded values + :raises ValueError: If there is an error decoding any key or value in the map + """ + metadata = self._app_spec.state.maps.box[map_name] + prefix = base64.b64decode(metadata.prefix or "") + box_names = self._algorand.app.get_box_names(self._app_id) + + result = {} + for box in box_names: + if not box.name_raw.startswith(prefix): + continue + + encoded_key = prefix + box.name_raw + base64_key = base64.b64encode(encoded_key).decode("utf-8") + + try: + key = get_abi_decoded_value(box.name_raw[len(prefix) :], metadata.key_type, self._app_spec.structs) + value = get_abi_decoded_value( + self._algorand.app.get_box_value(self._app_id, base64.b64decode(base64_key)), + metadata.value_type, + self._app_spec.structs, + ) + result[str(key)] = value + except Exception as e: + if "Failed to decode key" in str(e): + raise ValueError(f"Failed to decode key {base64_key}") from e + raise ValueError(f"Failed to decode value for key {base64_key}") from e + + return result + + return _AppClientBoxMethods( + get_all=get_all, + get_value=get_value, + get_map_value=get_map_value, + get_map=get_map, + ) + + def _get_state_methods( # noqa: C901 + self, + state_getter: Callable[[], dict[str, AppState]], + key_getter: Callable[[], dict[str, StorageKey]], + map_getter: Callable[[], dict[str, StorageMap]], + ) -> _AppClientStateMethods: + 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 value.value if value else None + + def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401 + state = app_state or state_getter() + metadata = map_getter()[map_name] + + prefix = base64.b64decode(metadata.prefix or "") + encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs) + full_key = base64.b64encode(prefix + encoded_key).decode("utf-8") + value = 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 value.value if value else None + + def get_map(map_name: str) -> dict[str, ABIValue]: + state = state_getter() + metadata = map_getter()[map_name] + + prefix = base64.b64decode(metadata.prefix or "").decode("utf-8") + + 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 _BareParamsBuilder: + 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 | None = None + ) -> dict[str, Any]: + 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 or OnComplete.NoOpOC, + } + + def update( + self, + params: AppClientBareCallParams | None = None, + ) -> AppUpdateParams: + """Create parameters for updating an application. + + :param params: Optional compilation and send parameters, defaults to None + :return: Parameters for updating the application + """ + call_params: AppUpdateParams = AppUpdateParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.UpdateApplicationOC) + ) + return call_params + + def opt_in(self, params: AppClientBareCallParams | None = None) -> AppCallParams: + """Create parameters for opting into an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for opting into the application + """ + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.OptInOC) + ) + return call_params + + def delete(self, params: AppClientBareCallParams | None = None) -> AppCallParams: + """Create parameters for deleting an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for deleting the application + """ + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.DeleteApplicationOC) + ) + return call_params + + def clear_state(self, params: AppClientBareCallParams | None = None) -> AppCallParams: + """Create parameters for clearing application state. + + :param params: Optional send parameters, defaults to None + :return: Parameters for clearing application state + """ + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.ClearStateOC) + ) + return call_params + + def close_out(self, params: AppClientBareCallParams | None = None) -> AppCallParams: + """Create parameters for closing out of an application. + + :param params: Optional send parameters, defaults to None + :return: Parameters for closing out of the application + """ + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, OnComplete.CloseOutOC) + ) + return call_params + + def call( + self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC + ) -> AppCallParams: + """Create parameters for calling an application. + + :param params: Optional call parameters with on complete action, defaults to None + :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC + :return: Parameters for calling the application + """ + call_params: AppCallParams = AppCallParams( + **self._get_bare_params(params.__dict__ if params else {}, on_complete or OnComplete.NoOpOC) + ) + return call_params + + +class _MethodParamsBuilder: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_params_accessor = _BareParamsBuilder(client) + + @property + def bare(self) -> _BareParamsBuilder: + return self._bare_params_accessor + + def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: + """Create parameters for funding an application account. + + :param params: Parameters for funding the application account + :return: Parameters for sending a payment transaction to fund the application account + """ + + def random_note() -> bytes: + return base64.b64encode(os.urandom(16)) + + 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) -> AppCallMethodCallParams: + """Create parameters for opting into an application. + + :param params: Parameters for the opt-in call + :return: Parameters for opting into the application + """ + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.OptInOC + ) + return AppCallMethodCallParams(**input_params) + + def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: + """Create parameters for calling an application method. + + :param params: Parameters for the method call + :return: Parameters for calling the application method + """ + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC + ) + return AppCallMethodCallParams(**input_params) + + def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: + """Create parameters for deleting an application. + + :param params: Parameters for the delete call + :return: Parameters for deleting the application + """ + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.DeleteApplicationOC + ) + return AppDeleteMethodCallParams(**input_params) + + def update( + self, params: AppClientMethodCallParams, compilation_params: AppClientCompilationParams | None = None + ) -> AppUpdateMethodCallParams: + """Create parameters for updating an application. + + :param params: Parameters for the update call, optionally including compilation parameters + :param compilation_params: Parameters for the compilation, defaults to None + :return: Parameters for updating the application + """ + compile_params = ( + self._client.compile( + app_spec=self._client.app_spec, + app_manager=self._algorand.app, + compilation_params=compilation_params, + ).__dict__ + if compilation_params + else {} + ) + + input_params = { + **self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.UpdateApplicationOC + ), + **compile_params, + } + # Filter input_params to include only fields valid for AppUpdateMethodCallParams + app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCallParams)} + filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields} + return AppUpdateMethodCallParams(**filtered_input_params) + + def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams: + """Create parameters for closing out of an application. + + :param params: Parameters for the close-out call + :return: Parameters for closing out of the application + """ + input_params = self._get_abi_params( + params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.CloseOutOC + ) + return AppCallMethodCallParams(**input_params) + + def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + 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"] = self._app_spec.get_arc56_method(params["method"]).to_abi_method() + input_params["args"] = self._client._get_abi_args_with_default_values( + method_name_or_signature=params["method"], + args=params.get("args"), + sender=self._client._get_sender(input_params["sender"]), + ) + + return input_params + + +class _AppClientBareCallCreateTransactionMethods: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + + def update(self, params: AppClientBareCallParams | None = None) -> Transaction: + """Create a transaction to update an application. + + Creates a transaction that will update an existing application with new approval and clear state programs. + + :param params: Parameters for the update call including compilation and transaction options, defaults to None + :return: The constructed application update transaction + """ + return self._algorand.create_transaction.app_update( + self._client.params.bare.update(params or AppClientBareCallParams()) + ) + + def opt_in(self, params: AppClientBareCallParams | None = None) -> Transaction: + """Create a transaction to opt into an application. + + Creates a transaction that will opt the sender account into using this application. + + :param params: Parameters for the opt-in call including transaction options, defaults to None + :return: The constructed opt-in transaction + """ + return self._algorand.create_transaction.app_call( + self._client.params.bare.opt_in(params or AppClientBareCallParams()) + ) + + def delete(self, params: AppClientBareCallParams | None = None) -> Transaction: + """Create a transaction to delete an application. + + Creates a transaction that will delete this application from the blockchain. + + :param params: Parameters for the delete call including transaction options, defaults to None + :return: The constructed delete transaction + """ + return self._algorand.create_transaction.app_call( + self._client.params.bare.delete(params or AppClientBareCallParams()) + ) + + def clear_state(self, params: AppClientBareCallParams | None = None) -> Transaction: + """Create a transaction to clear application state. + + Creates a transaction that will clear the sender's local state for this application. + + :param params: Parameters for the clear state call including transaction options, defaults to None + :return: The constructed clear state transaction + """ + return self._algorand.create_transaction.app_call( + self._client.params.bare.clear_state(params or AppClientBareCallParams()) + ) + + def close_out(self, params: AppClientBareCallParams | None = None) -> Transaction: + """Create a transaction to close out of an application. + + Creates a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including transaction options, defaults to None + :return: The constructed close out transaction + """ + return self._algorand.create_transaction.app_call( + self._client.params.bare.close_out(params or AppClientBareCallParams()) + ) + + def call( + self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC + ) -> Transaction: + """Create a transaction to call an application. + + Creates a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including on complete action, defaults to None + :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC + :return: The constructed application call transaction + """ + return self._algorand.create_transaction.app_call( + self._client.params.bare.call(params or AppClientBareCallParams(), on_complete or OnComplete.NoOpOC) + ) + + +class _TransactionCreator: + def __init__(self, client: AppClient) -> None: + self._client = client + self._algorand = client._algorand + self._app_id = client._app_id + self._app_spec = client._app_spec + self._bare_create_transaction_methods = _AppClientBareCallCreateTransactionMethods(client) + + @property + def bare(self) -> _AppClientBareCallCreateTransactionMethods: + return self._bare_create_transaction_methods + + def fund_app_account(self, params: FundAppAccountParams) -> Transaction: + """Create a transaction to fund an application account. + + Creates a payment transaction to fund the application account with the specified parameters. + + :param params: Parameters for funding the application account including amount and transaction options + :return: The constructed payment transaction + """ + return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params)) + + def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to opt into an application. + + Creates a transaction that will opt the sender into this application with the specified parameters. + + :param params: Parameters for the opt-in call including method arguments and transaction options + :return: The constructed opt-in transaction(s) + """ + return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params)) + + def update(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to update an application. + + Creates a transaction that will update this application with new approval and clear state programs. + + :param params: Parameters for the update call including method arguments and transaction options + :return: The constructed update transaction(s) + """ + return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params)) + + def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to delete an application. + + Creates a transaction that will delete this application. + + :param params: Parameters for the delete call including method arguments and transaction options + :return: The constructed delete transaction(s) + """ + return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params)) + + def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to close out of an application. + + Creates a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including method arguments and transaction options + :return: The constructed close out transaction(s) + """ + return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params)) + + def call(self, params: AppClientMethodCallParams) -> BuiltTransactions: + """Create a transaction to call an application. + + Creates a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including method arguments and transaction options + :return: The constructed application call transaction(s) + """ + return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params)) + + +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: AppClientBareCallParams | None = None, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application update transaction. + + Sends a transaction to update an existing application with new approval and clear state programs. + + :param params: The parameters for the update call, including optional compilation parameters, + deploy time parameters, and transaction configuration + :param send_params: Send parameters, defaults to None + :param compilation_params: Parameters for the compilation, defaults to None + :return: The result of sending the transaction, including compilation artifacts and ABI return + value if applicable + """ + params = params or AppClientBareCallParams() + compilation = compilation_params or AppClientCompilationParams() + compiled = self._client.compile_app( + { + "deploy_time_params": compilation.get("deploy_time_params"), + "updatable": compilation.get("updatable"), + "deletable": compilation.get("deletable"), + } + ) + bare_params = self._client.params.bare.update(params) + bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval) + 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, send_params)) + return SendAppTransactionResult[ABIReturn]( + **{**call_result.__dict__, **(compiled.__dict__ if compiled else {})}, + abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)), + ) + + def opt_in( + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application opt-in transaction. + + Creates and sends a transaction that will opt the sender's account into this application. + + :param params: Parameters for the opt-in call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.opt_in(params or AppClientBareCallParams()), send_params + ) + ) + + def delete( + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application delete transaction. + + Creates and sends a transaction that will delete this application. + + :param params: Parameters for the delete call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.delete(params or AppClientBareCallParams()), send_params + ) + ) + + def clear_state( + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application clear state transaction. + + Creates and sends a transaction that will clear the sender's local state for this application. + + :param params: Parameters for the clear state call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.clear_state(params or AppClientBareCallParams()), send_params + ) + ) + + def close_out( + self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application close out transaction. + + Creates and sends a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including transaction options, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.close_out(params or AppClientBareCallParams()), send_params + ) + ) + + def call( + self, + params: AppClientBareCallParams | None = None, + on_complete: OnComplete | None = None, + send_params: SendParams | None = None, + ) -> SendAppTransactionResult[ABIReturn]: + """Send an application call transaction. + + Creates and sends a transaction that will call this application with the specified parameters. + + :param params: Parameters for the application call including transaction options, defaults to None + :param on_complete: The OnComplete action, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._algorand.send.app_call( + self._client.params.bare.call(params or AppClientBareCallParams(), on_complete), send_params + ) + ) + + +class _TransactionSender: + 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: + """Get accessor for bare application calls. + + :return: Accessor for making bare application calls without ABI encoding + """ + return self._bare_send_accessor + + def fund_app_account( + self, params: FundAppAccountParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Send funds to the application account. + + Creates and sends a payment transaction to fund the application account. + + :param params: Parameters for funding the app account including amount and transaction options + :param send_params: Send parameters, defaults to None + :return: The result of sending the payment transaction + """ + return self._client._handle_call_errors( # type: ignore[no-any-return] + lambda: self._algorand.send.payment(self._client.params.fund_app_account(params), send_params) + ) + + def opt_in( + self, params: AppClientMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application opt-in transaction. + + Creates and sends a transaction that will opt the sender into this application. + + :param params: Parameters for the opt-in call including method and transaction options + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params), send_params), + self._app_spec.get_arc56_method(params.method), + ) + ) + + def delete( + self, params: AppClientMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application delete transaction. + + Creates and sends a transaction that will delete this application. + + :param params: Parameters for the delete call including method and transaction options + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params), send_params), + self._app_spec.get_arc56_method(params.method), + ) + ) + + def update( + self, + params: AppClientMethodCallParams, + compilation_params: AppClientCompilationParams | None = None, + send_params: SendParams | None = None, + ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]: + """Send an application update transaction. + + Creates and sends a transaction that will update this application's program. + + :param params: Parameters for the update call including method, compilation and transaction options + :param compilation_params: Parameters for the compilation, defaults to None + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + result = self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_update_method_call( + self._client.params.update(params, compilation_params), send_params + ), + self._app_spec.get_arc56_method(params.method), + ) + ) + assert isinstance(result, SendAppUpdateTransactionResult) + return result + + def close_out( + self, params: AppClientMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application close out transaction. + + Creates and sends a transaction that will close out the sender's participation in this application. + + :param params: Parameters for the close out call including method and transaction options + :param send_params: Send parameters, defaults to None + :return: The result of sending the transaction, including ABI return value if applicable + """ + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params), send_params), + self._app_spec.get_arc56_method(params.method), + ) + ) + + def call( + self, params: AppClientMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[Arc56ReturnValueType]: + """Send an application call transaction. + + Creates and sends a transaction that will call this application with the specified parameters. + For read-only calls, simulates the transaction instead of sending it. + + :param params: Parameters for the application call including method and transaction options + :param send_params: Send parameters + :return: The result of sending or simulating the transaction, including ABI return value if applicable + """ + is_read_only_call = ( + params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None + ) and self._app_spec.get_arc56_method(params.method).readonly + + if is_read_only_call: + method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( + self._client.params.call(params) + ) + send_params = send_params or SendParams() + simulate_response = self._client._handle_call_errors( + lambda: method_call_to_simulate.simulate( + allow_unnamed_resources=send_params.get("populate_app_call_resources") or True, + skip_signatures=True, + allow_more_logs=True, + allow_empty_signatures=True, + extra_opcode_budget=None, + exec_trace_config=None, + simulation_round=None, + ) + ) + + return SendAppTransactionResult[Arc56ReturnValueType]( + 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, + abi_return=simulate_response.returns[-1].get_arc56_value( + self._app_spec.get_arc56_method(params.method), self._app_spec.structs + ), + ) + + return self._client._handle_call_errors( + lambda: self._client._process_method_call_return( + lambda: self._algorand.send.app_call_method_call(self._client.params.call(params), send_params), + self._app_spec.get_arc56_method(params.method), + ) + ) + + +@dataclass(kw_only=True, frozen=True) +class AppClientParams: + """Full parameters for creating an app client""" + + app_spec: Arc56Contract | Arc32Contract | str + algorand: AlgorandClient + app_id: int + app_name: str | None = None + default_sender: str | None = None + default_signer: TransactionSigner | None = None + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None + + +class AppClient: + """A client for interacting with an Algorand smart contract application. + + Provides a high-level interface for interacting with Algorand smart contracts, including + methods for calling application methods, managing state, and handling transactions. + + :param params: Parameters for creating the app client + """ + + def __init__(self, params: AppClientParams) -> None: + self._app_id = params.app_id + self._app_spec = self.normalise_app_spec(params.app_spec) + 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 = _StateAccessor(self) + self._params_accessor = _MethodParamsBuilder(self) + self._send_accessor = _TransactionSender(self) + self._create_transaction_accessor = _TransactionCreator(self) + + @property + def algorand(self) -> AlgorandClient: + """Get the Algorand client instance. + + :return: The Algorand client used by this app client + """ + return self._algorand + + @property + def app_id(self) -> int: + """Get the application ID. + + :return: The ID of the Algorand application + """ + return self._app_id + + @property + def app_address(self) -> str: + """Get the application's Algorand address. + + :return: The Algorand address associated with this application + """ + return self._app_address + + @property + def app_name(self) -> str: + """Get the application name. + + :return: The name of the application + """ + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + """Get the application specification. + + :return: The ARC-56 contract specification for this application + """ + return self._app_spec + + @property + def state(self) -> _StateAccessor: + """Get the state accessor. + + :return: The state accessor for this application + """ + return self._state_accessor + + @property + def params(self) -> _MethodParamsBuilder: + """Get the method parameters builder. + + :return: The method parameters builder for this application + """ + return self._params_accessor + + @property + def send(self) -> _TransactionSender: + """Get the transaction sender. + + :return: The transaction sender for this application + """ + return self._send_accessor + + @property + def create_transaction(self) -> _TransactionCreator: + """Get the transaction creator. + + :return: The transaction creator for this application + """ + return self._create_transaction_accessor + + @staticmethod + def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Contract: + """Normalize an application specification to ARC-56 format. + + :param app_spec: The application specification to normalize + :return: The normalized ARC-56 contract specification + :raises ValueError: If the app spec format is invalid + """ + if isinstance(app_spec, str): + spec_dict = json.loads(app_spec) + spec = Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict + else: + spec = app_spec + + match spec: + case Arc56Contract(): + return spec + case Arc32Contract(): + return Arc56Contract.from_arc32(spec.to_json()) + case dict(): + return Arc56Contract.from_dict(spec) + case _: + raise ValueError("Invalid app spec format") + + @staticmethod + def from_network( + app_spec: Arc56Contract | Arc32Contract | str, + algorand: AlgorandClient, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + """Create an AppClient instance from network information. + + :param app_spec: The application specification + :param algorand: The Algorand client instance + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :return: A new AppClient instance + :raises Exception: If no app ID is found for the network + """ + network = algorand.client.network() + app_spec = AppClient.normalise_app_spec(app_spec) + network_names = [network.genesis_hash] + + if network.is_localnet: + network_names.append("localnet") + if network.is_mainnet: + network_names.append("mainnet") + if network.is_testnet: + 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 from_creator_and_name( + creator_address: str, + app_name: str, + app_spec: Arc56Contract | Arc32Contract | str, + algorand: AlgorandClient, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: ApplicationLookup | None = None, + ) -> AppClient: + """Create an AppClient instance from creator address and application name. + + :param creator_address: The address of the application creator + :param app_name: The name of the application + :param app_spec: The application specification + :param algorand: The Algorand client instance + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :return: A new AppClient instance + :raises ValueError: If the app is not found for the creator and name + """ + app_spec_ = AppClient.normalise_app_spec(app_spec) + app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name( + creator_address=creator_address, ignore_cache=ignore_cache or False + ) + app_metadata = app_lookup.apps.get(app_name or app_spec_.name) + if not app_metadata: + raise ValueError(f"App not found for creator {creator_address} and name {app_name or app_spec_.name}") + + return AppClient( + AppClientParams( + app_id=app_metadata.app_id, + app_spec=app_spec_, + algorand=algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + + @staticmethod + def compile( + app_spec: Arc56Contract, + app_manager: AppManager, + compilation_params: AppClientCompilationParams | None = None, + ) -> AppClientCompilationResult: + """Compile the application's TEAL code. + + :param app_spec: The application specification + :param app_manager: The application manager instance + :param compilation_params: Optional compilation parameters + :return: The compilation result + :raises ValueError: If attempting to compile without source or byte code + """ + compilation_params = compilation_params or AppClientCompilationParams() + deploy_time_params = compilation_params.get("deploy_time_params") + updatable = compilation_params.get("updatable") + deletable = compilation_params.get("deletable") + + def is_base64(s: str) -> bool: + try: + 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.approval or not app_spec.byte_code.clear: + raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code") + + return AppClientCompilationResult( + approval_program=base64.b64decode(app_spec.byte_code.approval), + clear_state_program=base64.b64decode(app_spec.byte_code.clear), + ) + + compiled_approval = app_manager.compile_teal_template( + app_spec.source.get_decoded_approval(), + template_params=deploy_time_params, + deployment_metadata=( + {"updatable": updatable, "deletable": deletable} + if updatable is not None or deletable is not None + else None + ), + ) + + compiled_clear = app_manager.compile_teal_template( + app_spec.source.get_decoded_clear(), + template_params=deploy_time_params, + ) + + if config.debug and config.project_root: + persist_sourcemaps( + sources=[ + PersistSourceMapInput( + compiled_teal=compiled_approval, app_name=app_spec.name, file_name="approval.teal" + ), + PersistSourceMapInput(compiled_teal=compiled_clear, app_name=app_spec.name, file_name="clear.teal"), + ], + project_root=config.project_root, + client=app_manager._algod, + with_sources=True, + ) + + return AppClientCompilationResult( + approval_program=compiled_approval.compiled_base64_to_bytes, + compiled_approval=compiled_approval, + 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, + is_clear_state_program: bool = False, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + program: bytes | None = None, + approval_source_info: ProgramSourceInfo | None = None, + clear_source_info: ProgramSourceInfo | None = None, + ) -> Exception: + source_map = clear_source_map if is_clear_state_program else approval_source_map + + error_details = parse_logic_error(str(e)) + if not error_details: + return e + + # The PC value to find in the ARC56 SourceInfo + arc56_pc = error_details["pc"] + + program_source_info = clear_source_info if is_clear_state_program else approval_source_info + + # The offset to apply to the PC if using the cblocks pc offset method + cblocks_offset = 0 + + # If the program uses cblocks offset, then we need to adjust the PC accordingly + if program_source_info and program_source_info.pc_offset_method == PcOffsetMethod.CBLOCKS: + if not program: + raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset") + + cblocks_offset = get_constant_block_offset(program) + arc56_pc = error_details["pc"] - cblocks_offset + + # Find the source info for this PC and get the error message + source_info = None + if program_source_info and program_source_info.source_info: + source_info = next( + (s for s in program_source_info.source_info if isinstance(s, SourceInfo) 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_decoded_clear() + if is_clear_state_program + else app_spec.source.get_decoded_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 + + message = e.logic_error_str if isinstance(e, LogicError) else str(e) + app_id = re.search(r"(?<=app=)\d+", message) + tx_id = re.search(r"(?<=transaction )\S+(?=:)", message) + error = Exception( + f"Runtime error when executing {app_spec.name} " + f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction " + f"{tx_id.group() if tx_id else 'N/A'}: {error_message}" + ) + error.__cause__ = e + return error + + return e + + def compile_app( + self, + compilation_params: AppClientCompilationParams | None = None, + ) -> AppClientCompilationResult: + """Compile the application's TEAL code. + + :param compilation_params: Optional compilation parameters + :return: The compilation result + """ + result = AppClient.compile(self._app_spec, self._algorand.app, compilation_params) + + 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 = _MISSING, # type: ignore[assignment] + default_sender: str | None = _MISSING, # type: ignore[assignment] + default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment] + approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] + clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment] + ) -> AppClient: + """Create a cloned AppClient instance with optionally overridden parameters. + + :param app_name: Optional new application name + :param default_sender: Optional new default sender + :param default_signer: Optional new default signer + :param approval_source_map: Optional new approval source map + :param clear_source_map: Optional new clear source map + :return: A new AppClient instance with the specified parameters + """ + return AppClient( + AppClientParams( + app_id=self._app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=self._app_name if app_name is _MISSING else app_name, + default_sender=self._default_sender if default_sender is _MISSING else default_sender, + default_signer=self._default_signer if default_signer is _MISSING else default_signer, + approval_source_map=( + self._approval_source_map if approval_source_map is _MISSING else approval_source_map + ), + clear_source_map=(self._clear_source_map if clear_source_map is _MISSING else clear_source_map), + ) + ) + + def export_source_maps(self) -> AppSourceMaps: + """Export the application's source maps. + + :return: The application's source maps + :raises ValueError: If source maps haven't been loaded + """ + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "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: + """Import source maps for the application. + + :param source_maps: The source maps to import + :raises ValueError: If source maps are invalid or missing + """ + if not source_maps.approval_source_map: + raise ValueError("Approval source map is required") + if not source_maps.clear_source_map: + 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]: + """Get local state for an account. + + :param address: The account address + :return: The account's local state for this application + """ + return self._state_accessor.get_local_state(address) + + def get_global_state(self) -> dict[str, AppState]: + """Get the application's global state. + + :return: The application's global state + """ + return self._state_accessor.get_global_state() + + def get_box_names(self) -> list[BoxName]: + """Get all box names for the application. + + :return: List of box names + """ + return self._algorand.app.get_box_names(self._app_id) + + def get_box_value(self, name: BoxIdentifier) -> bytes: + """Get the value of a box. + + :param name: The box identifier + :return: The box value as bytes + """ + return self._algorand.app.get_box_value(self._app_id, name) + + def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + """Get a box value decoded according to an ABI type. + + :param name: The box identifier + :param abi_type: The ABI type to decode as + :return: The decoded box value + """ + return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type) + + def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]: + """Get values for multiple boxes. + + :param filter_func: Optional function to filter box names + :return: List of box values + """ + names = [n for n in self.get_box_names() if not filter_func or filter_func(n)] + values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names]) + return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)] + + def get_box_values_from_abi_type( + self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None + ) -> list[BoxABIValue]: + """Get multiple box values decoded according to an ABI type. + + :param abi_type: The ABI type to decode as + :param filter_func: Optional function to filter box names + :return: List of decoded box values + """ + names = self.get_box_names() + if filter_func: + names = [name for name in names if filter_func(name)] + + values = self._algorand.app.get_box_values_from_abi_type( + self.app_id, [name.name_raw for name in names], abi_type + ) + + return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)] + + def fund_app_account( + self, params: FundAppAccountParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Fund the application's account. + + :param params: The funding parameters + :param send_params: Send parameters, defaults to None + :return: The transaction result + """ + return self.send.fund_app_account(params, send_params) + + def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception: + source_info = None + if hasattr(self._app_spec, "source_info") and self._app_spec.source_info: + source_info = ( + self._app_spec.source_info.clear if is_clear_state_program else self._app_spec.source_info.approval + ) + + pc_offset_method = source_info.pc_offset_method if source_info else None + + program: bytes | None = None + if pc_offset_method == "cblocks": + 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=e, + app_spec=self._app_spec, + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=program, + approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None), + clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None), + ) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + 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 | TransactionSignerAccountProtocol | None + ) -> TransactionSigner | TransactionSignerAccountProtocol | None: + return signer or (self._default_signer if not sender or sender == self._default_sender else None) + + def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]: + 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: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None, + sender: str, + ) -> list[Any]: + method = self._app_spec.get_arc56_method(method_name_or_signature) + result: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] = [] + + for i, method_arg in enumerate(method.args): + arg_value = args[i] if args and i < len(args) else None + + if arg_value is not None: + 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 + + 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": + default_method = self._app_spec.get_arc56_method(default_value.data) + empty_args = [None] * len(default_method.args) + call_result = self.send.call( + AppClientMethodCallParams( + method=default_value.data, + args=empty_args, + sender=sender, + ) + ) + + if not call_result.abi_return: + raise ValueError("Default value method call did not return a value") + + if isinstance(call_result.abi_return, dict): + result.append( + get_abi_tuple_from_abi_struct( + call_result.abi_return, + self._app_spec.structs[str(default_method.returns.struct)], + self._app_spec.structs, + ) + ) + elif call_result.abi_return: + result.append(call_result.abi_return) + + case "local" | "global": + 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": + 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): + raise ValueError( + f"No value provided for required argument " + f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}" + ) + elif arg_value is None and default_value is None: + # At this point only allow explicit None values if no default value was identified + result.append(None) + + return result + + 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 = self._app_spec.get_arc56_method(params["method"]) + args = self._get_abi_args_with_default_values( + method_name_or_signature=params["method"], args=params.get("args"), sender=sender + ) + 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, + } + + def _process_method_call_return( + self, + result: Callable[[], SendAppUpdateTransactionResult[ABIReturn] | SendAppTransactionResult[ABIReturn]], + method: Method, + ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType] | SendAppTransactionResult[Arc56ReturnValueType]: + result_value = result() + abi_return = ( + result_value.abi_return.get_arc56_value(method, self._app_spec.structs) + if isinstance(result_value.abi_return, ABIReturn) + else None + ) + + if isinstance(result_value, SendAppUpdateTransactionResult): + return SendAppUpdateTransactionResult[Arc56ReturnValueType]( + **{**result_value.__dict__, "abi_return": abi_return} + ) + return SendAppTransactionResult[Arc56ReturnValueType](**{**result_value.__dict__, "abi_return": abi_return}) diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py new file mode 100644 index 00000000..830c3452 --- /dev/null +++ b/src/algokit_utils/applications/app_deployer.py @@ -0,0 +1,600 @@ +import base64 +import dataclasses +import json +from dataclasses import asdict, dataclass +from typing import Literal + +from algosdk.logic import get_application_address +from algosdk.v2client.indexer import IndexerClient + +from algokit_utils.applications.abi import ABIReturn +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.config import config +from algokit_utils.models.state import TealTemplateParams +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCallParams, + AppCreateParams, + AppDeleteMethodCallParams, + AppDeleteParams, + AppUpdateMethodCallParams, + AppUpdateParams, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import ( + AlgorandClientTransactionSender, + SendAppCreateTransactionResult, + SendAppTransactionResult, + SendAppUpdateTransactionResult, +) + +__all__ = [ + "APP_DEPLOY_NOTE_DAPP", + "AppDeployParams", + "AppDeployResult", + "AppDeployer", + "AppDeploymentMetaData", + "ApplicationLookup", + "ApplicationMetaData", + "ApplicationReference", + "OnSchemaBreak", + "OnUpdate", + "OperationPerformed", +] + + +APP_DEPLOY_NOTE_DAPP: str = "ALGOKIT_DEPLOYER" + +logger = config.logger + + +@dataclasses.dataclass +class AppDeploymentMetaData: + """Metadata about an application stored in a transaction note during creation.""" + + name: str + version: str + deletable: bool | None + updatable: bool | None + + def dictify(self) -> dict[str, str | bool]: + return {k: v for k, v in asdict(self).items() if v is not None} + + +@dataclasses.dataclass(frozen=True) +class ApplicationReference: + """Information about an Algorand app""" + + app_id: int + app_address: str + + +@dataclasses.dataclass(frozen=True) +class ApplicationMetaData: + """Complete metadata about a deployed app""" + + reference: ApplicationReference + deploy_metadata: AppDeploymentMetaData + created_round: int + updated_round: int + deleted: bool = False + + @property + def app_id(self) -> int: + return self.reference.app_id + + @property + def app_address(self) -> str: + return self.reference.app_address + + @property + def name(self) -> str: + return self.deploy_metadata.name + + @property + def version(self) -> str: + return self.deploy_metadata.version + + @property + def deletable(self) -> bool | None: + return self.deploy_metadata.deletable + + @property + def updatable(self) -> bool | None: + return self.deploy_metadata.updatable + + +@dataclasses.dataclass +class ApplicationLookup: + """Cache of {py:class}`ApplicationMetaData` for a specific `creator` + + Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple + apps or discovering multiple app_ids + """ + + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) + + +@dataclass(kw_only=True) +class AppDeployParams: + """Parameters for deploying an app""" + + metadata: AppDeploymentMetaData + deploy_time_params: TealTemplateParams | None = None + on_schema_break: (Literal["replace", "fail", "append"] | OnSchemaBreak) | None = None + on_update: (Literal["update", "replace", "fail", "append"] | OnUpdate) | None = None + create_params: AppCreateParams | AppCreateMethodCallParams + update_params: AppUpdateParams | AppUpdateMethodCallParams + delete_params: AppDeleteParams | AppDeleteMethodCallParams + existing_deployments: ApplicationLookup | None = None + ignore_cache: bool = False + max_fee: int | None = None + send_params: SendParams | None = None + + +# Union type for all possible deploy results +@dataclass(frozen=True) +class AppDeployResult: + app: ApplicationMetaData + operation_performed: OperationPerformed + create_result: SendAppCreateTransactionResult[ABIReturn] | None = None + update_result: SendAppUpdateTransactionResult[ABIReturn] | None = None + delete_result: SendAppTransactionResult[ABIReturn] | 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, ApplicationLookup] = {} + + def deploy(self, deployment: AppDeployParams) -> AppDeployResult: + # Create new instances with updated notes + send_params = deployment.send_params or SendParams() + suppress_log = send_params.get("suppress_log") or False + + logger.info( + f"Idempotently deploying app \"{deployment.metadata.name}\" from creator " + f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and " + f"{len(deployment.create_params.clear_state_program)} bytes of " + f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}", + suppress_log=suppress_log, + ) + note = TransactionComposer.arc2_note( + { + "dapp_name": APP_DEPLOY_NOTE_DAPP, + "format": "j", + "data": deployment.metadata.dictify(), + } + ) + create_params = dataclasses.replace(deployment.create_params, note=note) + update_params = dataclasses.replace(deployment.update_params, note=note) + + 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=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, + ) + + logger.debug("No detected changes in app, nothing to do.", suppress_log=suppress_log) + return AppDeployResult( + app=existing_app, + operation_performed=OperationPerformed.Nothing, + ) + + def _create_app( + self, + deployment: AppDeployParams, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Create a new application""" + + if isinstance(deployment.create_params, AppCreateMethodCallParams): + create_result = self._transaction_sender.app_create_method_call( + AppCreateMethodCallParams( + **{ + **asdict(deployment.create_params), + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ), + send_params=deployment.send_params, + ) + else: + create_result = self._transaction_sender.app_create( + AppCreateParams( + **{ + **asdict(deployment.create_params), + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ), + send_params=deployment.send_params, + ) + + app_metadata = ApplicationMetaData( + reference=ApplicationReference( + app_id=create_result.app_id, app_address=get_application_address(create_result.app_id) + ), + deploy_metadata=deployment.metadata, + created_round=create_result.confirmation.get("confirmed-round", 0) + if isinstance(create_result.confirmation, dict) + else 0, + updated_round=create_result.confirmation.get("confirmed-round", 0) + if isinstance(create_result.confirmation, dict) + else 0, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + return AppDeployResult( + app=app_metadata, + operation_performed=OperationPerformed.Create, + create_result=create_result, + ) + + def _replace_app( + self, + deployment: AppDeployParams, + existing_app: ApplicationMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + composer = self._transaction_sender.new_group() + + # Add create transaction + if isinstance(deployment.create_params, AppCreateMethodCallParams): + composer.add_app_create_method_call( + AppCreateMethodCallParams( + **{ + **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, + } + ) + ) + create_txn_index = composer.count() - 1 + + # Add delete transaction + if isinstance(deployment.delete_params, AppDeleteMethodCallParams): + delete_call_params = AppDeleteMethodCallParams( + **{ + **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) + delete_txn_index = composer.count() - 1 + + result = composer.send() + + create_result = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index) + delete_result = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index) + + app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload] + app_metadata = ApplicationMetaData( + reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)), + deploy_metadata=deployment.metadata, + created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload] + deleted=False, + ) + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + return AppDeployResult( + app=app_metadata, + operation_performed=OperationPerformed.Replace, + create_result=create_result, + update_result=None, + delete_result=delete_result, + ) + + def _update_app( + self, + deployment: AppDeployParams, + existing_app: ApplicationMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + """Update an existing application""" + + if isinstance(deployment.update_params, AppUpdateMethodCallParams): + result = self._transaction_sender.app_update_method_call( + AppUpdateMethodCallParams( + **{ + **deployment.update_params.__dict__, + "app_id": existing_app.app_id, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ), + send_params=deployment.send_params, + ) + 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, + } + ), + send_params=deployment.send_params, + ) + + app_metadata = ApplicationMetaData( + reference=ApplicationReference(app_id=existing_app.app_id, app_address=existing_app.app_address), + deploy_metadata=deployment.metadata, + created_round=existing_app.created_round, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + deleted=False, + ) + + self._update_app_lookup(deployment.create_params.sender, app_metadata) + + return AppDeployResult( + app=app_metadata, + operation_performed=OperationPerformed.Update, + update_result=result, + ) + + def _handle_schema_break( + self, + deployment: AppDeployParams, + existing_app: ApplicationMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail") or deployment.on_schema_break is None: + raise ValueError( + "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "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: ApplicationMetaData, + approval_program: bytes, + clear_program: bytes, + ) -> AppDeployResult: + if deployment.on_update in (OnUpdate.Fail, "fail") or deployment.on_update is None: + raise ValueError( + "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." + ) + + if deployment.on_update in (OnUpdate.AppendApp, "append"): + return self._create_app(deployment, approval_program, clear_program) + + if deployment.on_update in (OnUpdate.UpdateApp, "update"): + if existing_app.updatable: + return self._update_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app") + + if deployment.on_update in (OnUpdate.ReplaceApp, "replace"): + if existing_app.deletable: + return self._replace_app(deployment, existing_app, approval_program, clear_program) + else: + raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app") + + raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}") + + def _update_app_lookup(self, sender: str, app_metadata: ApplicationMetaData) -> None: + """Update the app lookup cache""" + + lookup = self._app_lookups.get(sender) + if not lookup: + self._app_lookups[sender] = ApplicationLookup( + creator=sender, + apps={app_metadata.name: app_metadata}, + ) + else: + lookup.apps[app_metadata.name] = app_metadata + + def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> ApplicationLookup: + """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, ApplicationMetaData] = {} + + # 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=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"]] = ApplicationMetaData( + reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)), + deploy_metadata=AppDeploymentMetaData( + name=metadata["name"], + version=metadata.get("version", "1.0"), + deletable=metadata.get("deletable"), + updatable=metadata.get("updatable"), + ), + created_round=creation_txn["confirmed-round"], + 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 = ApplicationLookup(creator=creator_address, apps=app_lookup) + self._app_lookups[creator_address] = lookup + return lookup diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py new file mode 100644 index 00000000..3b0fb423 --- /dev/null +++ b/src/algokit_utils/applications/app_factory.py @@ -0,0 +1,826 @@ +import base64 +import dataclasses +from collections.abc import Callable, Sequence +from dataclasses import asdict, dataclass +from typing import Any, Generic, TypeVar + +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction +from typing_extensions import Self + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.abi import ( + ABIReturn, + Arc56ReturnValueType, + get_abi_decoded_value, + get_abi_tuple_from_abi_struct, +) +from algokit_utils.applications.app_client import ( + AppClient, + AppClientBareCallCreateParams, + AppClientBareCallParams, + AppClientCompilationParams, + AppClientCompilationResult, + AppClientCreateSchema, + AppClientMethodCallCreateParams, + AppClientMethodCallParams, + AppClientParams, + CreateOnComplete, +) +from algokit_utils.applications.app_deployer import ( + AppDeploymentMetaData, + AppDeployParams, + AppDeployResult, + ApplicationLookup, + ApplicationMetaData, + OnSchemaBreak, + OnUpdate, + OperationPerformed, +) +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Method +from algokit_utils.models.application import ( + AppSourceMaps, +) +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCallParams, + AppCreateParams, + AppDeleteMethodCallParams, + AppDeleteParams, + AppUpdateMethodCallParams, + AppUpdateParams, + BuiltTransactions, +) +from algokit_utils.transactions.transaction_sender import ( + SendAppCreateTransactionResult, + SendAppTransactionResult, + SendAppUpdateTransactionResult, + SendSingleTransactionResult, +) + +T = TypeVar("T") + +__all__ = [ + "AppFactory", + "AppFactoryCreateMethodCallParams", + "AppFactoryCreateMethodCallResult", + "AppFactoryCreateParams", + "AppFactoryDeployResult", + "AppFactoryParams", + "SendAppCreateFactoryTransactionResult", + "SendAppFactoryTransactionResult", + "SendAppUpdateFactoryTransactionResult", +] + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryParams: + algorand: AlgorandClient + app_spec: Arc56Contract | ApplicationSpecification | str + app_name: str | None = None + default_sender: str | None = None + default_signer: TransactionSigner | None = None + version: str | None = None + compilation_params: AppClientCompilationParams | None = None + + +@dataclass(kw_only=True, frozen=True) +class _AppFactoryCreateBaseParams(AppClientCreateSchema): + on_complete: CreateOnComplete | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateParams(_AppFactoryCreateBaseParams, AppClientBareCallParams): + pass + + +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateMethodCallParams(_AppFactoryCreateBaseParams, AppClientMethodCallParams): + pass + + +ABIReturnT = TypeVar( + "ABIReturnT", + bound=Arc56ReturnValueType, +) + + +@dataclass(frozen=True, kw_only=True) +class AppFactoryCreateMethodCallResult(SendSingleTransactionResult, Generic[ABIReturnT]): + app_id: int + app_address: str + compiled_approval: Any | None = None + compiled_clear: Any | None = None + abi_return: ABIReturnT | None = None + + +@dataclass(frozen=True) +class SendAppFactoryTransactionResult(SendAppTransactionResult[Arc56ReturnValueType]): + pass + + +@dataclass(frozen=True) +class SendAppUpdateFactoryTransactionResult(SendAppUpdateTransactionResult[Arc56ReturnValueType]): + pass + + +@dataclass(frozen=True, kw_only=True) +class SendAppCreateFactoryTransactionResult(SendAppCreateTransactionResult[Arc56ReturnValueType]): + pass + + +@dataclass(frozen=True) +class AppFactoryDeployResult: + """Result from deploying an application via AppFactory""" + + app: ApplicationMetaData + operation_performed: OperationPerformed + create_result: SendAppCreateFactoryTransactionResult | None = None + update_result: SendAppUpdateFactoryTransactionResult | None = None + delete_result: SendAppFactoryTransactionResult | None = None + + @classmethod + def from_deploy_result( + cls, + response: AppDeployResult, + deploy_params: AppDeployParams, + app_spec: Arc56Contract, + app_compilation_data: AppClientCompilationResult | None = None, + ) -> Self: + def to_factory_result( + response_data: SendAppTransactionResult[ABIReturn] + | SendAppCreateTransactionResult + | SendAppUpdateTransactionResult + | None, + params: Any, # noqa: ANN401 + ) -> Any | None: # noqa: ANN401 + if not response_data: + return None + + response_data_dict = asdict(response_data) + abi_return = response_data.abi_return + if abi_return and abi_return.method: + response_data_dict["abi_return"] = abi_return.get_arc56_value(params.method, app_spec.structs) + + match response_data: + case SendAppCreateTransactionResult(): + return SendAppCreateFactoryTransactionResult(**response_data_dict) + case SendAppUpdateTransactionResult(): + response_data_dict["compiled_approval"] = ( + app_compilation_data.compiled_approval if app_compilation_data else None + ) + response_data_dict["compiled_clear"] = ( + app_compilation_data.compiled_clear if app_compilation_data else None + ) + return SendAppUpdateFactoryTransactionResult(**response_data_dict) + case SendAppTransactionResult(): + return SendAppFactoryTransactionResult(**response_data_dict) + + return cls( + app=response.app, + operation_performed=response.operation_performed, + create_result=to_factory_result( + response.create_result, + deploy_params.create_params, + ), + update_result=to_factory_result( + response.update_result, + deploy_params.update_params, + ), + delete_result=to_factory_result( + response.delete_result, + deploy_params.delete_params, + ), + ) + + +class _BareParamsBuilder: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create( + self, params: AppFactoryCreateParams | None = None, compilation_params: AppClientCompilationParams | None = None + ) -> AppCreateParams: + base_params = params or AppFactoryCreateParams() + compiled = self._factory.compile(compilation_params) + + return AppCreateParams( + **{ + **{ + param: value + for param, value in asdict(base_params).items() + if param in {f.name for f in dataclasses.fields(AppCreateParams)} + }, + "approval_program": compiled.approval_program, + "clear_state_program": compiled.clear_state_program, + "schema": base_params.schema + or { + "global_byte_slices": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_byte_slices": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, + }, + "sender": self._factory._get_sender(base_params.sender), + "signer": self._factory._get_signer(base_params.sender, base_params.signer), + "on_complete": base_params.on_complete or OnComplete.NoOpOC, + } + ) + + def deploy_update(self, params: AppClientBareCallParams | None = None) -> AppUpdateParams: + return AppUpdateParams( + **{ + **{ + param: value + for param, value in asdict(params or AppClientBareCallParams()).items() + if param in {f.name for f in dataclasses.fields(AppUpdateParams)} + }, + "app_id": 0, + "approval_program": "", + "clear_state_program": "", + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.UpdateApplicationOC, + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + } + ) + + def deploy_delete(self, params: AppClientBareCallParams | None = None) -> AppDeleteParams: + return AppDeleteParams( + **{ + **{ + param: value + for param, value in asdict(params or AppClientBareCallParams()).items() + if param in {f.name for f in dataclasses.fields(AppDeleteParams)} + }, + "app_id": 0, + "sender": self._factory._get_sender(params.sender if params else None), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "on_complete": OnComplete.DeleteApplicationOC, + } + ) + + +class _MethodParamsBuilder: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _BareParamsBuilder(factory) + + @property + def bare(self) -> _BareParamsBuilder: + return self._bare + + def create( + self, params: AppFactoryCreateMethodCallParams, compilation_params: AppClientCompilationParams | None = None + ) -> AppCreateMethodCallParams: + compiled = self._factory.compile(compilation_params) + + return AppCreateMethodCallParams( + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppCreateMethodCallParams)} + }, + "app_id": 0, + "approval_program": compiled.approval_program, + "clear_state_program": compiled.clear_state_program, + "schema": params.schema + or { + "global_byte_slices": self._factory._app_spec.state.schema.global_state.bytes, + "global_ints": self._factory._app_spec.state.schema.global_state.ints, + "local_byte_slices": self._factory._app_spec.state.schema.local_state.bytes, + "local_ints": self._factory._app_spec.state.schema.local_state.ints, + }, + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": params.on_complete or OnComplete.NoOpOC, + } + ) + + def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCallParams: + return AppUpdateMethodCallParams( + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppUpdateMethodCallParams)} + }, + "app_id": 0, + "approval_program": "", + "clear_state_program": "", + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory._app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": OnComplete.UpdateApplicationOC, + } + ) + + def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams: + return AppDeleteMethodCallParams( + **{ + **{ + param: value + for param, value in asdict(params).items() + if param in {f.name for f in dataclasses.fields(AppDeleteMethodCallParams)} + }, + "app_id": 0, + "sender": self._factory._get_sender(params.sender), + "signer": self._factory._get_signer( + params.sender if params else None, params.signer if params else None + ), + "method": self._factory.app_spec.get_arc56_method(params.method).to_abi_method(), + "args": self._factory._get_create_abi_args_with_default_values(params.method, params.args), + "on_complete": OnComplete.DeleteApplicationOC, + } + ) + + +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 _TransactionCreator: + 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: AppFactoryCreateParams | None = None, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> tuple[AppClient, SendAppCreateTransactionResult]: + compilation_params = compilation_params or AppClientCompilationParams() + compilation_params["updatable"] = ( + compilation_params.get("updatable") + if compilation_params.get("updatable") is not None + else self._factory._updatable + ) + compilation_params["deletable"] = ( + compilation_params.get("deletable") + if compilation_params.get("deletable") is not None + else self._factory._deletable + ) + compilation_params["deploy_time_params"] = ( + compilation_params.get("deploy_time_params") + if compilation_params.get("deploy_time_params") is not None + else self._factory._deploy_time_params + ) + + compiled = self._factory.compile(compilation_params) + + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create( + self._factory.params.bare.create(params, compilation_params), send_params + ) + ) + + return ( + self._factory.get_app_client_by_id( + app_id=result.app_id, + ), + SendAppCreateTransactionResult[ABIReturn]( + transaction=result.transaction, + confirmation=result.confirmation, + app_id=result.app_id, + app_address=result.app_address, + compiled_approval=compiled.compiled_approval if compiled else None, + compiled_clear=compiled.compiled_clear if compiled else None, + group_id=result.group_id, + tx_ids=result.tx_ids, + transactions=result.transactions, + confirmations=result.confirmations, + ), + ) + + +class _TransactionSender: + 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, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> tuple[AppClient, AppFactoryCreateMethodCallResult[Arc56ReturnValueType]]: + compilation_params = compilation_params or AppClientCompilationParams() + compilation_params["updatable"] = ( + compilation_params.get("updatable") + if compilation_params.get("updatable") is not None + else self._factory._updatable + ) + compilation_params["deletable"] = ( + compilation_params.get("deletable") + if compilation_params.get("deletable") is not None + else self._factory._deletable + ) + compilation_params["deploy_time_params"] = ( + compilation_params.get("deploy_time_params") + if compilation_params.get("deploy_time_params") is not None + else self._factory._deploy_time_params + ) + + compiled = self._factory.compile(compilation_params) + result = self._factory._handle_call_errors( + lambda: self._factory._parse_method_call_return( + lambda: self._algorand.send.app_create_method_call( + self._factory.params.create(params, compilation_params), send_params + ), + self._factory._app_spec.get_arc56_method(params.method), + ) + ) + + return ( + self._factory.get_app_client_by_id( + app_id=result.app_id, + ), + AppFactoryCreateMethodCallResult[Arc56ReturnValueType]( + transaction=result.transaction, + confirmation=result.confirmation, + tx_id=result.tx_id, + app_id=result.app_id, + app_address=result.app_address, + abi_return=result.abi_return, + compiled_approval=compiled.compiled_approval if compiled else None, + compiled_clear=compiled.compiled_clear if compiled else None, + group_id=result.group_id, + tx_ids=result.tx_ids, + transactions=result.transactions, + confirmations=result.confirmations, + returns=result.returns, + ), + ) + + +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._approval_source_map: SourceMap | None = None + self._clear_source_map: SourceMap | None = None + self._params_accessor = _MethodParamsBuilder(self) + self._send_accessor = _TransactionSender(self) + self._create_transaction_accessor = _TransactionCreator(self) + + compilation_params = params.compilation_params or AppClientCompilationParams() + self._deploy_time_params = compilation_params.get("deploy_time_params") + self._updatable = compilation_params.get("updatable") + self._deletable = compilation_params.get("deletable") + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def algorand(self) -> AlgorandClient: + return self._algorand + + @property + def params(self) -> _MethodParamsBuilder: + return self._params_accessor + + @property + def send(self) -> _TransactionSender: + return self._send_accessor + + @property + def create_transaction(self) -> _TransactionCreator: + return self._create_transaction_accessor + + def deploy( + self, + *, + on_update: OnUpdate | None = None, + on_schema_break: OnSchemaBreak | None = None, + create_params: AppClientMethodCallCreateParams | AppClientBareCallCreateParams | None = None, + update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + existing_deployments: ApplicationLookup | None = None, + ignore_cache: bool = False, + app_name: str | None = None, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> tuple[AppClient, AppFactoryDeployResult]: + """Deploy the application with the specified parameters.""" + # Resolve control parameters with factory defaults + send_params = send_params or SendParams() + compilation_params = compilation_params or AppClientCompilationParams() + resolved_updatable = ( + upd + if (upd := compilation_params.get("updatable")) is not None + else self._updatable or self._get_deploy_time_control("updatable") + ) + resolved_deletable = ( + dlb + if (dlb := compilation_params.get("deletable")) is not None + else self._deletable or self._get_deploy_time_control("deletable") + ) + resolved_deploy_time_params = compilation_params.get("deploy_time_params") or self._deploy_time_params + + def prepare_create_args() -> AppCreateMethodCallParams | AppCreateParams: + """Prepare create arguments based on parameter type.""" + if create_params and isinstance(create_params, AppClientMethodCallCreateParams): + return self.params.create( + AppFactoryCreateMethodCallParams( + **asdict(create_params), + ), + compilation_params={ + "updatable": resolved_updatable, + "deletable": resolved_deletable, + "deploy_time_params": resolved_deploy_time_params, + }, + ) + + base_params = create_params or AppClientBareCallCreateParams() + return self.params.bare.create( + AppFactoryCreateParams( + **asdict(base_params) if base_params else {}, + ), + compilation_params={ + "updatable": resolved_updatable, + "deletable": resolved_deletable, + "deploy_time_params": resolved_deploy_time_params, + }, + ) + + def prepare_update_args() -> AppUpdateMethodCallParams | AppUpdateParams: + """Prepare update arguments based on parameter type.""" + return ( + self.params.deploy_update(update_params) + if isinstance(update_params, AppClientMethodCallParams) + else self.params.bare.deploy_update(update_params) + ) + + def prepare_delete_args() -> AppDeleteMethodCallParams | AppDeleteParams: + """Prepare delete arguments based on parameter type.""" + return ( + self.params.deploy_delete(delete_params) + if isinstance(delete_params, AppClientMethodCallParams) + else self.params.bare.deploy_delete(delete_params) + ) + + # Execute deployment + deploy_params = AppDeployParams( + deploy_time_params=resolved_deploy_time_params, + on_schema_break=on_schema_break, + on_update=on_update, + existing_deployments=existing_deployments, + ignore_cache=ignore_cache, + create_params=prepare_create_args(), + update_params=prepare_update_args(), + delete_params=prepare_delete_args(), + metadata=AppDeploymentMetaData( + name=app_name or self._app_name, + version=self._version, + updatable=resolved_updatable, + deletable=resolved_deletable, + ), + send_params=send_params, + ) + deploy_result = self._algorand.app_deployer.deploy(deploy_params) + + # Prepare app client and factory deploy response + app_client = self.get_app_client_by_id( + app_id=deploy_result.app.app_id, + app_name=app_name, + default_sender=self._default_sender, + default_signer=self._default_signer, + ) + factory_deploy_result = AppFactoryDeployResult.from_deploy_result( + response=deploy_result, + deploy_params=deploy_params, + app_spec=app_client.app_spec, + app_compilation_data=self.compile( + AppClientCompilationParams( + deploy_time_params=resolved_deploy_time_params, + updatable=resolved_updatable, + deletable=resolved_deletable, + ) + ), + ) + + return app_client, factory_deploy_result + + def get_app_client_by_id( + self, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, # Address can be string or bytes + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> 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 get_app_client_by_creator_and_name( + self, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: ApplicationLookup | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient.from_creator_and_name( + creator_address=creator_address, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + app_spec=self._app_spec, + algorand=self._algorand, + ) + + def export_source_maps(self) -> AppSourceMaps: + 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_params: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + compilation = compilation_params or AppClientCompilationParams() + result = AppClient.compile( + app_spec=self._app_spec, + app_manager=self._algorand.app, + compilation_params=compilation, + ) + + 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 _expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: # noqa: FBT002 FBT001 + return AppClient._expose_logic_error_static( + e=e, + app_spec=self._app_spec, + is_clear_state_program=is_clear_state_program, + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + program=None, + approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None), + clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None), + ) + + def _get_deploy_time_control(self, control: str) -> bool | None: + approval = self._app_spec.source.get_decoded_approval() if self._app_spec.source else None + + template_name = UPDATABLE_TEMPLATE_NAME if control == "updatable" else DELETABLE_TEMPLATE_NAME + if not approval or template_name not in approval: + return None + + on_complete = "UpdateApplication" if control == "updatable" else "DeleteApplication" + return on_complete in self._app_spec.bare_actions.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 | 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 _get_signer(self, sender: str | None, signer: TransactionSigner | None) -> TransactionSigner | None: + return signer or (self._default_signer if not sender or sender == self._default_sender else None) + + def _handle_call_errors(self, call: Callable[[], T]) -> T: + try: + return call() + except Exception as e: + raise self._expose_logic_error(e) from None + + def _parse_method_call_return( + self, + result: Callable[ + [], SendAppTransactionResult | SendAppCreateTransactionResult | SendAppUpdateTransactionResult + ], + method: Method, + ) -> AppFactoryCreateMethodCallResult[Arc56ReturnValueType]: + result_value = result() + return AppFactoryCreateMethodCallResult[Arc56ReturnValueType]( + **{ + **result_value.__dict__, + "abi_return": result_value.abi_return.get_arc56_value(method, self._app_spec.structs) + if isinstance(result_value.abi_return, ABIReturn) + else None, + } + ) + + def _get_create_abi_args_with_default_values( + self, + method_name_or_signature: str, + user_args: Sequence[Any] | None, + ) -> list[Any]: + """ + Builds a list of ABI argument values for creation calls, applying default + argument values when not provided. + """ + method = self._app_spec.get_arc56_method(method_name_or_signature) + + results: list[Any] = [] + + for i, param in enumerate(method.args): + if user_args and i < len(user_args): + arg_value = user_args[i] + if param.struct and isinstance(arg_value, dict): + arg_value = get_abi_tuple_from_abi_struct( + arg_value, + self._app_spec.structs[param.struct], + self._app_spec.structs, + ) + results.append(arg_value) + continue + + default_value = getattr(param, "default_value", None) + if default_value: + if default_value.source == "literal": + raw_value = base64.b64decode(default_value.data) + value_type = default_value.type or str(param.type) + decoded_value = get_abi_decoded_value(raw_value, value_type, self._app_spec.structs) + results.append(decoded_value) + else: + raise ValueError( + f"Cannot provide default value from source={default_value.source} " + "for a contract creation call." + ) + else: + param_name = param.name or f"arg{i + 1}" + raise ValueError( + f"No value provided for required argument {param_name} " f"in call to method {method.name}" + ) + + return results diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py new file mode 100644 index 00000000..54269963 --- /dev/null +++ b/src/algokit_utils/applications/app_manager.py @@ -0,0 +1,470 @@ +import base64 +from collections.abc import Mapping +from typing import Any, cast + +import algosdk +import algosdk.atomic_transaction_composer +import algosdk.box_reference +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.box_reference import BoxReference as AlgosdkBoxReference +from algosdk.logic import get_application_address +from algosdk.source_map import SourceMap +from algosdk.v2client import algod + +from algokit_utils.applications.abi import ABIReturn, ABIType, ABIValue +from algokit_utils.models.application import ( + AppInformation, + AppState, + CompiledTeal, +) +from algokit_utils.models.state import BoxIdentifier, BoxName, BoxReference, DataTypeFlag, TealTemplateParams + +__all__ = [ + "DELETABLE_TEMPLATE_NAME", + "UPDATABLE_TEMPLATE_NAME", + "AppManager", +] + + +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.""" + + +def _is_valid_token_character(char: str) -> bool: + return char.isalnum() or char == "_" + + +def _last_token_base64(line: str, idx: int) -> bool: + try: + *_, last = line[:idx].split() + except ValueError: + return False + return last in ("base64", "b64") + + +def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + if end < 0: + end = len(line) + + idx = start + while idx < end: + token_idx = _find_unquoted_string(line, token, idx, end) + if token_idx is None: + break + trailing_idx = token_idx + len(token) + if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( + trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) + ): + return token_idx + idx = trailing_idx + return None + + +def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: + if end < 0: + end = len(line) + idx = start + in_quotes = in_base64 = False + while idx < end: + current_char = line[idx] + match current_char: + case " " | "(" if not in_quotes and _last_token_base64(line, idx): + in_base64 = True + case " " | ")" if not in_quotes and in_base64: + in_base64 = False + case "\\" if in_quotes: + idx += 1 + case '"': + in_quotes = not in_quotes + case _ if not in_quotes and not in_base64 and line.startswith(token, idx): + return idx + idx += 1 + return None + + +def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: + result: list[str] = [] + match_count = 0 + token = f"TMPL_{template_variable}" 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, "//") + if comment_idx is None: + comment_idx = len(line) + code = line[:comment_idx] + comment = line[comment_idx:] + trailing_idx = 0 + while True: + token_idx = _find_template_token(code, token, trailing_idx) + if token_idx is None: + break + + trailing_idx = token_idx + len(token) + prefix = code[:token_idx] + suffix = code[trailing_idx:] + code = f"{prefix}{value}{suffix}" + match_count += 1 + trailing_idx += token_idx_offset + result.append(code + comment) + return result, match_count + + +class AppManager: + """A manager class for interacting with Algorand applications. + + Provides functionality for compiling TEAL code, managing application state, + and interacting with application boxes. + + :param algod_client: The Algorand client instance to use for interacting with the network + """ + + def __init__(self, algod_client: algod.AlgodClient): + self._algod = algod_client + self._compilation_results: dict[str, CompiledTeal] = {} + + def compile_teal(self, teal_code: str) -> CompiledTeal: + """Compile TEAL source code. + + :param teal_code: The TEAL source code to compile + :return: The compiled TEAL code and associated metadata + """ + + if teal_code in self._compilation_results: + return self._compilation_results[teal_code] + + compiled = self._algod.compile(teal_code, source_map=True) + result = CompiledTeal( + teal=teal_code, + compiled=compiled["result"], + compiled_hash=compiled["hash"], + compiled_base64_to_bytes=base64.b64decode(compiled["result"]), + source_map=SourceMap(compiled.get("sourcemap", {})), + ) + self._compilation_results[teal_code] = result + return result + + def compile_teal_template( + self, + teal_template_code: str, + template_params: TealTemplateParams | None = None, + deployment_metadata: Mapping[str, bool | None] | None = None, + ) -> CompiledTeal: + """Compile a TEAL template with parameters. + + :param teal_template_code: The TEAL template code to compile + :param template_params: Parameters to substitute in the template + :param deployment_metadata: Deployment control parameters + :return: The compiled TEAL code and associated metadata + """ + + teal_code = AppManager.strip_teal_comments(teal_template_code) + teal_code = AppManager.replace_template_variables(teal_code, template_params or {}) + + if deployment_metadata: + teal_code = AppManager.replace_teal_template_deploy_time_control_params(teal_code, deployment_metadata) + + return self.compile_teal(teal_code) + + def get_compilation_result(self, teal_code: str) -> CompiledTeal | None: + """Get cached compilation result for TEAL code if available. + + :param teal_code: The TEAL source code + :return: The cached compilation result if available, None otherwise + """ + + return self._compilation_results.get(teal_code) + + def get_by_id(self, app_id: int) -> AppInformation: + """Get information about an application by ID. + + :param app_id: The application ID + :return: Information about the application + """ + + app = self._algod.application_info(app_id) + assert isinstance(app, dict) + app_params = app["params"] + + return AppInformation( + app_id=app_id, + app_address=get_application_address(app_id), + approval_program=base64.b64decode(app_params["approval-program"]), + clear_state_program=base64.b64decode(app_params["clear-state-program"]), + creator=app_params["creator"], + local_ints=app_params["local-state-schema"]["num-uint"], + local_byte_slices=app_params["local-state-schema"]["num-byte-slice"], + global_ints=app_params["global-state-schema"]["num-uint"], + global_byte_slices=app_params["global-state-schema"]["num-byte-slice"], + extra_program_pages=app_params.get("extra-program-pages", 0), + global_state=self.decode_app_state(app_params.get("global-state", [])), + ) + + def get_global_state(self, app_id: int) -> dict[str, AppState]: + """Get the global state of an application. + + :param app_id: The application ID + :return: The application's global state + """ + + return self.get_by_id(app_id).global_state + + def get_local_state(self, app_id: int, address: str) -> dict[str, AppState]: + """Get the local state for an account in an application. + + :param app_id: The application ID + :param address: The account address + :return: The account's local state for the application + :raises ValueError: If local state is not found + """ + + app_info = self._algod.account_application_info(address, app_id) + assert isinstance(app_info, dict) + if not app_info.get("app-local-state", {}).get("key-value"): + raise ValueError("Couldn't find local state") + return self.decode_app_state(app_info["app-local-state"]["key-value"]) + + def get_box_names(self, app_id: int) -> list[BoxName]: + """Get names of all boxes for an application. + + :param app_id: The application ID + :return: List of box names + """ + + box_result = self._algod.application_boxes(app_id) + assert isinstance(box_result, dict) + return [ + BoxName( + name_raw=base64.b64decode(b["name"]), + name_base64=b["name"], + name=base64.b64decode(b["name"]).decode("utf-8"), + ) + for b in box_result["boxes"] + ] + + def get_box_value(self, app_id: int, box_name: BoxIdentifier) -> bytes: + """Get the value stored in a box. + + :param app_id: The application ID + :param box_name: The box identifier + :return: The box value as bytes + """ + + name = AppManager.get_box_reference(box_name)[1] + box_result = self._algod.application_box_by_name(app_id, name) + assert isinstance(box_result, dict) + return base64.b64decode(box_result["value"]) + + def get_box_values(self, app_id: int, box_names: list[BoxIdentifier]) -> list[bytes]: + """Get values for multiple boxes. + + :param app_id: The application ID + :param box_names: List of box identifiers + :return: List of box values as bytes + """ + + return [self.get_box_value(app_id, box_name) for box_name in box_names] + + def get_box_value_from_abi_type(self, app_id: int, box_name: BoxIdentifier, abi_type: ABIType) -> ABIValue: + """Get and decode a box value using an ABI type. + + :param app_id: The application ID + :param box_name: The box identifier + :param abi_type: The ABI type to decode with + :return: The decoded box value + :raises ValueError: If decoding fails + """ + + value = self.get_box_value(app_id, box_name) + try: + parse_to_tuple = isinstance(abi_type, algosdk.abi.TupleType) + decoded_value = abi_type.decode(value) + return tuple(decoded_value) if parse_to_tuple else decoded_value + except Exception as e: + raise ValueError(f"Failed to decode box value {value.decode('utf-8')} with ABI type {abi_type}") from e + + def get_box_values_from_abi_type( + self, app_id: int, box_names: list[BoxIdentifier], abi_type: ABIType + ) -> list[ABIValue]: + """Get and decode multiple box values using an ABI type. + + :param app_id: The application ID + :param box_names: List of box identifiers + :param abi_type: The ABI type to decode with + :return: List of decoded box values + """ + + return [self.get_box_value_from_abi_type(app_id, box_name, abi_type) for box_name in box_names] + + @staticmethod + def get_box_reference(box_id: BoxIdentifier | BoxReference) -> tuple[int, bytes]: + """Get standardized box reference from various identifier types. + + :param box_id: The box identifier + :return: Tuple of (app_id, box_name_bytes) + :raises ValueError: If box identifier type is invalid + """ + + if isinstance(box_id, (BoxReference | AlgosdkBoxReference)): + return box_id.app_index, box_id.name + + 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 + ) -> ABIReturn | None: + """Get the ABI return value from a transaction confirmation. + + :param confirmation: The transaction confirmation + :param method: The ABI method + :return: The parsed ABI return value, or None if not available + """ + + if not method: + return None + + atc = algosdk.atomic_transaction_composer.AtomicTransactionComposer() + abi_result = atc.parse_result( + method, + "dummy_txn", + confirmation, # type: ignore[arg-type] + ) + + if not abi_result: + return None + + return ABIReturn(abi_result) + + @staticmethod + def decode_app_state(state: list[dict[str, Any]]) -> dict[str, AppState]: + """Decode application state from raw format. + + :param state: The raw application state + :return: Decoded application state + :raises ValueError: If unknown state data type is encountered + """ + + state_values: dict[str, AppState] = {} + + def decode_bytes_to_str(value: bytes) -> str: + try: + return value.decode("utf-8") + except UnicodeDecodeError: + return value.hex() + + for state_val in state: + key_base64 = state_val["key"] + key_raw = base64.b64decode(key_base64) + key = decode_bytes_to_str(key_raw) + teal_value = state_val["value"] + + data_type_flag = teal_value.get("action", teal_value.get("type")) + + if data_type_flag == DataTypeFlag.BYTES: + value_base64 = teal_value.get("bytes", "") + value_raw = base64.b64decode(value_base64) + state_values[key] = AppState( + key_raw=key_raw, + key_base64=key_base64, + value_raw=value_raw, + value_base64=value_base64, + value=decode_bytes_to_str(value_raw), + ) + elif data_type_flag == DataTypeFlag.UINT: + value = teal_value.get("uint", 0) + state_values[key] = AppState( + key_raw=key_raw, + key_base64=key_base64, + value_raw=None, + value_base64=None, + value=int(value), + ) + else: + raise ValueError(f"Received unknown state data type of {data_type_flag}") + + return state_values + + @staticmethod + def replace_template_variables(program: str, template_values: TealTemplateParams) -> str: + """Replace template variables in TEAL code. + + :param program: The TEAL program code + :param template_values: Template variable values to substitute + :return: TEAL code with substituted values + :raises ValueError: If template value type is unexpected + """ + + program_lines = program.splitlines() + for template_variable_name, template_value in template_values.items(): + match template_value: + case int(): + value = str(template_value) + case str(): + value = "0x" + template_value.encode("utf-8").hex() + case bytes(): + value = "0x" + template_value.hex() + case _: + raise ValueError( + f"Unexpected template value type {template_variable_name}: {template_value.__class__}" + ) + + program_lines, _ = _replace_template_variable(program_lines, template_variable_name, value) + + return "\n".join(program_lines) + + @staticmethod + def replace_teal_template_deploy_time_control_params( + teal_template_code: str, params: Mapping[str, bool | None] + ) -> str: + """Replace deploy-time control parameters in TEAL template. + + :param teal_template_code: The TEAL template code + :param params: The deploy-time control parameters + :return: TEAL code with substituted control parameters + :raises ValueError: If template variables not found in code + """ + + updatable = params.get("updatable") + if updatable is not None: + if UPDATABLE_TEMPLATE_NAME not in teal_template_code: + raise ValueError( + f"Deploy-time updatability control requested for app deployment, but {UPDATABLE_TEMPLATE_NAME} " + "not present in TEAL code" + ) + teal_template_code = teal_template_code.replace(UPDATABLE_TEMPLATE_NAME, str(int(updatable))) + + deletable = params.get("deletable") + if deletable is not None: + if DELETABLE_TEMPLATE_NAME not in teal_template_code: + raise ValueError( + f"Deploy-time deletability control requested for app deployment, but {DELETABLE_TEMPLATE_NAME} " + "not present in TEAL code" + ) + teal_template_code = teal_template_code.replace(DELETABLE_TEMPLATE_NAME, str(int(deletable))) + + return teal_template_code + + @staticmethod + def strip_teal_comments(teal_code: str) -> str: + def _strip_comment(line: str) -> str: + comment_idx = _find_unquoted_string(line, "//") + if comment_idx is None: + return line + return line[:comment_idx].rstrip() + + return "\n".join(_strip_comment(line) for line in teal_code.splitlines()) diff --git a/src/algokit_utils/applications/app_spec/__init__.py b/src/algokit_utils/applications/app_spec/__init__.py new file mode 100644 index 00000000..dbbb41fb --- /dev/null +++ b/src/algokit_utils/applications/app_spec/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.applications.app_spec.arc32 import * # noqa: F403 +from algokit_utils.applications.app_spec.arc56 import * # noqa: F403 diff --git a/src/algokit_utils/applications/app_spec/arc32.py b/src/algokit_utils/applications/app_spec/arc32.py new file mode 100644 index 00000000..ff3b8f6b --- /dev/null +++ b/src/algokit_utils/applications/app_spec/arc32.py @@ -0,0 +1,207 @@ +import base64 +import dataclasses +import json +from enum import IntFlag +from pathlib import Path +from typing import Any, Literal, TypeAlias, TypedDict + +from algosdk.abi import Contract +from algosdk.abi.method import MethodDict +from algosdk.transaction import StateSchema + +__all__ = [ + "AppSpecStateDict", + "Arc32Contract", + "CallConfig", + "DefaultArgumentDict", + "DefaultArgumentType", + "MethodConfigDict", + "MethodHints", + "OnCompleteActionName", + "StateDict", + "StructArgDict", +] + + +AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]] +"""Type defining Application Specification state entries""" + + +class CallConfig(IntFlag): + """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type""" + + NEVER = 0 + """Never handle the specified on completion type""" + CALL = 1 + """Only handle the specified on completion type for application calls""" + CREATE = 2 + """Only handle the specified on completion type for application create calls""" + ALL = 3 + """Handle the specified on completion type for both create and normal application calls""" + + +class StructArgDict(TypedDict): + name: str + elements: list[list[str]] + + +OnCompleteActionName: TypeAlias = Literal[ + "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application" +] +"""String literals representing on completion transaction types""" +MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig] +"""Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type""" +DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"] +"""Literal values describing the types of default argument sources""" + + +class DefaultArgumentDict(TypedDict): + """ + DefaultArgument is a container for any arguments that may + be resolved prior to calling some target method + """ + + source: DefaultArgumentType + data: int | str | bytes | MethodDict + + +StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword + "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict} +) + + +@dataclasses.dataclass(kw_only=True) +class MethodHints: + """MethodHints provides hints to the caller about how to call the method""" + + #: hint to indicate this method can be called through Dryrun + read_only: bool = False + #: hint to provide names for tuple argument indices + #: method_name=>param_name=>{name:str, elements:[str,str]} + structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict) + #: defaults + default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict) + call_config: MethodConfigDict = dataclasses.field(default_factory=dict) + + def empty(self) -> bool: + return not self.dictify() + + def dictify(self) -> dict[str, Any]: + d: dict[str, Any] = {} + if self.read_only: + d["read_only"] = True + if self.default_arguments: + d["default_arguments"] = self.default_arguments + if self.structs: + d["structs"] = self.structs + if any(v for v in self.call_config.values() if v != CallConfig.NEVER): + d["call_config"] = _encode_method_config(self.call_config) + return d + + @staticmethod + def undictify(data: dict[str, Any]) -> "MethodHints": + return MethodHints( + read_only=data.get("read_only", False), + default_arguments=data.get("default_arguments", {}), + structs=data.get("structs", {}), + call_config=_decode_method_config(data.get("call_config", {})), + ) + + +def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: + return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} + + +def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: + return {k: CallConfig[v] for k, v in data.items()} + + +def _encode_source(teal_text: str) -> str: + return base64.b64encode(teal_text.encode()).decode("utf-8") + + +def _decode_source(b64_text: str) -> str: + return base64.b64decode(b64_text).decode("utf-8") + + +def _encode_state_schema(schema: StateSchema) -> dict[str, int]: + return { + "num_byte_slices": schema.num_byte_slices, + "num_uints": schema.num_uints, + } # type: ignore[unused-ignore] + + +def _decode_state_schema(data: dict[str, int]) -> StateSchema: + return StateSchema( + num_byte_slices=data.get("num_byte_slices", 0), + num_uints=data.get("num_uints", 0), + ) + + +@dataclasses.dataclass(kw_only=True) +class Arc32Contract: + """ARC-0032 application specification + + See """ + + approval_program: str + clear_program: str + contract: Contract + hints: dict[str, MethodHints] + schema: StateDict + global_state_schema: StateSchema + local_state_schema: StateSchema + bare_call_config: MethodConfigDict + + def dictify(self) -> dict: + return { + "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()}, + "source": { + "approval": _encode_source(self.approval_program), + "clear": _encode_source(self.clear_program), + }, + "state": { + "global": _encode_state_schema(self.global_state_schema), + "local": _encode_state_schema(self.local_state_schema), + }, + "schema": self.schema, + "contract": self.contract.dictify(), + "bare_call_config": _encode_method_config(self.bare_call_config), + } + + def to_json(self, indent: int | None = None) -> str: + return json.dumps(self.dictify(), indent=indent) + + @staticmethod + def from_json(application_spec: str) -> "Arc32Contract": + json_spec = json.loads(application_spec) + return Arc32Contract( + approval_program=_decode_source(json_spec["source"]["approval"]), + clear_program=_decode_source(json_spec["source"]["clear"]), + schema=json_spec["schema"], + global_state_schema=_decode_state_schema(json_spec["state"]["global"]), + local_state_schema=_decode_state_schema(json_spec["state"]["local"]), + contract=Contract.undictify(json_spec["contract"]), + hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()}, + bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})), + ) + + def export(self, directory: Path | str | None = None) -> None: + """Write out the artifacts generated by the application to disk. + + Writes the approval program, clear program, contract specification and application specification + to files in the specified directory. + + :param directory: Path to the directory where the artifacts should be written. If not specified, + uses the current working directory + """ + if directory is None: + output_dir = Path.cwd() + else: + output_dir = Path(directory) + output_dir.mkdir(exist_ok=True, parents=True) + + (output_dir / "approval.teal").write_text(self.approval_program) + (output_dir / "clear.teal").write_text(self.clear_program) + (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4)) + (output_dir / "application.json").write_text(self.to_json()) diff --git a/src/algokit_utils/applications/app_spec/arc56.py b/src/algokit_utils/applications/app_spec/arc56.py new file mode 100644 index 00000000..b743b27d --- /dev/null +++ b/src/algokit_utils/applications/app_spec/arc56.py @@ -0,0 +1,1023 @@ +from __future__ import annotations + +import base64 +import json +from base64 import b64encode +from collections.abc import Callable, Sequence +from dataclasses import asdict, dataclass +from enum import Enum +from typing import Any, Literal, overload + +import algosdk +from algosdk.abi import Method as AlgosdkMethod + +from algokit_utils.applications.app_spec.arc32 import Arc32Contract + +__all__ = [ + "Actions", + "Arc56Contract", + "BareActions", + "Boxes", + "ByteCode", + "CallEnum", + "Compiler", + "CompilerInfo", + "CompilerVersion", + "CreateEnum", + "DefaultValue", + "Event", + "EventArg", + "Global", + "Keys", + "Local", + "Maps", + "Method", + "MethodArg", + "Network", + "PcOffsetMethod", + "ProgramSourceInfo", + "Recommendations", + "Returns", + "Schema", + "ScratchVariables", + "Source", + "SourceInfo", + "SourceInfoModel", + "State", + "StorageKey", + "StorageMap", + "StructField", + "TemplateVariables", +] + + +class _ActionType(str, Enum): + CALL = "CALL" + CREATE = "CREATE" + + +@dataclass +class StructField: + """Represents a field in a struct type. + + :ivar name: Name of the struct field + :ivar type: Type of the struct field, either a string or list of StructFields + """ + + name: str + type: list[StructField] | str + + @staticmethod + def from_dict(data: dict[str, Any]) -> StructField: + if isinstance(data["type"], list): + data["type"] = [StructField.from_dict(item) for item in data["type"]] + return StructField(**data) + + +class CallEnum(str, Enum): + """Enum representing different call types for application transactions.""" + + CLEAR_STATE = "ClearState" + CLOSE_OUT = "CloseOut" + DELETE_APPLICATION = "DeleteApplication" + NO_OP = "NoOp" + OPT_IN = "OptIn" + UPDATE_APPLICATION = "UpdateApplication" + + +class CreateEnum(str, Enum): + """Enum representing different create types for application transactions.""" + + DELETE_APPLICATION = "DeleteApplication" + NO_OP = "NoOp" + OPT_IN = "OptIn" + + +@dataclass +class BareActions: + """Represents bare call and create actions for an application. + + :ivar call: List of allowed call actions + :ivar create: List of allowed create actions + """ + + call: list[CallEnum] + create: list[CreateEnum] + + @staticmethod + def from_dict(data: dict[str, Any]) -> BareActions: + return BareActions(**data) + + +@dataclass +class ByteCode: + """Represents the approval and clear program bytecode. + + :ivar approval: Base64 encoded approval program bytecode + :ivar clear: Base64 encoded clear program bytecode + """ + + approval: str + clear: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> ByteCode: + return ByteCode(**data) + + +class Compiler(str, Enum): + """Enum representing different compiler types.""" + + ALGOD = "algod" + PUYA = "puya" + + +@dataclass +class CompilerVersion: + """Represents compiler version information. + + :ivar commit_hash: Git commit hash of the compiler + :ivar major: Major version number + :ivar minor: Minor version number + :ivar patch: Patch version number + """ + + commit_hash: str | None = None + major: int | None = None + minor: int | None = None + patch: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> CompilerVersion: + return CompilerVersion(**data) + + +@dataclass +class CompilerInfo: + """Information about the compiler used. + + :ivar compiler: Type of compiler used + :ivar compiler_version: Version information for the compiler + """ + + compiler: Compiler + compiler_version: CompilerVersion + + @staticmethod + def from_dict(data: dict[str, Any]) -> CompilerInfo: + data["compiler_version"] = CompilerVersion.from_dict(data["compiler_version"]) + return CompilerInfo(**data) + + +@dataclass +class Network: + """Network-specific application information. + + :ivar app_id: Application ID on the network + """ + + app_id: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Network: + return Network(**data) + + +@dataclass +class ScratchVariables: + """Information about scratch space variables. + + :ivar slot: Scratch slot number + :ivar type: Type of the scratch variable + """ + + slot: int + type: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> ScratchVariables: + return ScratchVariables(**data) + + +@dataclass +class Source: + """Source code for approval and clear programs. + + :ivar approval: Base64 encoded approval program source + :ivar clear: Base64 encoded clear program source + """ + + approval: str + clear: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> Source: + return Source(**data) + + def get_decoded_approval(self) -> str: + """Get decoded approval program source. + + :return: Decoded approval program source code + """ + return self._decode_source(self.approval) + + def get_decoded_clear(self) -> str: + """Get decoded clear program source. + + :return: Decoded clear program source code + """ + return self._decode_source(self.clear) + + def _decode_source(self, b64_text: str) -> str: + return base64.b64decode(b64_text).decode("utf-8") + + +@dataclass +class Global: + """Global state schema. + + :ivar bytes: Number of byte slices in global state + :ivar ints: Number of integers in global state + """ + + bytes: int + ints: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Global: + return Global(**data) + + +@dataclass +class Local: + """Local state schema. + + :ivar bytes: Number of byte slices in local state + :ivar ints: Number of integers in local state + """ + + bytes: int + ints: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> Local: + return Local(**data) + + +@dataclass +class Schema: + """Application state schema. + + :ivar global_state: Global state schema + :ivar local_state: Local state schema + """ + + global_state: Global # actual schema field is "global" since it's a reserved word + local_state: Local # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Schema: + global_state = Global.from_dict(data["global"]) + local_state = Local.from_dict(data["local"]) + return Schema(global_state=global_state, local_state=local_state) + + +@dataclass +class TemplateVariables: + """Template variable information. + + :ivar type: Type of the template variable + :ivar value: Optional value of the template variable + """ + + type: str + value: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> TemplateVariables: + return TemplateVariables(**data) + + +@dataclass +class EventArg: + """Event argument information. + + :ivar type: Type of the event argument + :ivar desc: Optional description of the argument + :ivar name: Optional name of the argument + :ivar struct: Optional struct type name + """ + + type: str + desc: str | None = None + name: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> EventArg: + return EventArg(**data) + + +@dataclass +class Event: + """Event information. + + :ivar args: List of event arguments + :ivar name: Name of the event + :ivar desc: Optional description of the event + """ + + args: list[EventArg] + name: str + desc: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Event: + data["args"] = [EventArg.from_dict(item) for item in data["args"]] + return Event(**data) + + +@dataclass +class Actions: + """Method actions information. + + :ivar call: Optional list of allowed call actions + :ivar create: Optional list of allowed create actions + """ + + call: list[CallEnum] | None = None + create: list[CreateEnum] | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Actions: + return Actions(**data) + + +@dataclass +class DefaultValue: + """Default value information for method arguments. + + :ivar data: Default value data + :ivar source: Source of the default value + :ivar type: Optional type of the default value + """ + + data: str + source: Literal["box", "global", "local", "literal", "method"] + type: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> DefaultValue: + return DefaultValue(**data) + + +@dataclass +class MethodArg: + """Method argument information. + + :ivar type: Type of the argument + :ivar default_value: Optional default value + :ivar desc: Optional description + :ivar name: Optional name + :ivar struct: Optional struct type name + """ + + type: str + default_value: DefaultValue | None = None + desc: str | None = None + name: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> MethodArg: + if data.get("default_value"): + data["default_value"] = DefaultValue.from_dict(data["default_value"]) + return MethodArg(**data) + + +@dataclass +class Boxes: + """Box storage requirements. + + :ivar key: Box key + :ivar read_bytes: Number of bytes to read + :ivar write_bytes: Number of bytes to write + :ivar app: Optional application ID + """ + + key: str + read_bytes: int + write_bytes: int + app: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Boxes: + return Boxes(**data) + + +@dataclass +class Recommendations: + """Method execution recommendations. + + :ivar accounts: Optional list of accounts + :ivar apps: Optional list of applications + :ivar assets: Optional list of assets + :ivar boxes: Optional box storage requirements + :ivar inner_transaction_count: Optional inner transaction count + """ + + accounts: list[str] | None = None + apps: list[int] | None = None + assets: list[int] | None = None + boxes: Boxes | None = None + inner_transaction_count: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Recommendations: + if data.get("boxes"): + data["boxes"] = Boxes.from_dict(data["boxes"]) + return Recommendations(**data) + + +@dataclass +class Returns: + """Method return information. + + :ivar type: Return type + :ivar desc: Optional description + :ivar struct: Optional struct type name + """ + + type: str + desc: str | None = None + struct: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Returns: + return Returns(**data) + + +@dataclass +class Method: + """Method information. + + :ivar actions: Allowed actions + :ivar args: Method arguments + :ivar name: Method name + :ivar returns: Return information + :ivar desc: Optional description + :ivar events: Optional list of events + :ivar readonly: Optional readonly flag + :ivar recommendations: Optional execution recommendations + """ + + actions: Actions + args: list[MethodArg] + name: str + returns: Returns + desc: str | None = None + events: list[Event] | None = None + readonly: bool | None = None + recommendations: Recommendations | None = None + + _abi_method: AlgosdkMethod | None = None + + def __post_init__(self) -> None: + self._abi_method = AlgosdkMethod.undictify(asdict(self)) + + def to_abi_method(self) -> AlgosdkMethod: + """Convert to ABI method. + + :raises ValueError: If underlying ABI method is not initialized + :return: ABI method + """ + if self._abi_method is None: + raise ValueError("Underlying core ABI method class is not initialized!") + return self._abi_method + + @staticmethod + def from_dict(data: dict[str, Any]) -> Method: + data["actions"] = Actions.from_dict(data["actions"]) + data["args"] = [MethodArg.from_dict(item) for item in data["args"]] + data["returns"] = Returns.from_dict(data["returns"]) + if data.get("events"): + data["events"] = [Event.from_dict(item) for item in data["events"]] + if data.get("recommendations"): + data["recommendations"] = Recommendations.from_dict(data["recommendations"]) + return Method(**data) + + +class PcOffsetMethod(str, Enum): + """PC offset method types.""" + + CBLOCKS = "cblocks" + NONE = "none" + + +@dataclass +class SourceInfo: + """Source code location information. + + :ivar pc: List of program counter values + :ivar error_message: Optional error message + :ivar source: Optional source code + :ivar teal: Optional TEAL version + """ + + pc: list[int] + error_message: str | None = None + source: str | None = None + teal: int | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SourceInfo: + return SourceInfo(**data) + + +@dataclass +class StorageKey: + """Storage key information. + + :ivar key: Storage key + :ivar key_type: Type of the key + :ivar value_type: Type of the value + :ivar desc: Optional description + """ + + key: str + key_type: str + value_type: str + desc: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> StorageKey: + return StorageKey(**data) + + +@dataclass +class StorageMap: + """Storage map information. + + :ivar key_type: Type of map keys + :ivar value_type: Type of map values + :ivar desc: Optional description + :ivar prefix: Optional key prefix + """ + + key_type: str + value_type: str + desc: str | None = None + prefix: str | None = None + + @staticmethod + def from_dict(data: dict[str, Any]) -> StorageMap: + return StorageMap(**data) + + +@dataclass +class Keys: + """Storage keys for different storage types. + + :ivar box: Box storage keys + :ivar global_state: Global state storage keys + :ivar local_state: Local state storage keys + """ + + box: dict[str, StorageKey] + global_state: dict[str, StorageKey] # actual schema field is "global" since it's a reserved word + local_state: dict[str, StorageKey] # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Keys: + box = {key: StorageKey.from_dict(value) for key, value in data["box"].items()} + global_state = {key: StorageKey.from_dict(value) for key, value in data["global"].items()} + local_state = {key: StorageKey.from_dict(value) for key, value in data["local"].items()} + return Keys(box=box, global_state=global_state, local_state=local_state) + + +@dataclass +class Maps: + """Storage maps for different storage types. + + :ivar box: Box storage maps + :ivar global_state: Global state storage maps + :ivar local_state: Local state storage maps + """ + + box: dict[str, StorageMap] + global_state: dict[str, StorageMap] # actual schema field is "global" since it's a reserved word + local_state: dict[str, StorageMap] # actual schema field is "local" for consistency with renamed "global" + + @staticmethod + def from_dict(data: dict[str, Any]) -> Maps: + box = {key: StorageMap.from_dict(value) for key, value in data["box"].items()} + global_state = {key: StorageMap.from_dict(value) for key, value in data["global"].items()} + local_state = {key: StorageMap.from_dict(value) for key, value in data["local"].items()} + return Maps(box=box, global_state=global_state, local_state=local_state) + + +@dataclass +class State: + """Application state information. + + :ivar keys: Storage keys + :ivar maps: Storage maps + :ivar schema: State schema + """ + + keys: Keys + maps: Maps + schema: Schema + + @staticmethod + def from_dict(data: dict[str, Any]) -> State: + data["keys"] = Keys.from_dict(data["keys"]) + data["maps"] = Maps.from_dict(data["maps"]) + data["schema"] = Schema.from_dict(data["schema"]) + return State(**data) + + +@dataclass +class ProgramSourceInfo: + """Program source information. + + :ivar pc_offset_method: PC offset method + :ivar source_info: List of source info entries + """ + + pc_offset_method: PcOffsetMethod + source_info: list[SourceInfo] + + @staticmethod + def from_dict(data: dict[str, Any]) -> ProgramSourceInfo: + data["source_info"] = [SourceInfo.from_dict(item) for item in data["source_info"]] + return ProgramSourceInfo(**data) + + +@dataclass +class SourceInfoModel: + """Source information for approval and clear programs. + + :ivar approval: Approval program source info + :ivar clear: Clear program source info + """ + + approval: ProgramSourceInfo + clear: ProgramSourceInfo + + @staticmethod + def from_dict(data: dict[str, Any]) -> SourceInfoModel: + data["approval"] = ProgramSourceInfo.from_dict(data["approval"]) + data["clear"] = ProgramSourceInfo.from_dict(data["clear"]) + return SourceInfoModel(**data) + + +def _dict_keys_to_snake_case( + value: Any, # noqa: ANN401 +) -> Any: # noqa: ANN401 + def camel_to_snake(s: str) -> str: + return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_") + + match value: + case dict(): + new_dict: dict[str, Any] = {} + for key, val in value.items(): + new_dict[camel_to_snake(str(key))] = _dict_keys_to_snake_case(val) + return new_dict + case list(): + return [_dict_keys_to_snake_case(item) for item in value] + case _: + return value + + +class _Arc32ToArc56Converter: + def __init__(self, arc32_application_spec: str): + self.arc32 = json.loads(arc32_application_spec) + + def convert(self) -> Arc56Contract: + source_data = self.arc32.get("source") + return Arc56Contract( + name=self.arc32["contract"]["name"], + desc=self.arc32["contract"].get("desc"), + arcs=[], + methods=self._convert_methods(self.arc32), + structs=self._convert_structs(self.arc32), + state=self._convert_state(self.arc32), + source=Source(**source_data) if source_data else None, + bare_actions=BareActions( + call=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CALL), + create=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CREATE), + ), + ) + + def _convert_storage_keys(self, schema: dict) -> dict[str, StorageKey]: + """Convert ARC32 schema declared fields to ARC56 storage keys.""" + return { + name: StorageKey( + key=b64encode(field["key"].encode()).decode(), + key_type="AVMString", + value_type="AVMUint64" if field["type"] == "uint64" else "AVMBytes", + desc=field.get("descr"), + ) + for name, field in schema.items() + } + + def _convert_state(self, arc32: dict) -> State: + """Convert ARC32 state and schema to ARC56 state specification.""" + state_data = arc32.get("state", {}) + return State( + schema=Schema( + global_state=Global( + ints=state_data.get("global", {}).get("num_uints", 0), + bytes=state_data.get("global", {}).get("num_byte_slices", 0), + ), + local_state=Local( + ints=state_data.get("local", {}).get("num_uints", 0), + bytes=state_data.get("local", {}).get("num_byte_slices", 0), + ), + ), + keys=Keys( + global_state=self._convert_storage_keys(arc32.get("schema", {}).get("global", {}).get("declared", {})), + local_state=self._convert_storage_keys(arc32.get("schema", {}).get("local", {}).get("declared", {})), + box={}, + ), + maps=Maps(global_state={}, local_state={}, box={}), + ) + + def _convert_structs(self, arc32: dict) -> dict[str, list[StructField]]: + """Extract and convert struct definitions from hints.""" + return { + struct["name"]: [StructField(name=elem[0], type=elem[1]) for elem in struct["elements"]] + for hint in arc32.get("hints", {}).values() + for struct in hint.get("structs", {}).values() + } + + def _convert_default_value(self, arg_type: str, default_arg: dict[str, Any] | None) -> DefaultValue | None: + """Convert ARC32 default argument to ARC56 format.""" + if not default_arg or not default_arg.get("source"): + return None + + source_mapping = { + "constant": "literal", + "global-state": "global", + "local-state": "local", + "abi-method": "method", + } + + mapped_source = source_mapping.get(default_arg["source"]) + if not mapped_source: + return None + elif mapped_source == "method": + return DefaultValue( + source=mapped_source, # type: ignore[arg-type] + data=default_arg.get("data", {}).get("name"), + ) + + arg_data = default_arg.get("data") + + if isinstance(arg_data, int): + arg_data = algosdk.abi.ABIType.from_string("uint64").encode(arg_data) + elif isinstance(arg_data, str): + arg_data = arg_data.encode() + else: + raise ValueError(f"Invalid default argument data type: {type(arg_data)}") + + return DefaultValue( + source=mapped_source, # type: ignore[arg-type] + data=base64.b64encode(arg_data).decode("utf-8"), + type=arg_type if arg_type != "string" else "AVMString", + ) + + @overload + def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CALL]) -> list[CallEnum]: ... + + @overload + def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CREATE]) -> list[CreateEnum]: ... + + def _convert_actions(self, config: dict | None, action_type: _ActionType) -> Sequence[CallEnum | CreateEnum]: + """Extract supported actions from call config.""" + if not config: + return [] + + actions: list[CallEnum | CreateEnum] = [] + mappings = { + "no_op": (CallEnum.NO_OP, CreateEnum.NO_OP), + "opt_in": (CallEnum.OPT_IN, CreateEnum.OPT_IN), + "close_out": (CallEnum.CLOSE_OUT, None), + "delete_application": (CallEnum.DELETE_APPLICATION, CreateEnum.DELETE_APPLICATION), + "update_application": (CallEnum.UPDATE_APPLICATION, None), + } + + for action, (call_enum, create_enum) in mappings.items(): + if action in config and config[action] in ["ALL", action_type]: + if action_type == "CALL" and call_enum: + actions.append(call_enum) + elif action_type == "CREATE" and create_enum: + actions.append(create_enum) + + return actions + + def _convert_method_actions(self, hint: dict | None) -> Actions: + """Convert method call config to ARC56 actions.""" + config = hint.get("call_config", {}) if hint else {} + return Actions( + call=self._convert_actions(config, _ActionType.CALL), + create=self._convert_actions(config, _ActionType.CREATE), + ) + + def _convert_methods(self, arc32: dict) -> list[Method]: + """Convert ARC32 methods to ARC56 format.""" + methods = [] + contract = arc32["contract"] + hints = arc32.get("hints", {}) + + for method in contract["methods"]: + args_sig = ",".join(a["type"] for a in method["args"]) + signature = f"{method['name']}({args_sig}){method['returns']['type']}" + hint = hints.get(signature, {}) + + methods.append( + Method( + name=method["name"], + desc=method.get("desc"), + readonly=hint.get("read_only"), + args=[ + MethodArg( + name=arg.get("name"), + type=arg["type"], + desc=arg.get("desc"), + struct=hint.get("structs", {}).get(arg.get("name", ""), {}).get("name"), + default_value=self._convert_default_value( + arg["type"], hint.get("default_arguments", {}).get(arg.get("name")) + ), + ) + for arg in method["args"] + ], + returns=Returns( + type=method["returns"]["type"], + desc=method["returns"].get("desc"), + struct=hint.get("structs", {}).get("output", {}).get("name"), + ), + actions=self._convert_method_actions(hint), + events=[], # ARC32 doesn't specify events + ) + ) + return methods + + +def _arc56_dict_factory() -> Callable[[list[tuple[str, Any]]], dict[str, Any]]: + """Creates a dict factory that handles ARC-56 JSON field naming conventions.""" + + word_map = {"global_state": "global", "local_state": "local"} + blocklist = ["_abi_method"] + + def to_camel(key: str) -> str: + key = word_map.get(key, key) + words = key.split("_") + return words[0] + "".join(word.capitalize() for word in words[1:]) + + def dict_factory(entries: list[tuple[str, Any]]) -> dict[str, Any]: + return {to_camel(k): v for k, v in entries if v is not None and k not in blocklist} + + return dict_factory + + +@dataclass +class Arc56Contract: + """ARC-0056 application specification. + + See https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md + + :ivar arcs: List of supported ARC version numbers + :ivar bare_actions: Bare call and create actions + :ivar methods: List of contract methods + :ivar name: Contract name + :ivar state: Contract state information + :ivar structs: Contract struct definitions + :ivar byte_code: Optional bytecode for approval and clear programs + :ivar compiler_info: Optional compiler information + :ivar desc: Optional contract description + :ivar events: Optional list of contract events + :ivar networks: Optional network deployment information + :ivar scratch_variables: Optional scratch variable information + :ivar source: Optional source code + :ivar source_info: Optional source code information + :ivar template_variables: Optional template variable information + """ + + arcs: list[int] + bare_actions: BareActions + methods: list[Method] + name: str + state: State + structs: dict[str, list[StructField]] + byte_code: ByteCode | None = None + compiler_info: CompilerInfo | None = None + desc: str | None = None + events: list[Event] | None = None + networks: dict[str, Network] | None = None + scratch_variables: dict[str, ScratchVariables] | None = None + source: Source | None = None + source_info: SourceInfoModel | None = None + template_variables: dict[str, TemplateVariables] | None = None + + @staticmethod + def from_dict(application_spec: dict) -> Arc56Contract: + """Create Arc56Contract from dictionary. + + :param application_spec: Dictionary containing contract specification + :return: Arc56Contract instance + """ + data = _dict_keys_to_snake_case(application_spec) + data["bare_actions"] = BareActions.from_dict(data["bare_actions"]) + data["methods"] = [Method.from_dict(item) for item in data["methods"]] + data["state"] = State.from_dict(data["state"]) + data["structs"] = { + key: [StructField.from_dict(item) for item in value] for key, value in application_spec["structs"].items() + } + if data.get("byte_code"): + data["byte_code"] = ByteCode.from_dict(data["byte_code"]) + if data.get("compiler_info"): + data["compiler_info"] = CompilerInfo.from_dict(data["compiler_info"]) + if data.get("events"): + data["events"] = [Event.from_dict(item) for item in data["events"]] + if data.get("networks"): + data["networks"] = {key: Network.from_dict(value) for key, value in data["networks"].items()} + if data.get("scratch_variables"): + data["scratch_variables"] = { + key: ScratchVariables.from_dict(value) for key, value in data["scratch_variables"].items() + } + if data.get("source"): + data["source"] = Source.from_dict(data["source"]) + if data.get("source_info"): + data["source_info"] = SourceInfoModel.from_dict(data["source_info"]) + if data.get("template_variables"): + data["template_variables"] = { + key: TemplateVariables.from_dict(value) for key, value in data["template_variables"].items() + } + return Arc56Contract(**data) + + @staticmethod + def from_json(application_spec: str) -> Arc56Contract: + return Arc56Contract.from_dict(json.loads(application_spec)) + + @staticmethod + def from_arc32(arc32_application_spec: str | Arc32Contract) -> Arc56Contract: + return _Arc32ToArc56Converter( + arc32_application_spec.to_json() + if isinstance(arc32_application_spec, Arc32Contract) + else arc32_application_spec + ).convert() + + @staticmethod + def get_abi_struct_from_abi_tuple( + decoded_tuple: Any, # noqa: ANN401 + struct_fields: list[StructField], + structs: dict[str, list[StructField]], + ) -> dict[str, Any]: + result = {} + for i, field in enumerate(struct_fields): + key = field.name + field_type = field.type + value = decoded_tuple[i] + if isinstance(field_type, str): + if field_type in structs: + value = Arc56Contract.get_abi_struct_from_abi_tuple(value, structs[field_type], structs) + elif isinstance(field_type, list): + value = Arc56Contract.get_abi_struct_from_abi_tuple(value, field_type, structs) + result[key] = value + return result + + def to_json(self, indent: int | None = None) -> str: + return json.dumps(self.dictify(), indent=indent) + + def dictify(self) -> dict: + return asdict(self, dict_factory=_arc56_dict_factory()) + + def get_arc56_method(self, method_name_or_signature: str) -> Method: + if "(" not in method_name_or_signature: + # Filter by method name + methods = [m for m in self.methods if m.name == method_name_or_signature] + if not methods: + raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.") + if len(methods) > 1: + signatures = [AlgosdkMethod.undictify(m.__dict__).get_signature() for m in self.methods] + raise ValueError( + f"Received a call to method {method_name_or_signature} in contract {self.name}, " + f"but this resolved to multiple methods; please pass in an ABI signature instead: " + f"{', '.join(signatures)}" + ) + method = methods[0] + else: + # Find by signature + method = None + for m in self.methods: + abi_method = AlgosdkMethod.undictify(asdict(m)) + if abi_method.get_signature() == method_name_or_signature: + method = m + break + + if method is None: + raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.") + + return method diff --git a/src/algokit_utils/applications/enums.py b/src/algokit_utils/applications/enums.py new file mode 100644 index 00000000..20d7e786 --- /dev/null +++ b/src/algokit_utils/applications/enums.py @@ -0,0 +1,40 @@ +from enum import Enum + +# NOTE: this is moved to a separate file to avoid circular imports + + +class OnSchemaBreak(Enum): + """Action to take if an Application's schema has breaking changes""" + + Fail = 0 + """Fail the deployment""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new Application""" + + +class OnUpdate(Enum): + """Action to take if an Application has been updated""" + + Fail = 0 + """Fail the deployment""" + UpdateApp = 1 + """Update the Application with the new approval and clear programs""" + ReplaceApp = 2 + """Create a new Application and delete the old Application in a single transaction""" + AppendApp = 3 + """Create a new application""" + + +class OperationPerformed(Enum): + """Describes the actions taken during deployment""" + + Nothing = 0 + """An existing Application was found""" + Create = 1 + """No existing Application was found, created a new Application""" + Update = 2 + """An existing Application was found, but was out of date, updated to latest version""" + Replace = 3 + """An existing Application was found, but was out of date, created a new Application and deleted the original""" diff --git a/src/algokit_utils/asset.py b/src/algokit_utils/asset.py index 085ea8c5..c7087f0c 100644 --- a/src/algokit_utils/asset.py +++ b/src/algokit_utils/asset.py @@ -1,168 +1,32 @@ -import logging -from typing import TYPE_CHECKING - -from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner -from algosdk.constants import TX_GROUP_LIMIT -from algosdk.transaction import AssetTransferTxn - -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - -from enum import Enum, auto - -from algokit_utils.models import Account - -__all__ = ["opt_in", "opt_out"] -logger = logging.getLogger(__name__) - - -class ValidationType(Enum): - OPTIN = auto() - OPTOUT = auto() - - -def _ensure_account_is_valid(algod_client: "AlgodClient", account: Account) -> None: - try: - algod_client.account_info(account.address) - except Exception as err: - error_message = f"Account address{account.address} does not exist" - logger.debug(error_message) - raise err - - -def _ensure_asset_balance_conditions( - algod_client: "AlgodClient", account: Account, asset_ids: list, validation_type: ValidationType -) -> None: - invalid_asset_ids = [] - account_info = algod_client.account_info(account.address) - account_assets = account_info.get("assets", []) # type: ignore # noqa: PGH003 - for asset_id in asset_ids: - asset_exists_in_account_info = any(asset["asset-id"] == asset_id for asset in account_assets) - if validation_type == ValidationType.OPTIN: - if asset_exists_in_account_info: - logger.debug(f"Asset {asset_id} is already opted in for account {account.address}") - invalid_asset_ids.append(asset_id) - - elif validation_type == ValidationType.OPTOUT: - if not account_assets or not asset_exists_in_account_info: - logger.debug(f"Account {account.address} does not have asset {asset_id}") - invalid_asset_ids.append(asset_id) - else: - asset_balance = next((asset["amount"] for asset in account_assets if asset["asset-id"] == asset_id), 0) - if asset_balance != 0: - logger.debug(f"Asset {asset_id} balance is not zero") - invalid_asset_ids.append(asset_id) - - if len(invalid_asset_ids) > 0: - action = "opted out" if validation_type == ValidationType.OPTOUT else "opted in" - condition_message = ( - "their amount is zero and that the account has" - if validation_type == ValidationType.OPTOUT - else "they are valid and that the account has not" - ) - - error_message = ( - f"Assets {invalid_asset_ids} cannot be {action}. Ensure that " - f"{condition_message} previously opted into them." - ) - raise ValueError(error_message) - - -def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: - """ - Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, - it must `opt-in` to receive it. An opt-in transaction places an asset holding of 0 into the account and increases - its minimum balance by [100,000 microAlgos](https://developer.algorand.org/docs/get-details/asa/#assets-overview). - - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the algosdk library. - account (Account): An instance of the Account class representing the account that wants to opt-in to the assets. - asset_ids (list[int]): A list of integers representing the asset IDs to opt-in to. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values - are the transaction IDs for opting-in to each asset. - """ - _ensure_account_is_valid(algod_client, account) - _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTIN) - suggested_params = algod_client.suggested_params() - result = {} - for i in range(0, len(asset_ids), TX_GROUP_LIMIT): - atc = AtomicTransactionComposer() - chunk = asset_ids[i : i + TX_GROUP_LIMIT] - for asset_id in chunk: - asset = algod_client.asset_info(asset_id) - xfer_txn = AssetTransferTxn( - sp=suggested_params, - sender=account.address, - receiver=account.address, - close_assets_to=None, - revocation_target=None, - amt=0, - note=f"opt in asset id ${asset_id}", - index=asset["index"], # type: ignore # noqa: PGH003 - rekey_to=None, - ) - - transaction_with_signer = TransactionWithSigner( - txn=xfer_txn, - signer=account.signer, - ) - atc.add_transaction(transaction_with_signer) - atc.execute(algod_client, 4) - - for index, asset_id in enumerate(chunk): - result[asset_id] = atc.tx_ids[index] - - return result - - -def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: - """ - Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. - The account also recovers the Minimum Balance Requirement for the asset (100,000 microAlgos) - The `optOut` function manages the opt-out process, permitting the account to discontinue holding a group of assets. - - It's essential to note that an account can only opt_out of an asset if its balance of that asset is zero. - - Args: - algod_client (AlgodClient): An instance of the AlgodClient class from the `algosdk` library. - account (Account): An instance of the Account class that holds the private key and address for an account. - asset_ids (list[int]): A list of integers representing the asset IDs of the ASAs to opt out from. - Returns: - dict[int, str]: A dictionary where the keys are the asset IDs and the values are the transaction IDs of - the executed transactions. - - """ - _ensure_account_is_valid(algod_client, account) - _ensure_asset_balance_conditions(algod_client, account, asset_ids, ValidationType.OPTOUT) - suggested_params = algod_client.suggested_params() - result = {} - for i in range(0, len(asset_ids), TX_GROUP_LIMIT): - atc = AtomicTransactionComposer() - chunk = asset_ids[i : i + TX_GROUP_LIMIT] - for asset_id in chunk: - asset = algod_client.asset_info(asset_id) - asset_creator = asset["params"]["creator"] # type: ignore # noqa: PGH003 - xfer_txn = AssetTransferTxn( - sp=suggested_params, - sender=account.address, - receiver=account.address, - close_assets_to=asset_creator, - revocation_target=None, - amt=0, - note=f"opt out asset id ${asset_id}", - index=asset["index"], # type: ignore # noqa: PGH003 - rekey_to=None, - ) - - transaction_with_signer = TransactionWithSigner( - txn=xfer_txn, - signer=account.signer, - ) - atc.add_transaction(transaction_with_signer) - atc.execute(algod_client, 4) - - for index, asset_id in enumerate(chunk): - result[asset_id] = atc.tx_ids[index] - - return result +import warnings + +warnings.warn( + """The legacy v2 asset module is deprecated and will be removed in a future version. + +Replacements for opt_in/opt_out functionality: + +1. Using TransactionComposer: + composer.add_asset_opt_in(AssetOptInParams( + sender=account.address, + asset_id=123 + )) + composer.add_asset_opt_out(AssetOptOutParams( + sender=account.address, + asset_id=123, + creator=creator_address + )) + +2. Using AlgorandClient: + client.asset.opt_in(AssetOptInParams(...)) + client.asset.opt_out(AssetOptOutParams(...)) + +3. For bulk operations: + client.asset.bulk_opt_in(account, [asset_ids]) + client.asset.bulk_opt_out(account, [asset_ids]) + +Refer to AssetManager class from algokit_utils for more functionality.""", + DeprecationWarning, + stacklevel=2, +) + +from algokit_utils._legacy_v2.asset import * # noqa: F403, E402 diff --git a/src/algokit_utils/assets/__init__.py b/src/algokit_utils/assets/__init__.py new file mode 100644 index 00000000..ec7116dd --- /dev/null +++ b/src/algokit_utils/assets/__init__.py @@ -0,0 +1 @@ +from algokit_utils.assets.asset_manager import * # noqa: F403 diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py new file mode 100644 index 00000000..571b748b --- /dev/null +++ b/src/algokit_utils/assets/asset_manager.py @@ -0,0 +1,320 @@ +from collections.abc import Callable +from dataclasses import dataclass + +import algosdk +from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.v2client import algod + +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.transaction import SendParams +from algokit_utils.transactions.transaction_composer import ( + AssetOptInParams, + AssetOptOutParams, + TransactionComposer, +) + +__all__ = ["AccountAssetInformation", "AssetInformation", "AssetManager", "BulkAssetOptInOutResult"] + + +@dataclass(kw_only=True, frozen=True) +class AccountAssetInformation: + """Information about an account's holding of a particular asset. + + :ivar asset_id: The ID of the asset + :ivar balance: The amount of the asset held by the account + :ivar frozen: Whether the asset is frozen for this account + :ivar round: The round this information was retrieved at + """ + + asset_id: int + balance: int + frozen: bool + round: int + + +@dataclass(kw_only=True, frozen=True) +class AssetInformation: + """Information about an Algorand Standard Asset (ASA). + + :ivar asset_id: The ID of the asset + :ivar creator: The address of the account that created the asset + :ivar total: The total amount of the smallest divisible units that were created of the asset + :ivar decimals: The amount of decimal places the asset was created with + :ivar default_frozen: Whether the asset was frozen by default for all accounts, defaults to None + :ivar manager: The address of the optional account that can manage the configuration of the asset and destroy it, + defaults to None + :ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, + defaults to None + :ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, + defaults to None + :ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, + defaults to None + :ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None + :ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None + :ivar asset_name: The optional name of the asset, defaults to None + :ivar asset_name_b64: The optional name of the asset as bytes, defaults to None + :ivar url: Optional URL where more information about the asset can be retrieved, defaults to None + :ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None + :ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, + defaults to None + """ + + asset_id: int + creator: str + total: int + decimals: int + default_frozen: bool | None = None + manager: str | None = None + reserve: str | None = None + freeze: str | None = None + clawback: str | None = None + unit_name: str | None = None + unit_name_b64: bytes | None = None + asset_name: str | None = None + asset_name_b64: bytes | None = None + url: str | None = None + url_b64: bytes | None = None + metadata_hash: bytes | None = None + + +@dataclass(kw_only=True, frozen=True) +class BulkAssetOptInOutResult: + """Result from performing a bulk opt-in or bulk opt-out for an account against a series of assets. + + :ivar asset_id: The ID of the asset opted into / out of + :ivar transaction_id: The transaction ID of the resulting opt in / out + """ + + asset_id: int + transaction_id: str + + +class AssetManager: + """A manager for Algorand Standard Assets (ASAs). + + :param algod_client: An algod client + :param new_group: A function that creates a new TransactionComposer transaction group + """ + + def __init__(self, algod_client: algod.AlgodClient, new_group: Callable[[], TransactionComposer]): + self._algod = algod_client + self._new_group = new_group + + def get_by_id(self, asset_id: int) -> AssetInformation: + """Returns the current asset information for the asset with the given ID. + + :param asset_id: The ID of the asset + :return: The asset information + """ + asset = self._algod.asset_info(asset_id) + assert isinstance(asset, dict) + params = asset["params"] + + return AssetInformation( + asset_id=asset_id, + total=params["total"], + decimals=params["decimals"], + asset_name=params.get("name"), + asset_name_b64=params.get("name-b64"), + unit_name=params.get("unit-name"), + unit_name_b64=params.get("unit-name-b64"), + url=params.get("url"), + url_b64=params.get("url-b64"), + creator=params["creator"], + manager=params.get("manager"), + clawback=params.get("clawback"), + freeze=params.get("freeze"), + reserve=params.get("reserve"), + default_frozen=params.get("default-frozen"), + metadata_hash=params.get("metadata-hash"), + ) + + def get_account_information( + self, sender: str | SigningAccount | TransactionSigner, asset_id: int + ) -> AccountAssetInformation: + """Returns the given sender account's asset holding for a given asset. + + :param sender: The address of the sender/account to look up + :param asset_id: The ID of the asset to return a holding for + :return: The account asset holding information + """ + address = self._get_address_from_sender(sender) + info = self._algod.account_asset_info(address, asset_id) + assert isinstance(info, dict) + + return AccountAssetInformation( + asset_id=asset_id, + balance=info["asset-holding"]["amount"], + frozen=info["asset-holding"]["is-frozen"], + round=info["round"], + ) + + def bulk_opt_in( # noqa: PLR0913 + self, + account: str, + asset_ids: list[int], + 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, + send_params: SendParams | None = None, + ) -> list[BulkAssetOptInOutResult]: + """Opt an account in to a list of Algorand Standard Assets. + + :param account: The account to opt-in + :param asset_ids: The list of asset IDs to opt-in to + :param signer: The signer to use for the transaction, defaults to None + :param rekey_to: The address to rekey the account to, defaults to None + :param note: The note to include in the transaction, defaults to None + :param lease: The lease to include in the transaction, defaults to None + :param static_fee: The static fee to include in the transaction, defaults to None + :param extra_fee: The extra fee to include in the transaction, defaults to None + :param max_fee: The maximum fee to include in the transaction, defaults to None + :param validity_window: The validity window to include in the transaction, defaults to None + :param first_valid_round: The first valid round to include in the transaction, defaults to None + :param last_valid_round: The last valid round to include in the transaction, defaults to None + :param send_params: The send parameters to use for the transaction, defaults to None + :return: An array of records matching asset ID to transaction ID of the opt in + """ + results: list[BulkAssetOptInOutResult] = [] + sender = self._get_address_from_sender(account) + + for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT): + composer = self._new_group() + + for asset_id in asset_group: + params = AssetOptInParams( + sender=sender, + asset_id=asset_id, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + composer.add_asset_opt_in(params) + + result = composer.send(send_params) + + for i, asset_id in enumerate(asset_group): + results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) + + return results + + def bulk_opt_out( # noqa: C901, PLR0913 + self, + *, + account: str, + asset_ids: list[int], + ensure_zero_balance: bool = True, + 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, + send_params: SendParams | None = None, + ) -> list[BulkAssetOptInOutResult]: + """Opt an account out of a list of Algorand Standard Assets. + + :param account: The account to opt-out + :param asset_ids: The list of asset IDs to opt-out of + :param ensure_zero_balance: Whether to check if the account has a zero balance first, defaults to True + :param signer: The signer to use for the transaction, defaults to None + :param rekey_to: The address to rekey the account to, defaults to None + :param note: The note to include in the transaction, defaults to None + :param lease: The lease to include in the transaction, defaults to None + :param static_fee: The static fee to include in the transaction, defaults to None + :param extra_fee: The extra fee to include in the transaction, defaults to None + :param max_fee: The maximum fee to include in the transaction, defaults to None + :param validity_window: The validity window to include in the transaction, defaults to None + :param first_valid_round: The first valid round to include in the transaction, defaults to None + :param last_valid_round: The last valid round to include in the transaction, defaults to None + :param send_params: The send parameters to use for the transaction, defaults to None + :raises ValueError: If ensure_zero_balance is True and account has non-zero balance or is not opted in + :return: An array of records matching asset ID to transaction ID of the opt out + """ + results: list[BulkAssetOptInOutResult] = [] + sender = self._get_address_from_sender(account) + + for asset_group in _chunk_array(asset_ids, algosdk.constants.TX_GROUP_LIMIT): + composer = self._new_group() + + not_opted_in_asset_ids: list[int] = [] + non_zero_balance_asset_ids: list[int] = [] + + if ensure_zero_balance: + for asset_id in asset_group: + try: + account_asset_info = self.get_account_information(sender, asset_id) + if account_asset_info.balance != 0: + non_zero_balance_asset_ids.append(asset_id) + except Exception: + not_opted_in_asset_ids.append(asset_id) + + if not_opted_in_asset_ids or non_zero_balance_asset_ids: + error_message = f"Account {sender}" + if not_opted_in_asset_ids: + error_message += f" is not opted-in to Asset(s) {', '.join(map(str, not_opted_in_asset_ids))}" + if non_zero_balance_asset_ids: + error_message += ( + f" has non-zero balance for Asset(s) {', '.join(map(str, non_zero_balance_asset_ids))}" + ) + error_message += "; can't opt-out." + raise ValueError(error_message) + + for asset_id in asset_group: + asset_info = self.get_by_id(asset_id) + params = AssetOptOutParams( + sender=sender, + asset_id=asset_id, + creator=asset_info.creator, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + composer.add_asset_opt_out(params) + + result = composer.send(send_params) + + for i, asset_id in enumerate(asset_group): + results.append(BulkAssetOptInOutResult(asset_id=asset_id, transaction_id=result.tx_ids[i])) + + return results + + @staticmethod + def _get_address_from_sender(sender: str | SigningAccount | TransactionSigner) -> str: + if isinstance(sender, str): + return sender + if isinstance(sender, SigningAccount): + return sender.address + if isinstance(sender, AccountTransactionSigner): + return str(algosdk.account.address_from_private_key(sender.private_key)) + raise ValueError(f"Unsupported sender type: {type(sender)}") + + +def _chunk_array(array: list, size: int) -> list[list]: + return [array[i : i + size] for i in range(0, len(array), size)] diff --git a/src/algokit_utils/beta/_utils.py b/src/algokit_utils/beta/_utils.py new file mode 100644 index 00000000..f28f96a3 --- /dev/null +++ b/src/algokit_utils/beta/_utils.py @@ -0,0 +1,36 @@ +from typing import NoReturn + + +def deprecated_import_error(old_path: str, new_path: str) -> NoReturn: + """Helper to create consistent deprecation error messages""" + raise ImportError( + f"WARNING: The module '{old_path}' has been removed in algokit-utils v3. " + f"Please update your imports to use '{new_path}' instead. " + "See the migration guide for more details: " + "https://github.com/algorandfoundation/algokit-utils-py/blob/main/docs/source/v3-migration-guide.md" + ) + + +def handle_getattr(name: str) -> NoReturn: + param_mappings = { + "ClientManager": "algokit_utils.ClientManager", + "AlgorandClient": "algokit_utils.AlgorandClient", + "AlgoSdkClients": "algokit_utils.AlgoSdkClients", + "AccountManager": "algokit_utils.AccountManager", + "PayParams": "algokit_utils.transactions.PaymentParams", + "AlgokitComposer": "algokit_utils.TransactionComposer", + "AssetCreateParams": "algokit_utils.transactions.AssetCreateParams", + "AssetConfigParams": "algokit_utils.transactions.AssetConfigParams", + "AssetFreezeParams": "algokit_utils.transactions.AssetFreezeParams", + "AssetDestroyParams": "algokit_utils.transactions.AssetDestroyParams", + "AssetTransferParams": "algokit_utils.transactions.AssetTransferParams", + "AssetOptInParams": "algokit_utils.transactions.AssetOptInParams", + "AppCallParams": "algokit_utils.transactions.AppCallParams", + "MethodCallParams": "algokit_utils.transactions.MethodCallParams", + "OnlineKeyRegParams": "algokit_utils.transactions.OnlineKeyRegistrationParams", + } + + if name in param_mappings: + deprecated_import_error(f"algokit_utils.beta.{name}", param_mappings[name]) + + raise AttributeError(f"module 'algokit_utils.beta' has no attribute '{name}'") diff --git a/src/algokit_utils/beta/account_manager.py b/src/algokit_utils/beta/account_manager.py index 7eddff75..90835e43 100644 --- a/src/algokit_utils/beta/account_manager.py +++ b/src/algokit_utils/beta/account_manager.py @@ -1,200 +1,9 @@ -from collections.abc import Callable -from dataclasses import dataclass from typing import Any -from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account -from algosdk.account import generate_account -from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner -from typing_extensions import Self +from algokit_utils.beta._utils import handle_getattr -from .client_manager import ClientManager +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" -@dataclass -class AddressAndSigner: - address: str - signer: TransactionSigner - - -class AccountManager: - """Creates and keeps track of addresses and signers""" - - def __init__(self, client_manager: ClientManager): - """ - Create a new account manager. - - :param client_manager: The ClientManager client to use for algod and kmd clients - """ - self._client_manager = client_manager - self._accounts = dict[str, TransactionSigner]() - 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` - :return: The `AccountManager` so method calls can be chained - """ - self._default_signer = signer - return self - - def set_signer(self, sender: str, signer: TransactionSigner) -> Self: - """ - Tracks the given account for later signing. - - :param sender: The sender address to use this signer for - :param signer: The signer to sign transactions with for the given sender - :return: The AccountCreator instance for method chaining - """ - self._accounts[sender] = signer - return self - - def get_signer(self, sender: str) -> TransactionSigner: - """ - Returns the `TransactionSigner` for the given sender address. - - If no signer has been registered for that address then the default signer is used if registered. - - :param sender: The sender address - :return: The `TransactionSigner` or throws an error if not found - """ - signer = self._accounts.get(sender, None) or 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]: - """ - 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) - 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 - - # TODO - # def from_mnemonic(self, mnemonic_secret: str, sender: Optional[str] = None) -> AddrAndSigner: - # """ - # Tracks and returns an Algorand account with secret key loaded (i.e. that can sign transactions) by taking the mnemonic secret. - - # Example: - # account = account.from_mnemonic("mnemonic secret ...") - # rekeyed_account = account.from_mnemonic("mnemonic secret ...", "SENDERADDRESS...") - - # :param mnemonic_secret: The mnemonic secret representing the private key of an account; **Note: Be careful how the mnemonic is handled**, - # never commit it into source control and ideally load it from the environment (ideally via a secret storage service) rather than the file system. - # :param sender: The optional sender address to use this signer for (aka a rekeyed account) - # :return: The account - # """ - # account = mnemonic_account(mnemonic_secret) - # return self.signer_account(rekeyed_account(account, sender) if sender else account) - - def from_kmd( - self, - name: str, - predicate: Callable[[dict[str, Any]], bool] | None = None, - ) -> AddressAndSigner: - """ - Tracks and returns an Algorand account with private key loaded from the given KMD wallet (identified by name). - - Example (Get default funded account in a LocalNet): - default_dispenser_account = account.from_kmd('unencrypted-default-wallet', - lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000 - ) - - :param name: The name of the wallet to retrieve an account from - :param predicate: An optional filter to use to find the account (otherwise it will return a random account from the wallet) - :return: The account - """ - account = get_kmd_wallet_account( - name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd - ) - if not 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) - - # TODO - # def multisig( - # self, multisig_params: algosdk.MultisigMetadata, signing_accounts: Union[algosdk.Account, SigningAccount] - # ) -> TransactionSignerAccount: - # """ - # Tracks and returns an account that supports partial or full multisig signing. - - # Example: - # account = account.multisig( - # { - # "version": 1, - # "threshold": 1, - # "addrs": ["ADDRESS1...", "ADDRESS2..."] - # }, - # account.from_environment('ACCOUNT1') - # ) - - # :param multisig_params: The parameters that define the multisig account - # :param signing_accounts: The signers that are currently present - # :return: A multisig account wrapper - # """ - # return self.signer_account(multisig_account(multisig_params, signing_accounts)) - - def random(self) -> AddressAndSigner: - """ - Tracks and returns a new, random Algorand account with secret key loaded. - - Example: - account = account.random() - - :return: The account - """ - (sk, addr) = generate_account() # type: ignore[no-untyped-call] - signer = AccountTransactionSigner(sk) - - self.set_signer(addr, signer) - - return AddressAndSigner(address=addr, signer=signer) - - def dispenser(self) -> AddressAndSigner: - """ - Returns an account (with private key loaded) that can act as a dispenser. - - Example: - account = account.dispenser() - - 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']. - - :return: The account - """ - acct = get_dispenser_account(self._client_manager.algod) - - self.set_signer(acct.address, acct.signer) - - return AddressAndSigner(address=acct.address, signer=acct.signer) - - def localnet_dispenser(self) -> AddressAndSigner: - """ - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts). - - Example: - account = account.localnet_dispenser() - - :return: The account - """ - acct = get_localnet_default_account(self._client_manager.algod) - self.set_signer(acct.address, acct.signer) - return AddressAndSigner(address=acct.address, signer=acct.signer) + handle_getattr(name) diff --git a/src/algokit_utils/beta/algorand_client.py b/src/algokit_utils/beta/algorand_client.py index e80dadaf..90835e43 100644 --- a/src/algokit_utils/beta/algorand_client.py +++ b/src/algokit_utils/beta/algorand_client.py @@ -1,319 +1,9 @@ -import copy -import time -from collections.abc import Callable -from dataclasses import dataclass from typing import Any -from algokit_utils.beta.account_manager import AccountManager -from algokit_utils.beta.client_manager import AlgoSdkClients, ClientManager -from algokit_utils.beta.composer import ( - AlgokitComposer, - AppCallParams, - AssetConfigParams, - AssetCreateParams, - AssetDestroyParams, - AssetFreezeParams, - AssetOptInParams, - AssetTransferParams, - MethodCallParams, - OnlineKeyRegParams, - PayParams, -) -from algokit_utils.network_clients import ( - AlgoClientConfigs, - get_algod_client, - get_algonode_config, - get_default_localnet_config, - get_indexer_client, - get_kmd_client, -) -from algosdk.atomic_transaction_composer import AtomicTransactionResponse, TransactionSigner -from algosdk.transaction import SuggestedParams, Transaction, wait_for_confirmation -from typing_extensions import Self +from algokit_utils.beta._utils import handle_getattr -__all__ = [ - "AlgorandClient", - "AssetCreateParams", - "AssetOptInParams", - "MethodCallParams", - "PayParams", - "AssetFreezeParams", - "AssetConfigParams", - "AssetDestroyParams", - "AppCallParams", - "OnlineKeyRegParams", - "AssetTransferParams", -] +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" -@dataclass -class AlgorandClientSendMethods: - """ - Methods used to send a transaction to the network and wait for confirmation - """ - - payment: Callable[[PayParams], dict[str, Any]] - asset_create: Callable[[AssetCreateParams], dict[str, Any]] - asset_config: Callable[[AssetConfigParams], dict[str, Any]] - asset_freeze: Callable[[AssetFreezeParams], dict[str, Any]] - asset_destroy: Callable[[AssetDestroyParams], dict[str, Any]] - asset_transfer: Callable[[AssetTransferParams], dict[str, Any]] - app_call: Callable[[AppCallParams], dict[str, Any]] - online_key_reg: Callable[[OnlineKeyRegParams], dict[str, Any]] - method_call: Callable[[MethodCallParams], dict[str, Any]] - asset_opt_in: Callable[[AssetOptInParams], dict[str, Any]] - - -@dataclass -class AlgorandClientTransactionMethods: - """ - Methods used to form a transaction without signing or sending to the network - """ - - payment: Callable[[PayParams], Transaction] - asset_create: Callable[[AssetCreateParams], Transaction] - asset_config: Callable[[AssetConfigParams], Transaction] - asset_freeze: Callable[[AssetFreezeParams], Transaction] - asset_destroy: Callable[[AssetDestroyParams], Transaction] - asset_transfer: Callable[[AssetTransferParams], Transaction] - app_call: Callable[[AppCallParams], Transaction] - online_key_reg: Callable[[OnlineKeyRegParams], Transaction] - method_call: Callable[[MethodCallParams], list[Transaction]] - asset_opt_in: Callable[[AssetOptInParams], Transaction] - - -class AlgorandClient: - """A client that brokers easy access to Algorand functionality.""" - - def __init__(self, config: AlgoClientConfigs | AlgoSdkClients): - self._client_manager: ClientManager = ClientManager(config) - self._account_manager: AccountManager = AccountManager(self._client_manager) - - self._cached_suggested_params: SuggestedParams | None = None - self._cached_suggested_params_expiry: float | None = None - self._cached_suggested_params_timeout: int = 3_000 # three seconds - - self._default_validity_window: int = 10 - - def _unwrap_single_send_result(self, results: AtomicTransactionResponse) -> dict[str, Any]: - return { - "confirmation": wait_for_confirmation(self._client_manager.algod, results.tx_ids[0]), - "tx_id": results.tx_ids[0], - } - - def set_default_validity_window(self, validity_window: int) -> Self: - """ - Sets the default validity window for transactions. - - :param validity_window: The number of rounds between the first and last valid rounds - :return: The `AlgorandClient` so method calls can be chained - """ - self._default_validity_window = validity_window - return self - - 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` - :return: The `AlgorandClient` so method calls can be chained - """ - self._account_manager.set_default_signer(signer) - return self - - def set_signer(self, sender: str, signer: TransactionSigner) -> Self: - """ - Tracks the given account for later signing. - - :param sender: The sender address to use this signer for - :param signer: The signer to sign transactions with for the given sender - :return: The `AlgorandClient` so method calls can be chained - """ - self._account_manager.set_signer(sender, signer) - return self - - def set_suggested_params(self, suggested_params: SuggestedParams, until: float | None = None) -> Self: - """ - Sets a cache value to use for suggested params. - - :param suggested_params: The suggested params to use - :param until: A timestamp until which to cache, or if not specified then the timeout is used - :return: The `AlgorandClient` so method calls can be chained - """ - self._cached_suggested_params = suggested_params - self._cached_suggested_params_expiry = until or time.time() + self._cached_suggested_params_timeout - return self - - def set_suggested_params_timeout(self, timeout: int) -> Self: - """ - Sets the timeout for caching suggested params. - - :param timeout: The timeout in milliseconds - :return: The `AlgorandClient` so method calls can be chained - """ - self._cached_suggested_params_timeout = timeout - return self - - def get_suggested_params(self) -> SuggestedParams: - """Get suggested params for a transaction (either cached or from algod if the cache is stale or empty)""" - if self._cached_suggested_params and ( - self._cached_suggested_params_expiry is None or self._cached_suggested_params_expiry > time.time() - ): - return copy.deepcopy(self._cached_suggested_params) - - self._cached_suggested_params = self._client_manager.algod.suggested_params() - self._cached_suggested_params_expiry = time.time() + self._cached_suggested_params_timeout - - return copy.deepcopy(self._cached_suggested_params) - - @property - def client(self) -> ClientManager: - """Get clients, including algosdk clients and app clients.""" - return self._client_manager - - @property - def account(self) -> AccountManager: - """Get or create accounts that can sign transactions.""" - return self._account_manager - - def new_group(self) -> AlgokitComposer: - """Start a new `AlgokitComposer` transaction group""" - return AlgokitComposer( - algod=self.client.algod, - get_signer=lambda addr: self.account.get_signer(addr), - get_suggested_params=self.get_suggested_params, - default_validity_window=self._default_validity_window, - ) - - @property - def send(self) -> AlgorandClientSendMethods: - """Methods for sending a transaction and waiting for confirmation""" - return AlgorandClientSendMethods( - payment=lambda params: self._unwrap_single_send_result(self.new_group().add_payment(params).execute()), - asset_create=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_create(params).execute() - ), - asset_config=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_config(params).execute() - ), - asset_freeze=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_freeze(params).execute() - ), - asset_destroy=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_destroy(params).execute() - ), - asset_transfer=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_transfer(params).execute() - ), - app_call=lambda params: self._unwrap_single_send_result(self.new_group().add_app_call(params).execute()), - online_key_reg=lambda params: self._unwrap_single_send_result( - self.new_group().add_online_key_reg(params).execute() - ), - method_call=lambda params: self._unwrap_single_send_result( - self.new_group().add_method_call(params).execute() - ), - asset_opt_in=lambda params: self._unwrap_single_send_result( - self.new_group().add_asset_opt_in(params).execute() - ), - ) - - @property - def transactions(self) -> AlgorandClientTransactionMethods: - """Methods for building transactions""" - - return AlgorandClientTransactionMethods( - payment=lambda params: self.new_group().add_payment(params).build_group()[0].txn, - asset_create=lambda params: self.new_group().add_asset_create(params).build_group()[0].txn, - asset_config=lambda params: self.new_group().add_asset_config(params).build_group()[0].txn, - asset_freeze=lambda params: self.new_group().add_asset_freeze(params).build_group()[0].txn, - asset_destroy=lambda params: self.new_group().add_asset_destroy(params).build_group()[0].txn, - asset_transfer=lambda params: self.new_group().add_asset_transfer(params).build_group()[0].txn, - app_call=lambda params: self.new_group().add_app_call(params).build_group()[0].txn, - online_key_reg=lambda params: self.new_group().add_online_key_reg(params).build_group()[0].txn, - method_call=lambda params: [txn.txn for txn in self.new_group().add_method_call(params).build_group()], - asset_opt_in=lambda params: self.new_group().add_asset_opt_in(params).build_group()[0].txn, - ) - - @staticmethod - def default_local_net() -> "AlgorandClient": - """ - Returns an `AlgorandClient` pointing at default LocalNet ports and API token. - - :return: The `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"), - ) - ) - - @staticmethod - def test_net() -> "AlgorandClient": - """ - Returns an `AlgorandClient` pointing at TestNet using AlgoNode. - - :return: The `AlgorandClient` - """ - return AlgorandClient( - AlgoClientConfigs( - algod_config=get_algonode_config("testnet", "algod", ""), - indexer_config=get_algonode_config("testnet", "indexer", ""), - kmd_config=None, - ) - ) - - @staticmethod - def main_net() -> "AlgorandClient": - """ - Returns an `AlgorandClient` pointing at MainNet using AlgoNode. - - :return: The `AlgorandClient` - """ - return AlgorandClient( - AlgoClientConfigs( - algod_config=get_algonode_config("mainnet", "algod", ""), - indexer_config=get_algonode_config("mainnet", "indexer", ""), - kmd_config=None, - ) - ) - - @staticmethod - def from_clients(clients: AlgoSdkClients) -> "AlgorandClient": - """ - Returns an `AlgorandClient` pointing to the given client(s). - - :param clients: The clients to use - :return: The `AlgorandClient` - """ - return AlgorandClient(clients) - - @staticmethod - def from_environment() -> "AlgorandClient": - """ - Returns an `AlgorandClient` loading the configuration from environment variables. - - Retrieve configurations from environment variables when defined or get defaults. - - Expects to be called from a Python environment. - - :return: The `AlgorandClient` - """ - return AlgorandClient( - AlgoSdkClients( - algod=get_algod_client(), - kmd=get_kmd_client(), - indexer=get_indexer_client(), - ) - ) - - @staticmethod - def from_config(config: AlgoClientConfigs) -> "AlgorandClient": - """ - Returns an `AlgorandClient` from the given config. - - :param config: The config to use - :return: The `AlgorandClient` - """ - return AlgorandClient(config) + handle_getattr(name) diff --git a/src/algokit_utils/beta/client_manager.py b/src/algokit_utils/beta/client_manager.py index 1069eacf..90835e43 100644 --- a/src/algokit_utils/beta/client_manager.py +++ b/src/algokit_utils/beta/client_manager.py @@ -1,78 +1,9 @@ -import algosdk -from algokit_utils.dispenser_api import TestNetDispenserApiClient -from algokit_utils.network_clients import AlgoClientConfigs, get_algod_client, get_indexer_client, get_kmd_client -from algosdk.kmd import KMDClient -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient +from typing import Any +from algokit_utils.beta._utils import handle_getattr -class AlgoSdkClients: - """ - Clients from algosdk that interact with the official Algorand APIs. - Attributes: - algod (AlgodClient): Algod client, see https://developer.algorand.org/docs/rest-apis/algod/ - indexer (Optional[IndexerClient]): Optional indexer client, see https://developer.algorand.org/docs/rest-apis/indexer/ - kmd (Optional[KMDClient]): Optional KMD client, see https://developer.algorand.org/docs/rest-apis/kmd/ - """ +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" - def __init__( - self, - algod: algosdk.v2client.algod.AlgodClient, - indexer: IndexerClient | None = None, - kmd: KMDClient | None = None, - ): - self.algod = algod - self.indexer = indexer - self.kmd = kmd - - -class ClientManager: - """ - Exposes access to various API clients. - - Args: - clients_or_config (Union[AlgoConfig, AlgoSdkClients]): algosdk clients or config for interacting with the official Algorand APIs. - """ - - def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients): - if isinstance(clients_or_configs, AlgoSdkClients): - _clients = clients_or_configs - 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) - 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, - ) - self._algod = _clients.algod - self._indexer = _clients.indexer - self._kmd = _clients.kmd - - @property - def algod(self) -> AlgodClient: - """Returns an algosdk Algod API client.""" - return self._algod - - @property - def indexer(self) -> IndexerClient: - """Returns an algosdk Indexer API client or raises an error if it's not been provided.""" - if not self._indexer: - raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured") - return self._indexer - - @property - def kmd(self) -> KMDClient: - """Returns an algosdk KMD API client or raises an error if it's not been provided.""" - if not self._kmd: - raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured") - return self._kmd - - def get_testnet_dispenser( - self, auth_token: str | None = None, request_timeout: int | None = None - ) -> TestNetDispenserApiClient: - if request_timeout: - return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout) - - return TestNetDispenserApiClient(auth_token=auth_token) + handle_getattr(name) diff --git a/src/algokit_utils/beta/composer.py b/src/algokit_utils/beta/composer.py index c474c9be..90835e43 100644 --- a/src/algokit_utils/beta/composer.py +++ b/src/algokit_utils/beta/composer.py @@ -1,716 +1,9 @@ -from collections.abc import Callable -from dataclasses import dataclass -from typing import Union +from typing import Any -import algosdk -from algosdk.abi import Method -from algosdk.atomic_transaction_composer import ( - AtomicTransactionComposer, - AtomicTransactionResponse, - TransactionSigner, - TransactionWithSigner, -) -from algosdk.box_reference import BoxReference -from algosdk.transaction import OnComplete -from algosdk.v2client.algod import AlgodClient +from algokit_utils.beta._utils import handle_getattr -@dataclass(frozen=True) -class SenderParam: - sender: str +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Handle deprecated imports of parameter classes""" - -@dataclass(frozen=True) -class CommonTxnParams: - """ - Common transaction parameters. - - :param signer: The function used to sign transactions. - :param rekey_to: Change the signing key of the sender to the given address. - :param note: Note to attach to the transaction. - :param lease: Prevent multiple transactions with the same lease being included within the validity window. - :param static_fee: The transaction fee. In most cases you want to use `extra_fee` unless setting the fee to 0 to be covered by another transaction. - :param extra_fee: The fee to pay IN ADDITION to the suggested fee. Useful for covering inner transaction fees. - :param max_fee: Throw an error if the fee for the transaction is more than this amount. - :param validity_window: How many rounds the transaction should be valid for. - :param first_valid_round: Set the first round this transaction is valid. If left undefined, the value from algod will be used. Only set this when you intentionally want this to be some time in the future. - :param last_valid_round: The last round this transaction is valid. It is recommended to use validity_window instead. - """ - - signer: TransactionSigner | None = None - rekey_to: str | None = None - note: bytes | None = None - lease: bytes | None = None - static_fee: int | None = None - extra_fee: int | None = None - max_fee: int | None = None - validity_window: int | None = None - first_valid_round: int | None = None - last_valid_round: int | None = None - - -@dataclass(frozen=True) -class _RequiredPayTxnParams(SenderParam): - receiver: str - amount: int - - -@dataclass(frozen=True) -class PayParams(CommonTxnParams, _RequiredPayTxnParams): - """ - Payment transaction parameters. - - :param receiver: The account that will receive the ALGO. - :param amount: Amount to send. - :param close_remainder_to: If given, close the sender account and send the remaining balance to this address. - """ - - close_remainder_to: str | None = None - - -@dataclass(frozen=True) -class _RequiredAssetCreateParams(SenderParam): - total: int - - -@dataclass(frozen=True) -class AssetCreateParams(CommonTxnParams, _RequiredAssetCreateParams): - """ - Asset creation parameters. - - :param total: The total amount of the smallest divisible unit to create. - :param decimals: The amount of decimal places the asset should have. - :param default_frozen: Whether the asset is frozen by default in the creator address. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. Clawback will be permanently disabled if undefined or an empty string. - :param unit_name: The short ticker name for the asset. - :param asset_name: The full name of the asset. - :param url: The metadata URL for the asset. - :param metadata_hash: Hash of the metadata contained in the metadata URL. - """ - - decimals: int | None = None - default_frozen: bool | None = None - manager: str | None = None - reserve: str | None = None - freeze: str | None = None - clawback: str | None = None - unit_name: str | None = None - asset_name: str | None = None - url: str | None = None - metadata_hash: bytes | None = None - - -@dataclass(frozen=True) -class _RequiredAssetConfigParams(SenderParam): - asset_id: int - - -@dataclass(frozen=True) -class AssetConfigParams(CommonTxnParams, _RequiredAssetConfigParams): - """ - Asset configuration parameters. - - :param asset_id: ID of the asset. - :param manager: The address that can change the manager, reserve, clawback, and freeze addresses. There will permanently be no manager if undefined or an empty string. - :param reserve: The address that holds the uncirculated supply. - :param freeze: The address that can freeze the asset in any account. Freezing will be permanently disabled if undefined or an empty string. - :param clawback: The address that can clawback the asset from any account. Clawback will be permanently disabled if undefined or an empty string. - """ - - manager: str | None = None - reserve: str | None = None - freeze: str | None = None - clawback: str | None = None - - -@dataclass(frozen=True) -class _RequiredAssetFreezeParams(SenderParam): - asset_id: int - account: str - frozen: bool - - -@dataclass(frozen=True) -class AssetFreezeParams(CommonTxnParams, _RequiredAssetFreezeParams): - """ - Asset freeze parameters. - - :param asset_id: The ID of the asset. - :param account: The account to freeze or unfreeze. - :param frozen: Whether the assets in the account should be frozen. - """ - - -@dataclass(frozen=True) -class _RequiredAssetDestroyParams(SenderParam): - asset_id: int - - -@dataclass(frozen=True) -class AssetDestroyParams(CommonTxnParams, _RequiredAssetDestroyParams): - """ - Asset destruction parameters. - - :param asset_id: ID of the asset. - """ - - -@dataclass(frozen=True) -class _RequiredOnlineKeyRegParams(SenderParam): - vote_key: str - selection_key: str - vote_first: int - vote_last: int - vote_key_dilution: int - - -@dataclass(frozen=True) -class OnlineKeyRegParams(CommonTxnParams, _RequiredOnlineKeyRegParams): - """ - Online key registration parameters. - - :param vote_key: The root participation public key. - :param selection_key: The VRF public key. - :param vote_first: The first round that the participation key is valid. Not to be confused with the `first_valid` round of the keyreg transaction. - :param vote_last: The last round that the participation key is valid. Not to be confused with the `last_valid` round of the keyreg transaction. - :param vote_key_dilution: This is the dilution for the 2-level participation key. It determines the interval (number of rounds) for generating new ephemeral keys. - :param state_proof_key: The 64 byte state proof public key commitment. - """ - - state_proof_key: bytes | None = None - - -@dataclass(frozen=True) -class _RequiredAssetTransferParams(SenderParam): - asset_id: int - amount: int - receiver: str - - -@dataclass(frozen=True) -class AssetTransferParams(CommonTxnParams, _RequiredAssetTransferParams): - """ - Asset transfer parameters. - - :param asset_id: ID of the asset. - :param amount: Amount of the asset to transfer (smallest divisible unit). - :param receiver: The account to send the asset to. - :param clawback_target: The account to take the asset from. - :param close_asset_to: The account to close the asset to. - """ - - clawback_target: str | None = None - close_asset_to: str | None = None - - -@dataclass(frozen=True) -class _RequiredAssetOptInParams(SenderParam): - asset_id: int - - -@dataclass(frozen=True) -class AssetOptInParams(CommonTxnParams, _RequiredAssetOptInParams): - """ - Asset opt-in parameters. - - :param asset_id: ID of the asset. - """ - - -@dataclass(frozen=True) -class AppCallParams(CommonTxnParams, SenderParam): - """ - Application call parameters. - - :param on_complete: The OnComplete action. - :param app_id: ID of the application. - :param approval_program: The program to execute for all OnCompletes other than ClearState. - :param clear_program: The program to execute for ClearState OnComplete. - :param schema: The state schema for the app. This is immutable. - :param args: Application arguments. - :param account_references: Account references. - :param app_references: App references. - :param asset_references: Asset references. - :param extra_pages: Number of extra pages required for the programs. - :param box_references: Box references. - """ - - on_complete: OnComplete | None = None - app_id: int | None = None - approval_program: bytes | None = None - clear_program: bytes | None = None - schema: dict[str, int] | None = None - args: list[bytes] | None = None - account_references: list[str] | None = None - app_references: list[int] | None = None - asset_references: list[int] | None = None - extra_pages: int | None = None - box_references: list[BoxReference] | None = None - - -@dataclass(frozen=True) -class _RequiredMethodCallParams(SenderParam): - app_id: int - method: Method - - -@dataclass(frozen=True) -class MethodCallParams(CommonTxnParams, _RequiredMethodCallParams): - """ - Method call parameters. - - :param app_id: ID of the application. - :param method: The ABI method to call. - :param args: Arguments to the ABI method. - """ - - args: list | None = None - - -TxnParams = Union[ # noqa: UP007 - PayParams, - AssetCreateParams, - AssetConfigParams, - AssetFreezeParams, - AssetDestroyParams, - OnlineKeyRegParams, - AssetTransferParams, - AssetOptInParams, - AppCallParams, - MethodCallParams, -] - - -class AlgokitComposer: - """ - A class for composing and managing Algorand transactions using the Algosdk library. - - Attributes: - txn_method_map (dict[str, algosdk.abi.Method]): A dictionary that maps transaction IDs to their corresponding ABI methods. - txns (List[Union[TransactionWithSigner, TxnParams, AtomicTransactionComposer]]): A list of transactions that have not yet been composed. - atc (AtomicTransactionComposer): An instance of AtomicTransactionComposer used to compose transactions. - algod (AlgodClient): The AlgodClient instance used by the composer for suggested params. - get_suggested_params (Callable[[], algosdk.future.transaction.SuggestedParams]): A function that returns suggested parameters for transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a TransactionSigner for that address. - default_validity_window (int): The default validity window for transactions. - """ - - def __init__( - self, - algod: AlgodClient, - get_signer: Callable[[str], TransactionSigner], - get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None, - default_validity_window: int | None = None, - ): - """ - Initialize an instance of the AlgokitComposer class. - - Args: - algod (AlgodClient): An instance of AlgodClient used to get suggested params and send transactions. - get_signer (Callable[[str], TransactionSigner]): A function that takes an address as input and returns a TransactionSigner for that address. - get_suggested_params (Optional[Callable[[], algosdk.future.transaction.SuggestedParams]], optional): A function that returns suggested parameters for transactions. If not provided, it defaults to using algod.suggested_params(). Defaults to None. - default_validity_window (Optional[int], optional): The default validity window for transactions. If not provided, it defaults to 10. Defaults to None. - """ - self.txn_method_map: dict[str, algosdk.abi.Method] = {} - self.txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] - self.atc: AtomicTransactionComposer = AtomicTransactionComposer() - self.algod: AlgodClient = algod - self.default_get_send_params = lambda: self.algod.suggested_params() - self.get_suggested_params = get_suggested_params or self.default_get_send_params - self.get_signer: Callable[[str], TransactionSigner] = get_signer - self.default_validity_window: int = default_validity_window or 10 - - def add_payment(self, params: PayParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_create(self, params: AssetCreateParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_config(self, params: AssetConfigParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_freeze(self, params: AssetFreezeParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_destroy(self, params: AssetDestroyParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_transfer(self, params: AssetTransferParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_asset_opt_in(self, params: AssetOptInParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_app_call(self, params: AppCallParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_online_key_reg(self, params: OnlineKeyRegParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def add_atc(self, atc: AtomicTransactionComposer) -> "AlgokitComposer": - self.txns.append(atc) - return self - - def add_method_call(self, params: MethodCallParams) -> "AlgokitComposer": - self.txns.append(params) - return self - - def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSigner]: - group = atc.build_group() - - for ts in group: - ts.txn.group = None - - method = atc.method_dict.get(len(group) - 1) - if method: - self.txn_method_map[group[-1].txn.get_txid()] = method # type: ignore[no-untyped-call] - - return group - - def _common_txn_build_step( - self, - params: CommonTxnParams, - txn: algosdk.transaction.Transaction, - suggested_params: algosdk.transaction.SuggestedParams, - ) -> algosdk.transaction.Transaction: - if params.lease: - txn.lease = params.lease - if params.rekey_to: - txn.rekey_to = params.rekey_to - if params.note: - txn.note = params.note - - if params.first_valid_round: - txn.first_valid_round = params.first_valid_round - - if params.last_valid_round: - txn.last_valid_round = params.last_valid_round - else: - txn.last_valid_round = txn.first_valid_round + (params.validity_window or self.default_validity_window) - - if params.static_fee is not None and params.extra_fee is not None: - raise ValueError("Cannot set both static_fee and extra_fee") - - if params.static_fee is not None: - txn.fee = params.static_fee - else: - txn.fee = txn.estimate_size() * suggested_params.fee or algosdk.constants.min_txn_fee # type: ignore[no-untyped-call] - if params.extra_fee: - txn.fee += params.extra_fee - - if params.max_fee is not None and txn.fee > params.max_fee: - raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}") - - return txn - - def _build_payment( - self, params: PayParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.PaymentTxn( - sender=params.sender, - sp=suggested_params, - receiver=params.receiver, - amt=params.amount, - close_remainder_to=params.close_remainder_to, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_asset_create( - self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetConfigTxn( - sender=params.sender, - sp=suggested_params, - total=params.total, - default_frozen=params.default_frozen or False, - unit_name=params.unit_name, - asset_name=params.asset_name, - manager=params.manager, - reserve=params.reserve, - freeze=params.freeze, - clawback=params.clawback, - url=params.url, - metadata_hash=params.metadata_hash, - decimals=params.decimals or 0, - strict_empty_address_check=False, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_app_call( - self, params: AppCallParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - sdk_params = { - "sender": params.sender, - "sp": suggested_params, - "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, - "approval_program": params.approval_program, - "clear_program": params.clear_program, - "app_args": params.args, - "accounts": params.account_references, - "foreign_apps": params.app_references, - "foreign_assets": params.asset_references, - "extra_pages": params.extra_pages, - "local_schema": algosdk.transaction.StateSchema( - num_uints=params.schema.get("local_uints", 0), num_byte_slices=params.schema.get("local_byte_slices", 0) - ) # type: ignore[no-untyped-call] - if params.schema - else None, - "global_schema": algosdk.transaction.StateSchema( - num_uints=params.schema.get("global_uints", 0), - num_byte_slices=params.schema.get("global_byte_slices", 0), - ) # type: ignore[no-untyped-call] - if params.schema - else None, - } - - if not params.app_id: - if params.approval_program is None or params.clear_program is None: - raise ValueError("approval_program and clear_program are required for application creation") - - txn = algosdk.transaction.ApplicationCreateTxn(**sdk_params) # type: ignore[no-untyped-call] - else: - sdk_params["index"] = params.app_id - txn = algosdk.transaction.ApplicationCallTxn(**sdk_params) # type: ignore[assignment,no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_asset_config( - self, params: AssetConfigParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetConfigTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - manager=params.manager, - reserve=params.reserve, - freeze=params.freeze, - clawback=params.clawback, - strict_empty_address_check=False, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_asset_destroy( - self, params: AssetDestroyParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetDestroyTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_asset_freeze( - self, params: AssetFreezeParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetFreezeTxn( - sender=params.sender, - sp=suggested_params, - index=params.asset_id, - target=params.account, - new_freeze_state=params.frozen, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_asset_transfer( - self, params: AssetTransferParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.AssetTransferTxn( - sender=params.sender, - sp=suggested_params, - receiver=params.receiver, - amt=params.amount, - index=params.asset_id, - close_assets_to=params.close_asset_to, - revocation_target=params.clawback_target, - ) # type: ignore[no-untyped-call] - - return self._common_txn_build_step(params, txn, suggested_params) - - def _build_key_reg( - self, params: OnlineKeyRegParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> algosdk.transaction.Transaction: - txn = algosdk.transaction.KeyregTxn( - sender=params.sender, - sp=suggested_params, - votekey=params.vote_key, - selkey=params.selection_key, - votefst=params.vote_first, - votelst=params.vote_last, - votekd=params.vote_key_dilution, - rekey_to=params.rekey_to, - nonpart=False, - sprfkey=params.state_proof_key, - ) # type: ignore[no-untyped-call] - - 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): - return len(x) == 0 or all(self._is_abi_value(item) for item in x) - - return isinstance(x, bool | int | float | str | bytes) - - def _build_method_call( # noqa: C901, PLR0912 - self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams - ) -> list[TransactionWithSigner]: - method_args = [] - arg_offset = 0 - - if params.args: - for i, arg in enumerate(params.args): - if self._is_abi_value(arg): - method_args.append(arg) - continue - - if algosdk.abi.is_abi_transaction_type(params.method.args[i + arg_offset].type): - match arg: - case MethodCallParams(): - temp_txn_with_signers = self._build_method_call(arg, suggested_params) - method_args.extend(temp_txn_with_signers) - arg_offset += len(temp_txn_with_signers) - 1 - continue - case AppCallParams(): - txn = self._build_app_call(arg, suggested_params) - case PayParams(): - txn = self._build_payment(arg, suggested_params) - case AssetOptInParams(): - txn = self._build_asset_transfer( - AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params - ) - case AssetCreateParams(): - txn = self._build_asset_create(arg, suggested_params) - case AssetConfigParams(): - txn = self._build_asset_config(arg, suggested_params) - case AssetDestroyParams(): - txn = self._build_asset_destroy(arg, suggested_params) - case AssetFreezeParams(): - txn = self._build_asset_freeze(arg, suggested_params) - case AssetTransferParams(): - txn = self._build_asset_transfer(arg, suggested_params) - case OnlineKeyRegParams(): - txn = self._build_key_reg(arg, suggested_params) - case _: - raise ValueError(f"Unsupported method arg transaction type: {arg}") - - method_args.append( - TransactionWithSigner(txn=txn, signer=params.signer or self.get_signer(params.sender)) - ) - - continue - - raise ValueError(f"Unsupported method arg: {arg}") - - method_atc = AtomicTransactionComposer() - - method_atc.add_method_call( - app_id=params.app_id or 0, - method=params.method, - sender=params.sender, - sp=suggested_params, - signer=params.signer or self.get_signer(params.sender), - method_args=method_args, - on_complete=algosdk.transaction.OnComplete.NoOpOC, - note=params.note, - lease=params.lease, - ) - - return self._build_atc(method_atc) - - def _build_txn( # noqa: C901, PLR0912 - self, - txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, - suggested_params: algosdk.transaction.SuggestedParams, - ) -> list[TransactionWithSigner]: - match txn: - case TransactionWithSigner(): - return [txn] - case AtomicTransactionComposer(): - return self._build_atc(txn) - case MethodCallParams(): - return self._build_method_call(txn, suggested_params) - - signer = txn.signer or self.get_signer(txn.sender) - - match txn: - case PayParams(): - payment = self._build_payment(txn, suggested_params) - return [TransactionWithSigner(txn=payment, signer=signer)] - case AssetCreateParams(): - asset_create = self._build_asset_create(txn, suggested_params) - return [TransactionWithSigner(txn=asset_create, signer=signer)] - case AppCallParams(): - app_call = self._build_app_call(txn, suggested_params) - return [TransactionWithSigner(txn=app_call, signer=signer)] - case AssetConfigParams(): - asset_config = self._build_asset_config(txn, suggested_params) - return [TransactionWithSigner(txn=asset_config, signer=signer)] - case AssetDestroyParams(): - asset_destroy = self._build_asset_destroy(txn, suggested_params) - return [TransactionWithSigner(txn=asset_destroy, signer=signer)] - case AssetFreezeParams(): - asset_freeze = self._build_asset_freeze(txn, suggested_params) - return [TransactionWithSigner(txn=asset_freeze, signer=signer)] - case AssetTransferParams(): - asset_transfer = self._build_asset_transfer(txn, suggested_params) - return [TransactionWithSigner(txn=asset_transfer, signer=signer)] - case AssetOptInParams(): - asset_transfer = self._build_asset_transfer( - AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params - ) - return [TransactionWithSigner(txn=asset_transfer, signer=signer)] - case OnlineKeyRegParams(): - key_reg = self._build_key_reg(txn, suggested_params) - return [TransactionWithSigner(txn=key_reg, signer=signer)] - case _: - raise ValueError(f"Unsupported txn: {txn}") - - def build_group(self) -> list[TransactionWithSigner]: - suggested_params = self.get_suggested_params() - - txn_with_signers: list[TransactionWithSigner] = [] - - for txn in self.txns: - txn_with_signers.extend(self._build_txn(txn, suggested_params)) - - for ts in txn_with_signers: - self.atc.add_transaction(ts) - - method_calls = {} - - for i, ts in enumerate(txn_with_signers): - method = self.txn_method_map.get(ts.txn.get_txid()) # type: ignore[no-untyped-call] - if method: - method_calls[i] = method - - self.atc.method_dict = method_calls - - return self.atc.build_group() - - def execute(self, *, max_rounds_to_wait: int | None = None) -> AtomicTransactionResponse: - group = self.build_group() - - wait_rounds = max_rounds_to_wait - - if wait_rounds is None: - last_round = max(txn.txn.last_valid_round for txn in group) - first_round = self.get_suggested_params().first - wait_rounds = last_round - first_round - - return self.atc.execute(client=self.algod, wait_rounds=wait_rounds) + handle_getattr(name) diff --git a/src/algokit_utils/clients/__init__.py b/src/algokit_utils/clients/__init__.py new file mode 100644 index 00000000..1a48b824 --- /dev/null +++ b/src/algokit_utils/clients/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.clients.client_manager import * # noqa: F403 +from algokit_utils.clients.dispenser_api_client import * # noqa: F403 diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py new file mode 100644 index 00000000..283ff6c1 --- /dev/null +++ b/src/algokit_utils/clients/client_manager.py @@ -0,0 +1,656 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, TypeVar +from urllib import parse + +import algosdk +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.kmd import KMDClient +from algosdk.source_map import SourceMap +from algosdk.transaction import SuggestedParams +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_deployer import ApplicationLookup +from algokit_utils.applications.app_spec.arc56 import Arc56Contract +from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient +from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig +from algokit_utils.protocols.typed_clients import TypedAppClientProtocol, TypedAppFactoryProtocol + +if TYPE_CHECKING: + from algokit_utils.algorand import AlgorandClient + from algokit_utils.applications.app_client import AppClient, AppClientCompilationParams + from algokit_utils.applications.app_factory import AppFactory + +__all__ = [ + "AlgoSdkClients", + "ClientManager", + "NetworkDetail", +] + +TypedFactoryT = TypeVar("TypedFactoryT", bound=TypedAppFactoryProtocol) +TypedAppClientT = TypeVar("TypedAppClientT", bound=TypedAppClientProtocol) + + +class AlgoSdkClients: + """Container for Algorand SDK client instances. + + Holds references to Algod, Indexer and KMD clients. + + :param algod: Algod client instance + :param indexer: Optional Indexer client instance + :param kmd: Optional KMD client instance + """ + + def __init__( + self, + algod: algosdk.v2client.algod.AlgodClient, + indexer: IndexerClient | None = None, + kmd: KMDClient | None = None, + ): + self.algod = algod + self.indexer = indexer + self.kmd = kmd + + +@dataclass(kw_only=True, frozen=True) +class NetworkDetail: + """Details about an Algorand network. + + Contains network type flags and genesis information. + """ + + is_testnet: bool + is_mainnet: bool + is_localnet: bool + genesis_id: str + genesis_hash: str + + +def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkConfig: + server = os.getenv(f"{environment_prefix}_SERVER") + if server is None: + raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") + port = os.getenv(f"{environment_prefix}_PORT") + if port: + parsed = parse.urlparse(server) + server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() + return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) + + +class ClientManager: + """Manager for Algorand SDK clients. + + Provides access to Algod, Indexer and KMD clients and helper methods for working with them. + + :param clients_or_configs: Either client instances or client configurations + :param algorand_client: AlgorandClient instance + """ + + def __init__(self, clients_or_configs: AlgoClientConfigs | AlgoSdkClients, algorand_client: AlgorandClient): + if isinstance(clients_or_configs, AlgoSdkClients): + _clients = clients_or_configs + elif isinstance(clients_or_configs, AlgoClientConfigs): + _clients = AlgoSdkClients( + 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=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 + self._suggested_params: SuggestedParams | None = None + + @property + def algod(self) -> AlgodClient: + """Returns an algosdk Algod API client. + + :return: Algod client instance + """ + return self._algod + + @property + def indexer(self) -> IndexerClient: + """Returns an algosdk Indexer API client. + + :raises ValueError: If no Indexer client is configured + :return: Indexer client instance + """ + if not self._indexer: + raise ValueError("Attempt to use Indexer client in AlgoKit instance with no Indexer configured") + return self._indexer + + @property + def indexer_if_present(self) -> IndexerClient | None: + """Returns the Indexer client if configured, otherwise None. + + :return: Indexer client instance or None + """ + return self._indexer + + @property + def kmd(self) -> KMDClient: + """Returns an algosdk KMD API client. + + :raises ValueError: If no KMD client is configured + :return: KMD client instance + """ + if not self._kmd: + raise ValueError("Attempt to use Kmd client in AlgoKit instance with no Kmd configured") + return self._kmd + + def network(self) -> NetworkDetail: + """Get details about the connected Algorand network. + + :return: Network details including type and genesis information + """ + if self._suggested_params is None: + self._suggested_params = self._algod.suggested_params() + sp = self._suggested_params + return NetworkDetail( + is_testnet=sp.gen in ["testnet-v1.0", "testnet-v1", "testnet"], + is_mainnet=sp.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"], + is_localnet=ClientManager.genesis_id_is_localnet(str(sp.gen)), + genesis_id=str(sp.gen), + genesis_hash=sp.gh, + ) + + def is_localnet(self) -> bool: + """Check if connected to a local network. + + :return: True if connected to a local network + """ + return self.network().is_localnet + + def is_testnet(self) -> bool: + """Check if connected to TestNet. + + :return: True if connected to TestNet + """ + return self.network().is_testnet + + def is_mainnet(self) -> bool: + """Check if connected to MainNet. + + :return: True if connected to MainNet + """ + return self.network().is_mainnet + + def get_testnet_dispenser( + self, auth_token: str | None = None, request_timeout: int | None = None + ) -> TestNetDispenserApiClient: + """Get a TestNet dispenser API client. + + :param auth_token: Optional authentication token + :param request_timeout: Optional request timeout in seconds + :return: TestNet dispenser client instance + """ + if request_timeout: + return TestNetDispenserApiClient(auth_token=auth_token, request_timeout=request_timeout) + + 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, + compilation_params: AppClientCompilationParams | None = None, + ) -> AppFactory: + """Get an application factory for deploying smart contracts. + + :param app_spec: Application specification + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param version: Optional version string + :param compilation_params: Optional compilation parameters + :raises ValueError: If no Algorand client is configured + :return: Application factory instance + """ + from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams + + if not self._algorand: + 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, + compilation_params=compilation_params, + ) + ) + + def get_app_client_by_id( + self, + app_spec: (Arc56Contract | ApplicationSpecification | str), + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + """Get an application client for an existing application by ID. + + :param app_spec: Application specification + :param app_id: Application ID + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Application client instance + """ + from algokit_utils.applications.app_client import AppClient, AppClientParams + + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return AppClient( + AppClientParams( + app_spec=app_spec, + algorand=self._algorand, + app_id=app_id, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + ) + ) + + def get_app_client_by_network( + self, + app_spec: (Arc56Contract | ApplicationSpecification | str), + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + """Get an application client for an existing application by network. + + :param app_spec: Application specification + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Application client instance + """ + from algokit_utils.applications.app_client import AppClient + + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return AppClient.from_network( + app_spec=app_spec, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + def get_app_client_by_creator_and_name( + self, + creator_address: str, + app_name: str, + app_spec: Arc56Contract | ApplicationSpecification | str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: ApplicationLookup | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + """Get an application client by creator address and name. + + :param creator_address: Creator address + :param app_name: Application name + :param app_spec: Application specification + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :return: Application client instance + """ + from algokit_utils.applications.app_client import AppClient + + return AppClient.from_creator_and_name( + creator_address=creator_address, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + app_spec=app_spec, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + @staticmethod + def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClient: + """Get an Algod client from config or environment. + + :param config: Optional client configuration + :return: Algod client instance + """ + config = config or _get_config_from_environment("ALGOD") + headers = {"X-Algo-API-Token": config.token or ""} + return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers) + + @staticmethod + def get_algod_client_from_environment() -> AlgodClient: + """Get an Algod client from environment variables. + + :return: Algod client instance + """ + return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) + + @staticmethod + def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient: + """Get a KMD client from config or environment. + + :param config: Optional client configuration + :return: KMD client instance + """ + config = config or _get_config_from_environment("KMD") + return KMDClient(config.token, config.server) + + @staticmethod + def get_kmd_client_from_environment() -> KMDClient: + """Get a KMD client from environment variables. + + :return: KMD client instance + """ + return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) + + @staticmethod + def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> IndexerClient: + """Get an Indexer client from config or environment. + + :param config: Optional client configuration + :return: Indexer client instance + """ + config = config or _get_config_from_environment("INDEXER") + headers = {"X-Indexer-API-Token": config.token} + return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers) + + @staticmethod + def get_indexer_client_from_environment() -> IndexerClient: + """Get an Indexer client from environment variables. + + :return: Indexer client instance + """ + return ClientManager.get_indexer_client(ClientManager.get_indexer_config_from_environment()) + + @staticmethod + def genesis_id_is_localnet(genesis_id: str | None) -> bool: + """Check if a genesis ID indicates a local network. + + :param genesis_id: Genesis ID to check + :return: True if genesis ID indicates a local network + """ + return genesis_id in ["devnet-v1", "sandnet-v1", "dockernet-v1"] + + def get_typed_app_client_by_creator_and_name( + self, + typed_client: type[TypedAppClientT], + *, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: ApplicationLookup | None = None, + ) -> TypedAppClientT: + """Get a typed application client by creator address and name. + + :param typed_client: Typed client class + :param creator_address: Creator address + :param app_name: Application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param ignore_cache: Optional flag to ignore cache + :param app_lookup_cache: Optional app lookup cache + :raises ValueError: If no Algorand client is configured + :return: Typed application client instance + """ + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client.from_creator_and_name( + creator_address=creator_address, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + ignore_cache=ignore_cache, + app_lookup_cache=app_lookup_cache, + algorand=self._algorand, + ) + + def get_typed_app_client_by_id( + self, + typed_client: type[TypedAppClientT], + *, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> TypedAppClientT: + """Get a typed application client by ID. + + :param typed_client: Typed client class + :param app_id: Application ID + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: Typed application client instance + """ + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client( + app_id=app_id, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + def get_typed_app_client_by_network( + self, + typed_client: type[TypedAppClientT], + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> TypedAppClientT: + """Returns a new typed client, resolves the app ID for the current network. + + Uses pre-determined network-specific app IDs specified in the ARC-56 app spec. + If no IDs are in the app spec or the network isn't recognised, an error is thrown. + + :param typed_client: The typed client class to instantiate + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param approval_source_map: Optional approval program source map + :param clear_source_map: Optional clear program source map + :raises ValueError: If no Algorand client is configured + :return: The typed client instance + """ + if not self._algorand: + raise ValueError("Attempt to get app client from a ClientManager without an Algorand client") + + return typed_client.from_network( + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + approval_source_map=approval_source_map, + clear_source_map=clear_source_map, + algorand=self._algorand, + ) + + def get_typed_app_factory( + self, + typed_factory: type[TypedFactoryT], + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + version: str | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> TypedFactoryT: + """Get a typed application factory. + + :param typed_factory: Typed factory class + :param app_name: Optional application name + :param default_sender: Optional default sender address + :param default_signer: Optional default transaction signer + :param version: Optional version string + :param compilation_params: Optional compilation parameters + :raises ValueError: If no Algorand client is configured + :return: Typed application factory instance + """ + if not self._algorand: + raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") + + return typed_factory( + algorand=self._algorand, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + version=version, + compilation_params=compilation_params, + ) + + @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. + + :return: 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 = ( + AlgoClientNetworkConfig( + server=algod_config.server, token=algod_config.token, port=os.getenv("KMD_PORT", "4002") + ) + if not any(net in algod_server.lower() for net in ["mainnet", "testnet"]) + else None + ) + else: + # Use localnet defaults + algod_config = ClientManager.get_default_localnet_config("algod") + indexer_config = ClientManager.get_default_localnet_config("indexer") + kmd_config = ClientManager.get_default_localnet_config("kmd") + + return AlgoClientConfigs( + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config, + ) + + @staticmethod + def get_default_localnet_config( + config_or_port: Literal["algod", "indexer", "kmd"] | int, + ) -> AlgoClientNetworkConfig: + """Get default configuration for local network services. + + :param config_or_port: Service name or port number + :return: Client configuration for local network + """ + port = ( + config_or_port + if isinstance(config_or_port, int) + else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port] + ) + + return AlgoClientNetworkConfig(server=f"http://localhost:{port}", token="a" * 64) + + @staticmethod + def get_algod_config_from_environment() -> AlgoClientNetworkConfig: + """Retrieve the algod configuration from environment variables. + Will raise an error if ALGOD_SERVER environment variable is not set + + :return: Algod client configuration + """ + return _get_config_from_environment("ALGOD") + + @staticmethod + def get_indexer_config_from_environment() -> AlgoClientNetworkConfig: + """Retrieve the indexer configuration from environment variables. + Will raise an error if INDEXER_SERVER environment variable is not set + + :return: Indexer client configuration + """ + return _get_config_from_environment("INDEXER") + + @staticmethod + def get_kmd_config_from_environment() -> AlgoClientNetworkConfig: + """Retrieve the kmd configuration from environment variables. + + :return: KMD client configuration + """ + return _get_config_from_environment("KMD") + + @staticmethod + def get_algonode_config( + network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"] + ) -> AlgoClientNetworkConfig: + """Returns the Algorand configuration to point to the free tier of the AlgoNode service. + + :param network: Which network to connect to - TestNet or MainNet + :param config: Which algod config to return - Algod or Indexer + :return: Configuration for the specified network and service + """ + service_type = "api" if config == "algod" else "idx" + return AlgoClientNetworkConfig( + 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 new file mode 100644 index 00000000..e471989e --- /dev/null +++ b/src/algokit_utils/clients/dispenser_api_client.py @@ -0,0 +1,192 @@ +import contextlib +import enum +import os +from dataclasses import dataclass + +import httpx + +from algokit_utils.config import config + +__all__ = [ + "DISPENSER_ACCESS_TOKEN_KEY", + "DISPENSER_ASSETS", + "DISPENSER_REQUEST_TIMEOUT", + "DispenserApiConfig", + "DispenserAsset", + "DispenserAssetName", + "DispenserFundResponse", + "DispenserLimitResponse", + "TestNetDispenserApiClient", +] + + +logger = config.logger + + +class DispenserApiConfig: + BASE_URL = "https://api.dispenser.algorandfoundation.tools" + + +class DispenserAssetName(enum.IntEnum): + ALGO = 0 + + +@dataclass +class DispenserAsset: + asset_id: int + decimals: int + description: str + + +@dataclass +class DispenserFundResponse: + tx_id: str + amount: int + + +@dataclass +class DispenserLimitResponse: + amount: int + + +DISPENSER_ASSETS = { + DispenserAssetName.ALGO: DispenserAsset( + asset_id=0, + decimals=6, + description="Algo", + ), +} +DISPENSER_REQUEST_TIMEOUT = 15 +DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN" + + +class TestNetDispenserApiClient: + """ + Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). + To get started create a new access token via `algokit dispenser login --ci` + and pass it to the client constructor as `auth_token`. + Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, + and it will be auto loaded. If both are set, the constructor argument takes precedence. + + Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. + """ + + auth_token: str + request_timeout = DISPENSER_REQUEST_TIMEOUT + + def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT): + auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY) + + if auth_token: + self.auth_token = auth_token + elif auth_token_from_env: + self.auth_token = auth_token_from_env + else: + raise Exception( + f"Can't init AlgoKit TestNet Dispenser API client " + f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or " + "the auth_token were provided." + ) + + self.request_timeout = request_timeout + + def _process_dispenser_request( + self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST" + ) -> httpx.Response: + """ + Generalized method to process http requests to dispenser API + """ + + headers = {"Authorization": f"Bearer {(auth_token)}"} + + # Set request arguments + request_args = { + "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}", + "headers": headers, + "timeout": self.request_timeout, + } + + if method.upper() != "GET" and data is not None: + request_args["json"] = data + + try: + response: httpx.Response = getattr(httpx, method.lower())(**request_args) + response.raise_for_status() + return response + + except httpx.HTTPStatusError as err: + error_message = f"Error processing dispenser API request: {err.response.status_code}" + error_response = None + with contextlib.suppress(Exception): + error_response = err.response.json() + + if error_response and error_response.get("code"): + error_message = error_response.get("code") + + elif err.response.status_code == httpx.codes.BAD_REQUEST: + error_message = err.response.json()["message"] + + raise Exception(error_message) from err + + except Exception as err: + error_message = "Error processing dispenser API request" + logger.debug(f"{error_message}: {err}", exc_info=True) + raise err + + def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse: + """ + Fund an account with Algos from the dispenser API + """ + + try: + response = self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix=f"fund/{asset_id}", + data={"receiver": address, "amount": amount, "assetID": asset_id}, + method="POST", + ) + + content = response.json() + return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"]) + + except Exception as err: + logger.exception(f"Error funding account {address}: {err}") + raise err + + def refund(self, refund_txn_id: str) -> None: + """ + Register a refund for a transaction with the dispenser API + """ + + try: + self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix="refund", + data={"refundTransactionID": refund_txn_id}, + method="POST", + ) + + except Exception as err: + logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}") + raise err + + def get_limit( + self, + address: str, + ) -> DispenserLimitResponse: + """ + Get current limit for an account with Algos from the dispenser API + """ + + try: + response = self._process_dispenser_request( + auth_token=self.auth_token, + url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit", + method="GET", + ) + content = response.json() + + return DispenserLimitResponse(amount=content["amount"]) + except Exception as err: + logger.exception(f"Error setting limit for account {address}: {err}") + raise err diff --git a/src/algokit_utils/common.py b/src/algokit_utils/common.py index 8071c98f..c0274574 100644 --- a/src/algokit_utils/common.py +++ b/src/algokit_utils/common.py @@ -1,28 +1,10 @@ -""" -This module contains common classes and methods that are reused in more than one file. -""" +import warnings -import base64 -import typing +warnings.warn( + "The legacy v2 common module is deprecated and will be removed in a future version. " + "Refer to `CompiledTeal` class from `algokit_utils` instead.", + DeprecationWarning, + stacklevel=2, +) -from algosdk.source_map import SourceMap - -from algokit_utils import deploy - -if typing.TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - - -class Program: - """A compiled TEAL program""" - - def __init__(self, program: str, client: "AlgodClient"): - """ - Fully compile the program source to binary and generate a - source map for matching pc to line number - """ - self.teal = program - result: dict = client.compile(deploy.strip_comments(self.teal), source_map=True) - self.raw_binary = base64.b64decode(result["result"]) - self.binary_hash: str = result["hash"] - self.source_map = SourceMap(result["sourcemap"]) +from algokit_utils._legacy_v2.common import * # noqa: F403, E402 diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py index 55850fd0..6c5d7cd0 100644 --- a/src/algokit_utils/config.py +++ b/src/algokit_utils/config.py @@ -2,14 +2,57 @@ import os from collections.abc import Callable from pathlib import Path - -logger = logging.getLogger(__name__) +from typing import Any # Environment variable to override the project root ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT") 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 +62,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 +109,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,14 +122,15 @@ def with_debug(self, func: Callable[[], str | None]) -> None: finally: self._debug = original_debug - def configure( # noqa: PLR0913 + def configure( self, *, - debug: bool, + debug: bool | None = None, project_root: Path | None = None, trace_all: bool = False, trace_buffer_size_mb: float = 256, max_search_depth: int = 10, + populate_app_call_resources: bool = False, ) -> None: """ Configures various settings for the application. @@ -85,28 +140,26 @@ def configure( # noqa: PLR0913 If you are executing the config from an algokit compliant project, you can simply call `config.configure(debug=True)`. - Args: - debug (bool): Indicates whether debug mode is enabled. - project_root (Path | None, optional): The path to the project root directory. Defaults to None. - trace_all (bool, optional): Indicates whether to trace all operations. Defaults to False. Which implies that + :param debug: Indicates whether debug mode is enabled. + :param project_root: The path to the project root directory. Defaults to None. + :param trace_all: Indicates whether to trace all operations. Defaults to False. Which implies that only the operations that are failed will be traced by default. - trace_buffer_size_mb (float, optional): The size of the trace buffer in megabytes. Defaults to 512mb. - max_search_depth (int, optional): The maximum depth to search for a specific file. Defaults to 10. - - Returns: - None + :param trace_buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256 + :param max_search_depth: The maximum depth to search for a specific file. Defaults to 10 + :param populate_app_call_resources: Indicates whether to populate app call resources. Defaults to False """ - self._debug = debug - - if project_root: + if debug is not None: + self._debug = debug + if project_root is not None: self._project_root = project_root.resolve(strict=True) - elif debug and ALGOKIT_PROJECT_ROOT: + elif debug is not None and ALGOKIT_PROJECT_ROOT: self._project_root = Path(ALGOKIT_PROJECT_ROOT).resolve(strict=True) self._trace_all = trace_all self._trace_buffer_size_mb = trace_buffer_size_mb self._max_search_depth = max_search_depth + self._populate_app_call_resources = populate_app_call_resources config = UpdatableConfig() diff --git a/src/algokit_utils/deploy.py b/src/algokit_utils/deploy.py index bb01c4f2..9991b3ab 100644 --- a/src/algokit_utils/deploy.py +++ b/src/algokit_utils/deploy.py @@ -1,897 +1,10 @@ -import base64 -import dataclasses -import json -import logging -import re -from collections.abc import Iterable, Mapping, Sequence -from enum import Enum -from typing import TYPE_CHECKING, TypeAlias, TypedDict +import warnings -from algosdk import transaction -from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner -from algosdk.logic import get_application_address -from algosdk.transaction import StateSchema - -from algokit_utils.application_specification import ( - ApplicationSpecification, - CallConfig, - MethodConfigDict, - OnCompleteActionName, -) -from algokit_utils.models import ( - ABIArgsDict, - ABIMethod, - Account, - CreateCallParameters, - TransactionResponse, +warnings.warn( + "The legacy v2 deploy module is deprecated and will be removed in a future version. " + "Refer to `AppFactory` and `AppDeployer` abstractions from `algokit_utils` module instead.", + DeprecationWarning, + stacklevel=2, ) -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - from algosdk.v2client.indexer import IndexerClient - - from algokit_utils.application_client import ApplicationClient - - -__all__ = [ - "UPDATABLE_TEMPLATE_NAME", - "DELETABLE_TEMPLATE_NAME", - "NOTE_PREFIX", - "ABICallArgs", - "ABICreateCallArgs", - "ABICallArgsDict", - "ABICreateCallArgsDict", - "DeploymentFailedError", - "AppReference", - "AppDeployMetaData", - "AppMetaData", - "AppLookup", - "DeployCallArgs", - "DeployCreateCallArgs", - "DeployCallArgsDict", - "DeployCreateCallArgsDict", - "Deployer", - "DeployResponse", - "OnUpdate", - "OnSchemaBreak", - "OperationPerformed", - "TemplateValueDict", - "TemplateValueMapping", - "get_app_id_from_tx_id", - "get_creator_apps", - "replace_template_variables", -] - -logger = logging.getLogger(__name__) - -DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT = 1000 -_UPDATABLE = "UPDATABLE" -_DELETABLE = "DELETABLE" -UPDATABLE_TEMPLATE_NAME = f"TMPL_{_UPDATABLE}" -"""Template variable name used to control if a smart contract is updatable or not at deployment""" -DELETABLE_TEMPLATE_NAME = f"TMPL_{_DELETABLE}" -"""Template variable name used to control if a smart contract is deletable or not at deployment""" -_TOKEN_PATTERN = re.compile(r"TMPL_[A-Z_]+") -TemplateValue: TypeAlias = int | str | bytes -TemplateValueDict: TypeAlias = dict[str, TemplateValue] -"""Dictionary of `dict[str, int | str | bytes]` representing template variable names and values""" -TemplateValueMapping: TypeAlias = Mapping[str, TemplateValue] -"""Mapping of `str` to `int | str | bytes` representing template variable names and values""" - -NOTE_PREFIX = "ALGOKIT_DEPLOYER:j" -"""ARC-0002 compliant note prefix for algokit_utils deployed applications""" -# This prefix is also used to filter for parsable transaction notes in get_creator_apps. -# However, as the note is base64 encoded first we need to consider it's base64 representation. -# When base64 encoding bytes, 3 bytes are stored in every 4 characters. -# So then we don't need to worry about the padding/changing characters of the prefix if it was followed by -# additional characters, assert the NOTE_PREFIX length is a multiple of 3. -assert len(NOTE_PREFIX) % 3 == 0 - - -class DeploymentFailedError(Exception): - pass - - -@dataclasses.dataclass -class AppReference: - """Information about an Algorand app""" - - app_id: int - app_address: str - - -@dataclasses.dataclass -class AppDeployMetaData: - """Metadata about an application stored in a transaction note during creation. - - The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field - as part of {py:meth}`ApplicationClient.deploy` - """ - - name: str - version: str - deletable: bool | None - updatable: bool | None - - @staticmethod - def from_json(value: str) -> "AppDeployMetaData": - json_value: dict = json.loads(value) - json_value.setdefault("deletable", None) - json_value.setdefault("updatable", None) - return AppDeployMetaData(**json_value) - - @classmethod - def from_b64(cls: type["AppDeployMetaData"], b64: str) -> "AppDeployMetaData": - return cls.decode(base64.b64decode(b64)) - - @classmethod - def decode(cls: type["AppDeployMetaData"], value: bytes) -> "AppDeployMetaData": - note = value.decode("utf-8") - assert note.startswith(NOTE_PREFIX) - return cls.from_json(note[len(NOTE_PREFIX) :]) - - def encode(self) -> bytes: - json_str = json.dumps(self.__dict__) - return f"{NOTE_PREFIX}{json_str}".encode() - - -@dataclasses.dataclass -class AppMetaData(AppReference, AppDeployMetaData): - """Metadata about a deployed app""" - - created_round: int - updated_round: int - created_metadata: AppDeployMetaData - deleted: bool - - -@dataclasses.dataclass -class AppLookup: - """Cache of {py:class}`AppMetaData` for a specific `creator` - - Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple - apps or discovering multiple app_ids - """ - - creator: str - apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict) - - -def _sort_by_round(txn: dict) -> tuple[int, int]: - confirmed = txn["confirmed-round"] - offset = txn["intra-round-offset"] - return confirmed, offset - - -def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: - if not metadata_b64: - return None - # noinspection PyBroadException - try: - return AppDeployMetaData.from_b64(metadata_b64) - except Exception: - return None - - -def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: - """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified - creator that have a transaction note containing {py:class}`AppDeployMetaData` - """ - apps: dict[str, AppMetaData] = {} - - creator_address = creator_account if isinstance(creator_account, str) else creator_account.address - token = None - # TODO: paginated indexer call instead of N + 1 calls - while True: - response = indexer.lookup_account_application_by_creator( - creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token - ) # type: ignore[no-untyped-call] - if "message" in response: # an error occurred - raise Exception(f"Error querying applications for {creator_address}: {response}") - for app in response["applications"]: - app_id = app["id"] - app_created_at_round = app["created-at-round"] - app_deleted = app.get("deleted", False) - search_transactions_response = indexer.search_transactions( - min_round=app_created_at_round, - txn_type="appl", - application_id=app_id, - address=creator_address, - address_role="sender", - note_prefix=NOTE_PREFIX.encode("utf-8"), - ) # type: ignore[no-untyped-call] - transactions: list[dict] = search_transactions_response["transactions"] - if not transactions: - continue - - created_transaction = next( - t - for t in transactions - if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address - ) - - transactions.sort(key=_sort_by_round, reverse=True) - latest_transaction = transactions[0] - app_updated_at_round = latest_transaction["confirmed-round"] - - create_metadata = _parse_note(created_transaction.get("note")) - update_metadata = _parse_note(latest_transaction.get("note")) - - if create_metadata and create_metadata.name: - apps[create_metadata.name] = AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - created_metadata=create_metadata, - created_round=app_created_at_round, - **(update_metadata or create_metadata).__dict__, - updated_round=app_updated_at_round, - deleted=app_deleted, - ) - - token = response.get("next-token") - if not token: - break - - return AppLookup(creator_address, apps) - - -def _state_schema(schema: dict[str, int]) -> StateSchema: - return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] - - -def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: - if to_schema.num_uints > from_schema.num_uints: - yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}" - if to_schema.num_byte_slices > from_schema.num_byte_slices: - yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}" - - -@dataclasses.dataclass(kw_only=True) -class AppChanges: - app_updated: bool - schema_breaking_change: bool - schema_change_description: str | None - - -def check_for_app_changes( # noqa: PLR0913 - algod_client: "AlgodClient", - *, - new_approval: bytes, - new_clear: bytes, - new_global_schema: StateSchema, - new_local_schema: StateSchema, - app_id: int, -) -> AppChanges: - application_info = algod_client.application_info(app_id) - assert isinstance(application_info, dict) - application_create_params = application_info["params"] - - current_approval = base64.b64decode(application_create_params["approval-program"]) - current_clear = base64.b64decode(application_create_params["clear-state-program"]) - current_global_schema = _state_schema(application_create_params["global-state-schema"]) - current_local_schema = _state_schema(application_create_params["local-state-schema"]) - - app_updated = current_approval != new_approval or current_clear != new_clear - - schema_changes: list[str] = [] - schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema)) - schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema)) - - return AppChanges( - app_updated=app_updated, - schema_breaking_change=bool(schema_changes), - schema_change_description=", ".join(schema_changes), - ) - - -def _is_valid_token_character(char: str) -> bool: - return char.isalnum() or char == "_" - - -def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: - result: list[str] = [] - match_count = 0 - token = f"TMPL_{template_variable}" - token_idx_offset = len(value) - len(token) - for line in program_lines: - comment_idx = _find_unquoted_string(line, "//") - if comment_idx is None: - comment_idx = len(line) - code = line[:comment_idx] - comment = line[comment_idx:] - trailing_idx = 0 - while True: - token_idx = _find_template_token(code, token, trailing_idx) - if token_idx is None: - break - - trailing_idx = token_idx + len(token) - prefix = code[:token_idx] - suffix = code[trailing_idx:] - code = f"{prefix}{value}{suffix}" - match_count += 1 - trailing_idx += token_idx_offset - result.append(code + comment) - return result, match_count - - -def add_deploy_template_variables( - template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None -) -> None: - if allow_update is not None: - template_values[_UPDATABLE] = int(allow_update) - if allow_delete is not None: - template_values[_DELETABLE] = int(allow_delete) - - -def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned. - Returns None if not found""" - - if end < 0: - end = len(line) - idx = start - in_quotes = in_base64 = False - while idx < end: - current_char = line[idx] - match current_char: - # enter base64 - case " " | "(" if not in_quotes and _last_token_base64(line, idx): - in_base64 = True - # exit base64 - case " " | ")" if not in_quotes and in_base64: - in_base64 = False - # escaped char - case "\\" if in_quotes: - # skip next character - idx += 1 - # quote boundary - case '"': - in_quotes = not in_quotes - # can test for match - case _ if not in_quotes and not in_base64 and line.startswith(token, idx): - # only match if not in quotes and string matches - return idx - idx += 1 - return None - - -def _last_token_base64(line: str, idx: int) -> bool: - try: - *_, last = line[:idx].split() - except ValueError: - return False - return last in ("base64", "b64") - - -def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None: - """Find the first template token within a line of TEAL. Only matches outside of quotes are returned. - Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING - Returns None if not found""" - if end < 0: - end = len(line) - - idx = start - while idx < end: - token_idx = _find_unquoted_string(line, token, idx, end) - if token_idx is None: - break - trailing_idx = token_idx + len(token) - if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start - trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end - ): - return token_idx - idx = trailing_idx - return None - - -def _strip_comment(line: str) -> str: - comment_idx = _find_unquoted_string(line, "//") - if comment_idx is None: - return line - return line[:comment_idx].rstrip() - - -def strip_comments(program: str) -> str: - return "\n".join(_strip_comment(line) for line in program.splitlines()) - - -def _has_token(program_without_comments: str, token: str) -> bool: - for line in program_without_comments.splitlines(): - token_idx = _find_template_token(line, token) - if token_idx is not None: - return True - return False - - -def _find_tokens(stripped_approval_program: str) -> list[str]: - return _TOKEN_PATTERN.findall(stripped_approval_program) - - -def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: - approval_program = strip_comments(approval_program) - if _has_token(approval_program, UPDATABLE_TEMPLATE_NAME) and _UPDATABLE not in template_values: - raise DeploymentFailedError( - "allow_update must be specified if deploy time configuration of update is being used" - ) - if _has_token(approval_program, DELETABLE_TEMPLATE_NAME) and _DELETABLE not in template_values: - raise DeploymentFailedError( - "allow_delete must be specified if deploy time configuration of delete is being used" - ) - all_tokens = _find_tokens(approval_program) - missing_values = [token for token in all_tokens if token[len("TMPL_") :] not in template_values] - if missing_values: - raise DeploymentFailedError(f"The following template values were not provided: {', '.join(missing_values)}") - - for template_variable_name in template_values: - tmpl_variable = f"TMPL_{template_variable_name}" - if not _has_token(approval_program, tmpl_variable): - if template_variable_name == _UPDATABLE: - raise DeploymentFailedError( - "allow_update must only be specified if deploy time configuration of update is being used" - ) - if template_variable_name == _DELETABLE: - raise DeploymentFailedError( - "allow_delete must only be specified if deploy time configuration of delete is being used" - ) - logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") - - -def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: - """Replaces `TMPL_*` variables in `program` with `template_values` - - ```{note} - `template_values` keys should *NOT* be prefixed with `TMPL_` - ``` - """ - program_lines = program.splitlines() - for template_variable_name, template_value in template_values.items(): - match template_value: - case int(): - value = str(template_value) - case str(): - value = "0x" + template_value.encode("utf-8").hex() - case bytes(): - value = "0x" + template_value.hex() - case _: - raise DeploymentFailedError( - f"Unexpected template value type {template_variable_name}: {template_value.__class__}" - ) - - program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value) - - return "\n".join(program_lines) - - -def has_template_vars(app_spec: ApplicationSpecification) -> bool: - return "TMPL_" in strip_comments(app_spec.approval_program) or "TMPL_" in strip_comments(app_spec.clear_program) - - -def get_deploy_control( - app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete -) -> bool | None: - if template_var not in strip_comments(app_spec.approval_program): - return None - return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( - h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER - ) - - -def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: - def get(key: OnCompleteActionName) -> CallConfig: - return method_config.get(key, CallConfig.NEVER) - - match on_complete: - case transaction.OnComplete.NoOpOC: - return get("no_op") - case transaction.OnComplete.UpdateApplicationOC: - return get("update_application") - case transaction.OnComplete.DeleteApplicationOC: - return get("delete_application") - case transaction.OnComplete.OptInOC: - return get("opt_in") - case transaction.OnComplete.CloseOutOC: - return get("close_out") - case transaction.OnComplete.ClearStateOC: - return get("clear_state") - - -class OnUpdate(Enum): - """Action to take if an Application has been updated""" - - Fail = 0 - """Fail the deployment""" - UpdateApp = 1 - """Update the Application with the new approval and clear programs""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new application""" - - -class OnSchemaBreak(Enum): - """Action to take if an Application's schema has breaking changes""" - - Fail = 0 - """Fail the deployment""" - ReplaceApp = 2 - """Create a new Application and delete the old Application in a single transaction""" - AppendApp = 3 - """Create a new Application""" - - -class OperationPerformed(Enum): - """Describes the actions taken during deployment""" - - Nothing = 0 - """An existing Application was found""" - Create = 1 - """No existing Application was found, created a new Application""" - Update = 2 - """An existing Application was found, but was out of date, updated to latest version""" - Replace = 3 - """An existing Application was found, but was out of date, created a new Application and deleted the original""" - - -@dataclasses.dataclass(kw_only=True) -class DeployResponse: - """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`""" - - app: AppMetaData - create_response: TransactionResponse | None = None - delete_response: TransactionResponse | None = None - update_response: TransactionResponse | None = None - action_taken: OperationPerformed = OperationPerformed.Nothing - - -@dataclasses.dataclass(kw_only=True) -class DeployCallArgs: - """Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - suggested_params: transaction.SuggestedParams | None = None - lease: bytes | str | None = None - accounts: list[str] | None = None - foreign_apps: list[int] | None = None - foreign_assets: list[int] | None = None - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None - rekey_to: str | None = None - - -@dataclasses.dataclass(kw_only=True) -class ABICall: - method: ABIMethod | bool | None = None - args: ABIArgsDict = dataclasses.field(default_factory=dict) - - -@dataclasses.dataclass(kw_only=True) -class DeployCreateCallArgs(DeployCallArgs): - """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - extra_pages: int | None = None - on_complete: transaction.OnComplete | None = None - - -@dataclasses.dataclass(kw_only=True) -class ABICallArgs(DeployCallArgs, ABICall): - """ABI Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - -@dataclasses.dataclass(kw_only=True) -class ABICreateCallArgs(DeployCreateCallArgs, ABICall): - """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - -class DeployCallArgsDict(TypedDict, total=False): - """Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - suggested_params: transaction.SuggestedParams - lease: bytes | str - accounts: list[str] - foreign_apps: list[int] - foreign_assets: list[int] - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] - rekey_to: str - - -class ABICallArgsDict(DeployCallArgsDict, TypedDict, total=False): - """ABI Parameters used to update or delete an application when calling - {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - method: ABIMethod | bool - args: ABIArgsDict - - -class DeployCreateCallArgsDict(DeployCallArgsDict, TypedDict, total=False): - """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - extra_pages: int | None - on_complete: transaction.OnComplete - - -class ABICreateCallArgsDict(DeployCreateCallArgsDict, TypedDict, total=False): - """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`""" - - method: ABIMethod | bool - args: ABIArgsDict - - -@dataclasses.dataclass(kw_only=True) -class Deployer: - app_client: "ApplicationClient" - creator: str - signer: TransactionSigner - sender: str - existing_app_metadata_or_reference: AppReference | AppMetaData - new_app_metadata: AppDeployMetaData - on_update: OnUpdate - on_schema_break: OnSchemaBreak - create_args: ABICreateCallArgs | ABICreateCallArgsDict | DeployCreateCallArgs | None - update_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None - delete_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None - - def deploy(self) -> DeployResponse: - """Ensures app associated with app client's creator is present and up to date""" - assert self.app_client.approval - assert self.app_client.clear - - if self.existing_app_metadata_or_reference.app_id == 0: - logger.info(f"{self.new_app_metadata.name} not found in {self.creator} account, deploying app.") - return self._create_app() - - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - logger.debug( - f"{self.existing_app_metadata_or_reference.name} found in {self.creator} account, " - f"with app id {self.existing_app_metadata_or_reference.app_id}, " - f"version={self.existing_app_metadata_or_reference.version}." - ) - - app_changes = check_for_app_changes( - self.app_client.algod_client, - new_approval=self.app_client.approval.raw_binary, - new_clear=self.app_client.clear.raw_binary, - new_global_schema=self.app_client.app_spec.global_state_schema, - new_local_schema=self.app_client.app_spec.local_state_schema, - app_id=self.existing_app_metadata_or_reference.app_id, - ) - - if app_changes.schema_breaking_change: - logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") - return self._deploy_breaking_change() - - if app_changes.app_updated: - logger.info(f"Detected a TEAL update in app id {self.existing_app_metadata_or_reference.app_id}") - return self._deploy_update() - - logger.info("No detected changes in app, nothing to do.") - return DeployResponse(app=self.existing_app_metadata_or_reference) - - def _deploy_breaking_change(self) -> DeployResponse: - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - if self.on_schema_break == OnSchemaBreak.Fail: - raise DeploymentFailedError( - "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " - "If you want to try deleting and recreating the app then " - "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" - ) - if self.on_schema_break == OnSchemaBreak.AppendApp: - logger.info("Schema break detected and on_schema_break=AppendApp, will attempt to create new app") - return self._create_app() - - if self.existing_app_metadata_or_reference.deletable: - logger.info( - "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" - ) - elif self.existing_app_metadata_or_reference.deletable is False: - logger.warning( - "App is not deletable but on_schema_break=ReplaceApp, " - "will attempt to delete app, delete will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is deletable but on_schema_break=ReplaceApp, will attempt to delete app" - ) - return self._create_and_delete_app() - - def _deploy_update(self) -> DeployResponse: - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - if self.on_update == OnUpdate.Fail: - raise DeploymentFailedError( - "Update detected and on_update=Fail, stopping deployment. " - "If you want to try updating the app then re-run with on_update=UpdateApp" - ) - if self.on_update == OnUpdate.AppendApp: - logger.info("Update detected and on_update=AppendApp, will attempt to create new app") - return self._create_app() - elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.UpdateApp: - logger.info("App is updatable and on_update=UpdateApp, will update app") - return self._update_app() - elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.ReplaceApp: - logger.warning( - "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" - ) - return self._create_and_delete_app() - elif self.on_update == OnUpdate.ReplaceApp: - if self.existing_app_metadata_or_reference.updatable is False: - logger.warning( - "App is not updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - return self._create_and_delete_app() - else: - if self.existing_app_metadata_or_reference.updatable is False: - logger.warning( - "App is not updatable but on_update=UpdateApp, " - "will attempt to update app, update will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=UpdateApp, will attempt to update app" - ) - return self._update_app() - - def _create_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - - method, abi_args, parameters = _convert_deploy_args( - self.create_args, self.new_app_metadata, self.signer, self.sender - ) - create_response = self.app_client.create( - method, - parameters, - **abi_args, - ) - logger.info( - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " - f"with app id {self.app_client.app_id}." - ) - assert create_response.confirmed_round is not None - app_metadata = _create_metadata(self.new_app_metadata, self.app_client.app_id, create_response.confirmed_round) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - return DeployResponse(app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create) - - def _create_and_delete_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - - logger.info( - f"Replacing {self.existing_app_metadata_or_reference.name} " - f"({self.existing_app_metadata_or_reference.version}) with " - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) in {self.creator} account." - ) - atc = AtomicTransactionComposer() - create_method, create_abi_args, create_parameters = _convert_deploy_args( - self.create_args, self.new_app_metadata, self.signer, self.sender - ) - self.app_client.compose_create( - atc, - create_method, - create_parameters, - **create_abi_args, - ) - create_txn_index = len(atc.txn_list) - 1 - delete_method, delete_abi_args, delete_parameters = _convert_deploy_args( - self.delete_args, self.new_app_metadata, self.signer, self.sender - ) - self.app_client.compose_delete( - atc, - delete_method, - delete_parameters, - **delete_abi_args, - ) - delete_txn_index = len(atc.txn_list) - 1 - create_delete_response = self.app_client.execute_atc(atc) - create_response = TransactionResponse.from_atr(create_delete_response, create_txn_index) - delete_response = TransactionResponse.from_atr(create_delete_response, delete_txn_index) - self.app_client.app_id = get_app_id_from_tx_id(self.app_client.algod_client, create_response.tx_id) - logger.info( - f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, " - f"with app id {self.app_client.app_id}." - ) - logger.info( - f"{self.existing_app_metadata_or_reference.name} " - f"({self.existing_app_metadata_or_reference.version}) with app id " - f"{self.existing_app_metadata_or_reference.app_id}, deleted successfully." - ) - - app_metadata = _create_metadata( - self.new_app_metadata, self.app_client.app_id, create_delete_response.confirmed_round - ) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - - return DeployResponse( - app=app_metadata, - create_response=create_response, - delete_response=delete_response, - action_taken=OperationPerformed.Replace, - ) - - def _update_app(self) -> DeployResponse: - assert self.app_client.existing_deployments - assert isinstance(self.existing_app_metadata_or_reference, AppMetaData) - - logger.info( - f"Updating {self.existing_app_metadata_or_reference.name} to {self.new_app_metadata.version} in " - f"{self.creator} account, with app id {self.existing_app_metadata_or_reference.app_id}" - ) - method, abi_args, parameters = _convert_deploy_args( - self.update_args, self.new_app_metadata, self.signer, self.sender - ) - update_response = self.app_client.update( - method, - parameters, - **abi_args, - ) - app_metadata = _create_metadata( - self.new_app_metadata, - self.app_client.app_id, - self.existing_app_metadata_or_reference.created_round, - updated_round=update_response.confirmed_round, - original_metadata=self.existing_app_metadata_or_reference.created_metadata, - ) - self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata - return DeployResponse(app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update) - - -def _create_metadata( - app_spec_note: AppDeployMetaData, - app_id: int, - created_round: int, - updated_round: int | None = None, - original_metadata: AppDeployMetaData | None = None, -) -> AppMetaData: - return AppMetaData( - app_id=app_id, - app_address=get_application_address(app_id), - created_metadata=original_metadata or app_spec_note, - created_round=created_round, - updated_round=updated_round or created_round, - name=app_spec_note.name, - version=app_spec_note.version, - deletable=app_spec_note.deletable, - updatable=app_spec_note.updatable, - deleted=False, - ) - - -def _convert_deploy_args( - _args: DeployCallArgs | DeployCallArgsDict | None, - note: AppDeployMetaData, - signer: TransactionSigner | None, - sender: str | None, -) -> tuple[ABIMethod | bool | None, ABIArgsDict, CreateCallParameters]: - args = _args.__dict__ if isinstance(_args, DeployCallArgs) else dict(_args or {}) - - # return most derived type, unused parameters are ignored - parameters = CreateCallParameters( - note=note.encode(), - signer=signer, - sender=sender, - suggested_params=args.get("suggested_params"), - lease=args.get("lease"), - accounts=args.get("accounts"), - foreign_assets=args.get("foreign_assets"), - foreign_apps=args.get("foreign_apps"), - boxes=args.get("boxes"), - rekey_to=args.get("rekey_to"), - extra_pages=args.get("extra_pages"), - on_complete=args.get("on_complete"), - ) - - return args.get("method"), args.get("args") or {}, parameters - - -def get_app_id_from_tx_id(algod_client: "AlgodClient", tx_id: str) -> int: - """Finds the app_id for provided transaction id""" - result = algod_client.pending_transaction_info(tx_id) - assert isinstance(result, dict) - app_id = result["application-index"] - assert isinstance(app_id, int) - return app_id +from algokit_utils._legacy_v2.deploy import * # noqa: F403, E402 diff --git a/src/algokit_utils/dispenser_api.py b/src/algokit_utils/dispenser_api.py index 66593e80..a338badc 100644 --- a/src/algokit_utils/dispenser_api.py +++ b/src/algokit_utils/dispenser_api.py @@ -1,178 +1,10 @@ -import contextlib -import enum -import logging -import os -from dataclasses import dataclass +import warnings -import httpx +warnings.warn( + "The legacy v2 dispenser api module is deprecated and will be removed in a future version. " + "Import from 'algokit_utils.clients.dispenser_api_client' instead.", + DeprecationWarning, + stacklevel=2, +) -logger = logging.getLogger(__name__) - - -class DispenserApiConfig: - BASE_URL = "https://api.dispenser.algorandfoundation.tools" - - -class DispenserAssetName(enum.IntEnum): - ALGO = 0 - - -@dataclass -class DispenserAsset: - asset_id: int - decimals: int - description: str - - -@dataclass -class DispenserFundResponse: - tx_id: str - amount: int - - -@dataclass -class DispenserLimitResponse: - amount: int - - -DISPENSER_ASSETS = { - DispenserAssetName.ALGO: DispenserAsset( - asset_id=0, - decimals=6, - description="Algo", - ), -} -DISPENSER_REQUEST_TIMEOUT = 15 -DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN" - - -class TestNetDispenserApiClient: - """ - Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md). - To get started create a new access token via `algokit dispenser login --ci` - and pass it to the client constructor as `auth_token`. - Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`, - and it will be auto loaded. If both are set, the constructor argument takes precedence. - - Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor. - """ - - auth_token: str - request_timeout = DISPENSER_REQUEST_TIMEOUT - - def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT): - auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY) - - if auth_token: - self.auth_token = auth_token - elif auth_token_from_env: - self.auth_token = auth_token_from_env - else: - raise Exception( - f"Can't init AlgoKit TestNet Dispenser API client " - f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or " - "the auth_token were provided." - ) - - self.request_timeout = request_timeout - - def _process_dispenser_request( - self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST" - ) -> httpx.Response: - """ - Generalized method to process http requests to dispenser API - """ - - headers = {"Authorization": f"Bearer {(auth_token)}"} - - # Set request arguments - request_args = { - "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}", - "headers": headers, - "timeout": self.request_timeout, - } - - if method.upper() != "GET" and data is not None: - request_args["json"] = data - - try: - response: httpx.Response = getattr(httpx, method.lower())(**request_args) - response.raise_for_status() - return response - - except httpx.HTTPStatusError as err: - error_message = f"Error processing dispenser API request: {err.response.status_code}" - error_response = None - with contextlib.suppress(Exception): - error_response = err.response.json() - - if error_response and error_response.get("code"): - error_message = error_response.get("code") - - elif err.response.status_code == httpx.codes.BAD_REQUEST: - error_message = err.response.json()["message"] - - raise Exception(error_message) from err - - except Exception as err: - error_message = "Error processing dispenser API request" - logger.debug(f"{error_message}: {err}", exc_info=True) - raise err - - def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse: - """ - Fund an account with Algos from the dispenser API - """ - - try: - response = self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix=f"fund/{asset_id}", - data={"receiver": address, "amount": amount, "assetID": asset_id}, - method="POST", - ) - - content = response.json() - return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"]) - - except Exception as err: - logger.exception(f"Error funding account {address}: {err}") - raise err - - def refund(self, refund_txn_id: str) -> None: - """ - Register a refund for a transaction with the dispenser API - """ - - try: - self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix="refund", - data={"refundTransactionID": refund_txn_id}, - method="POST", - ) - - except Exception as err: - logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}") - raise err - - def get_limit( - self, - address: str, - ) -> DispenserLimitResponse: - """ - Get current limit for an account with Algos from the dispenser API - """ - - try: - response = self._process_dispenser_request( - auth_token=self.auth_token, - url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit", - method="GET", - ) - content = response.json() - - return DispenserLimitResponse(amount=content["amount"]) - except Exception as err: - logger.exception(f"Error setting limit for account {address}: {err}") - raise err +from algokit_utils.clients.dispenser_api_client import * # noqa: F403, E402 diff --git a/src/algokit_utils/errors/__init__.py b/src/algokit_utils/errors/__init__.py new file mode 100644 index 00000000..1575d19e --- /dev/null +++ b/src/algokit_utils/errors/__init__.py @@ -0,0 +1 @@ +from algokit_utils.errors.logic_error import * # noqa: F403 diff --git a/src/algokit_utils/errors/logic_error.py b/src/algokit_utils/errors/logic_error.py new file mode 100644 index 00000000..755b89e4 --- /dev/null +++ b/src/algokit_utils/errors/logic_error.py @@ -0,0 +1,121 @@ +import base64 +import re +from collections.abc import Callable +from copy import copy +from typing import TYPE_CHECKING, TypedDict + +from algosdk.atomic_transaction_composer import ( + SimulateAtomicTransactionResponse, +) + +from algokit_utils.models.simulate import SimulationTrace + +if TYPE_CHECKING: + from algosdk.source_map import SourceMap as AlgoSourceMap +__all__ = [ + "LogicError", + "LogicErrorData", + "parse_logic_error", +] + + +LOGIC_ERROR = ( + ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" +) + + +class LogicErrorData(TypedDict): + transaction_id: str + message: str + pc: int + + +def parse_logic_error( + error_str: str, +) -> LogicErrorData | None: + match = re.match(LOGIC_ERROR, error_str) + if match is None: + return None + + return { + "transaction_id": match.group("transaction_id"), + "message": match.group("message"), + "pc": int(match.group("pc")), + } + + +class LogicError(Exception): + def __init__( + 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/logic_error.py b/src/algokit_utils/logic_error.py index 56d22f9f..462895f7 100644 --- a/src/algokit_utils/logic_error.py +++ b/src/algokit_utils/logic_error.py @@ -1,85 +1,10 @@ -import re -from copy import copy -from typing import TYPE_CHECKING, TypedDict +import warnings -from algokit_utils.models import SimulationTrace - -if TYPE_CHECKING: - from algosdk.source_map import SourceMap as AlgoSourceMap - -__all__ = [ - "LogicError", - "parse_logic_error", -] - -LOGIC_ERROR = ( - ".*transaction (?P[A-Z0-9]+): logic eval error: (?P.*). Details: .*pc=(?P[0-9]+).*" +warnings.warn( + "The legacy v2 logic error module is deprecated and will be removed in a future version. " + "Use 'from algokit_utils.errors import LogicError' instead.", + DeprecationWarning, + stacklevel=2, ) - -class LogicErrorData(TypedDict): - transaction_id: str - message: str - pc: int - - -def parse_logic_error( - error_str: str, -) -> LogicErrorData | None: - match = re.match(LOGIC_ERROR, error_str) - if match is None: - return None - - return { - "transaction_id": match.group("transaction_id"), - "message": match.group("message"), - "pc": int(match.group("pc")), - } - - -class LogicError(Exception): - def __init__( # noqa: PLR0913 - self, - *, - logic_error_str: str, - program: str, - source_map: "AlgoSourceMap | None", - transaction_id: str, - message: str, - pc: int, - logic_error: Exception | None = None, - traces: list[SimulationTrace] | None = None, - ): - self.logic_error = logic_error - self.logic_error_str = logic_error_str - self.program = program - self.source_map = source_map - self.lines = program.split("\n") - self.transaction_id = transaction_id - self.message = message - self.pc = pc - self.traces = traces - - self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None - - def __str__(self) -> str: - return ( - f"Txn {self.transaction_id} had error '{self.message}' at PC {self.pc}" - + (":" if self.line_no is None else f" and Source Line {self.line_no}:") - + f"\n{self.trace()}" - ) - - def trace(self, lines: int = 5) -> str: - if self.line_no is None: - return """ -Could not determine TEAL source line for the error as no approval source map was provided, to receive a trace of the -error please provide an approval SourceMap. Either by: - 1.) Providing template_values when creating the ApplicationClient, so a SourceMap can be obtained automatically OR - 2.) Set approval_source_map from a previously compiled approval program OR - 3.) Import a previously exported source map using import_source_map""" - - program_lines = copy(self.lines) - program_lines[self.line_no] += "\t\t<-- Error" - lines_before = max(0, self.line_no - lines) - lines_after = min(len(program_lines), self.line_no + lines) - return "\n\t" + "\n\t".join(program_lines[lines_before:lines_after]) +from algokit_utils.errors.logic_error import * # noqa: F403, E402 diff --git a/src/algokit_utils/models/__init__.py b/src/algokit_utils/models/__init__.py new file mode 100644 index 00000000..d4790dc4 --- /dev/null +++ b/src/algokit_utils/models/__init__.py @@ -0,0 +1,8 @@ +from algokit_utils._legacy_v2.models import * # noqa: F403 +from algokit_utils.models.account import * # noqa: F403 +from algokit_utils.models.amount import * # noqa: F403 +from algokit_utils.models.application import * # noqa: F403 +from algokit_utils.models.network import * # noqa: F403 +from algokit_utils.models.simulate import * # noqa: F403 +from algokit_utils.models.state import * # noqa: F403 +from algokit_utils.models.transaction import * # noqa: F403 diff --git a/src/algokit_utils/models/account.py b/src/algokit_utils/models/account.py new file mode 100644 index 00000000..daa88cb6 --- /dev/null +++ b/src/algokit_utils/models/account.py @@ -0,0 +1,193 @@ +import dataclasses + +import algosdk +import algosdk.atomic_transaction_composer +from algosdk.atomic_transaction_composer import AccountTransactionSigner, LogicSigTransactionSigner, TransactionSigner +from algosdk.transaction import LogicSigAccount as AlgosdkLogicSigAccount +from algosdk.transaction import Multisig, MultisigTransaction + +__all__ = [ + "DISPENSER_ACCOUNT_NAME", + "MultiSigAccount", + "MultisigMetadata", + "SigningAccount", + "TransactionSignerAccount", +] + + +DISPENSER_ACCOUNT_NAME = "DISPENSER" + + +@dataclasses.dataclass(kw_only=True) +class TransactionSignerAccount: + """A basic transaction signer account.""" + + address: str + signer: TransactionSigner + + def __post_init__(self) -> None: + if not isinstance(self.address, str): + raise TypeError("Address must be a string") + if not isinstance(self.signer, TransactionSigner): + raise TypeError("Signer must be a TransactionSigner instance") + + +@dataclasses.dataclass(kw_only=True) +class SigningAccount: + """Holds the private key and address for an account. + + Provides access to the account's private key, address, public key and transaction signer. + """ + + private_key: str + """Base64 encoded private key""" + address: str = dataclasses.field(default="") + """Address for this account""" + + def __post_init__(self) -> None: + if not self.address: + self.address = str(algosdk.account.address_from_private_key(self.private_key)) + + @property + def public_key(self) -> bytes: + """The public key for this account. + + :return: The public key as bytes + """ + public_key = algosdk.encoding.decode_address(self.address) + assert isinstance(public_key, bytes) + return public_key + + @property + def signer(self) -> AccountTransactionSigner: + """Get an AccountTransactionSigner for this account. + + :return: A transaction signer for this account + """ + return AccountTransactionSigner(self.private_key) + + @staticmethod + def new_account() -> "SigningAccount": + """Create a new random account. + + :return: A new Account instance + """ + private_key, address = algosdk.account.generate_account() + return SigningAccount(private_key=private_key) + + +@dataclasses.dataclass(kw_only=True) +class MultisigMetadata: + """Metadata for a multisig account. + + Contains the version, threshold and addresses for a multisig account. + """ + + version: int + threshold: int + addresses: list[str] + + +@dataclasses.dataclass(kw_only=True) +class MultiSigAccount: + """Account wrapper that supports partial or full multisig signing. + + Provides functionality to manage and sign transactions for a multisig account. + + :param multisig_params: The parameters for the multisig account + :param signing_accounts: The list of accounts that can sign + """ + + _params: MultisigMetadata + _signing_accounts: list[SigningAccount] + _addr: str + _signer: TransactionSigner + _multisig: Multisig + + def __init__(self, multisig_params: MultisigMetadata, signing_accounts: list[SigningAccount]) -> None: + self._params = multisig_params + self._signing_accounts = signing_accounts + self._multisig = Multisig(multisig_params.version, multisig_params.threshold, multisig_params.addresses) + self._addr = str(self._multisig.address()) + self._signer = algosdk.atomic_transaction_composer.MultisigTransactionSigner( + self._multisig, + [account.private_key for account in signing_accounts], + ) + + @property + def params(self) -> MultisigMetadata: + """Get the parameters for the multisig account. + + :return: The multisig account parameters + """ + return self._params + + @property + def signing_accounts(self) -> list[SigningAccount]: + """Get the list of accounts that are present to sign. + + :return: The list of signing accounts + """ + return self._signing_accounts + + @property + def address(self) -> str: + """Get the address of the multisig account. + + :return: The multisig account address + """ + return self._addr + + @property + def signer(self) -> TransactionSigner: + """Get the transaction signer for this multisig account. + + :return: The multisig transaction signer + """ + return self._signer + + def sign(self, transaction: algosdk.transaction.Transaction) -> MultisigTransaction: + """Sign the given transaction with all present signers. + + :param transaction: Either a transaction object or a raw, partially signed transaction + :return: The transaction signed by the present signers + """ + msig_txn = MultisigTransaction( + transaction, + self._multisig, + ) + for signer in self._signing_accounts: + msig_txn.sign(signer.private_key) + + return msig_txn + + +@dataclasses.dataclass(kw_only=True) +class LogicSigAccount: + """Account wrapper that supports logic sig signing. + + Provides functionality to manage and sign transactions for a logic sig account. + """ + + _account: AlgosdkLogicSigAccount + _signer: LogicSigTransactionSigner + + def __init__(self, account: AlgosdkLogicSigAccount) -> None: + self._account = account + self._signer = LogicSigTransactionSigner(account) + + @property + def address(self) -> str: + """Get the address of the multisig account. + + :return: The multisig account address + """ + return self._account.address() + + @property + def signer(self) -> LogicSigTransactionSigner: + """Get the transaction signer for this multisig account. + + :return: The multisig transaction signer + """ + return self._signer diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py new file mode 100644 index 00000000..0baa0e03 --- /dev/null +++ b/src/algokit_utils/models/amount.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from decimal import Decimal + +import algosdk +from typing_extensions import Self + +__all__ = ["AlgoAmount"] + + +class AlgoAmount: + """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. + + :param amount: A dictionary containing either algos, algo, microAlgos, or microAlgo as key + and their corresponding value as an integer or Decimal. + :raises ValueError: If an invalid amount format is provided. + + :example: + >>> amount = AlgoAmount({"algos": 1}) + >>> amount = AlgoAmount({"microAlgos": 1_000_000}) + """ + + def __init__(self, amount: dict[str, int | Decimal]): + if "microAlgos" in amount: + self.amount_in_micro_algo = int(amount["microAlgos"]) + elif "microAlgo" in amount: + self.amount_in_micro_algo = int(amount["microAlgo"]) + elif "algos" in amount: + self.amount_in_micro_algo = int(amount["algos"] * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) + elif "algo" in amount: + self.amount_in_micro_algo = int(amount["algo"] * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) + else: + raise ValueError("Invalid amount provided") + + @property + def micro_algos(self) -> int: + """Return the amount as a number in µAlgo. + + :returns: The amount in µAlgo. + """ + return self.amount_in_micro_algo + + @property + def micro_algo(self) -> int: + """Return the amount as a number in µAlgo. + + :returns: The amount in µAlgo. + """ + return self.amount_in_micro_algo + + @property + def algos(self) -> Decimal: + """Return the amount as a number in Algo. + + :returns: The amount in Algo. + """ + return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] + + @property + def algo(self) -> Decimal: + """Return the amount as a number in Algo. + + :returns: The amount in Algo. + """ + return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] + + @staticmethod + def from_algos(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of Algo. + + :param amount: The amount in Algo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_algos(1) + """ + return AlgoAmount({"algos": amount}) + + @staticmethod + def from_algo(amount: int | Decimal) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of Algo. + + :param amount: The amount in Algo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_algo(1) + """ + return AlgoAmount({"algo": amount}) + + @staticmethod + def from_micro_algos(amount: int) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of µAlgo. + + :param amount: The amount in µAlgo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_micro_algos(1_000_000) + """ + return AlgoAmount({"microAlgos": amount}) + + @staticmethod + def from_micro_algo(amount: int) -> AlgoAmount: + """Create an AlgoAmount object representing the given number of µAlgo. + + :param amount: The amount in µAlgo. + :returns: An AlgoAmount instance. + + :example: + >>> amount = AlgoAmount.from_micro_algo(1_000_000) + """ + return AlgoAmount({"microAlgo": amount}) + + def __str__(self) -> str: + return f"{self.micro_algo:,} µALGO" + + def __int__(self) -> int: + return self.micro_algos + + def __add__(self, other: AlgoAmount) -> AlgoAmount: + if isinstance(other, AlgoAmount): + total_micro_algos = self.micro_algos + other.micro_algos + else: + raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") + return AlgoAmount.from_micro_algos(total_micro_algos) + + def __radd__(self, other: AlgoAmount) -> AlgoAmount: + return self.__add__(other) + + def __iadd__(self, other: AlgoAmount) -> Self: + if isinstance(other, AlgoAmount): + self.amount_in_micro_algo += other.micro_algos + else: + raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") + return self + + def __eq__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo == other.amount_in_micro_algo + elif isinstance(other, int): + return self.amount_in_micro_algo == int(other) + raise TypeError(f"Unsupported operand type(s) for ==: 'AlgoAmount' and '{type(other).__name__}'") + + def __ne__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo != other.amount_in_micro_algo + elif isinstance(other, int): + return self.amount_in_micro_algo != int(other) + raise TypeError(f"Unsupported operand type(s) for !=: 'AlgoAmount' and '{type(other).__name__}'") + + def __lt__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo < other.amount_in_micro_algo + elif isinstance(other, int): + return self.amount_in_micro_algo < int(other) + raise TypeError(f"Unsupported operand type(s) for <: 'AlgoAmount' and '{type(other).__name__}'") + + def __le__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo <= other.amount_in_micro_algo + elif isinstance(other, int): + return self.amount_in_micro_algo <= int(other) + raise TypeError(f"Unsupported operand type(s) for <=: 'AlgoAmount' and '{type(other).__name__}'") + + def __gt__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo > other.amount_in_micro_algo + elif isinstance(other, int): + return self.amount_in_micro_algo > int(other) + raise TypeError(f"Unsupported operand type(s) for >: 'AlgoAmount' and '{type(other).__name__}'") + + def __ge__(self, other: object) -> bool: + if isinstance(other, AlgoAmount): + return self.amount_in_micro_algo >= other.amount_in_micro_algo + elif isinstance(other, int): + 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: AlgoAmount) -> AlgoAmount: + if isinstance(other, AlgoAmount): + total_micro_algos = self.micro_algos - other.micro_algos + 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) -> AlgoAmount: + if isinstance(other, (int)): + total_micro_algos = int(other) - self.micro_algos + return AlgoAmount.from_micro_algos(total_micro_algos) + raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'") + + def __isub__(self, other: AlgoAmount) -> Self: + if isinstance(other, AlgoAmount): + self.amount_in_micro_algo -= other.micro_algos + 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 new file mode 100644 index 00000000..46d2ddba --- /dev/null +++ b/src/algokit_utils/models/application.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import algosdk +from algosdk.source_map import SourceMap + +if TYPE_CHECKING: + pass + +__all__ = [ + "AppCompilationResult", + "AppInformation", + "AppSourceMaps", + "AppState", + "CompiledTeal", +] + + +@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: str + 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 + + +@dataclass(kw_only=True, frozen=True) +class AppSourceMaps: + approval_source_map: SourceMap | None = None + clear_source_map: SourceMap | None = None diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py new file mode 100644 index 00000000..6b4b6226 --- /dev/null +++ b/src/algokit_utils/models/network.py @@ -0,0 +1,25 @@ +import dataclasses + +__all__ = [ + "AlgoClientConfigs", + "AlgoClientNetworkConfig", +] + + +@dataclasses.dataclass +class AlgoClientNetworkConfig: + """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or + {py:class}`algosdk.v2client.indexer.IndexerClient`""" + + server: str + """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + token: str | None = None + """API Token to authenticate with the service""" + port: str | int | None = None + + +@dataclasses.dataclass +class AlgoClientConfigs: + algod_config: AlgoClientNetworkConfig + indexer_config: AlgoClientNetworkConfig | None + kmd_config: AlgoClientNetworkConfig | None diff --git a/src/algokit_utils/models/simulate.py b/src/algokit_utils/models/simulate.py new file mode 100644 index 00000000..bd200495 --- /dev/null +++ b/src/algokit_utils/models/simulate.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +__all__ = ["SimulationTrace"] + + +@dataclass +class SimulationTrace: + app_budget_added: int | None + app_budget_consumed: int | None + failure_message: str | None + exec_trace: dict[str, object] diff --git a/src/algokit_utils/models/state.py b/src/algokit_utils/models/state.py new file mode 100644 index 00000000..f5d7804e --- /dev/null +++ b/src/algokit_utils/models/state.py @@ -0,0 +1,59 @@ +import base64 +from collections.abc import Mapping +from dataclasses import dataclass +from enum import IntEnum +from typing import TypeAlias + +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.box_reference import BoxReference as AlgosdkBoxReference + +__all__ = [ + "BoxIdentifier", + "BoxName", + "BoxReference", + "BoxValue", + "DataTypeFlag", + "TealTemplateParams", +] + + +@dataclass(kw_only=True, frozen=True) +class BoxName: + name: str + name_raw: bytes + name_base64: str + + +@dataclass(kw_only=True, frozen=True) +class BoxValue: + name: BoxName + value: bytes + + +class DataTypeFlag(IntEnum): + BYTES = 1 + UINT = 2 + + +TealTemplateParams: TypeAlias = Mapping[str, str | int | bytes] | dict[str, str | int | bytes] + + +BoxIdentifier: TypeAlias = str | bytes | AccountTransactionSigner + + +class BoxReference(AlgosdkBoxReference): + def __init__(self, app_id: int, name: bytes | str): + super().__init__(app_index=app_id, name=self._b64_decode(name)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, (BoxReference | AlgosdkBoxReference)): + return self.app_index == other.app_index and self.name == other.name + return False + + def _b64_decode(self, value: str | bytes) -> bytes: + if isinstance(value, str): + try: + return base64.b64decode(value) + except Exception: + return value.encode("utf-8") + return value diff --git a/src/algokit_utils/models/transaction.py b/src/algokit_utils/models/transaction.py new file mode 100644 index 00000000..e0c07cda --- /dev/null +++ b/src/algokit_utils/models/transaction.py @@ -0,0 +1,100 @@ +from typing import Any, Literal, TypedDict, TypeVar + +import algosdk + +__all__ = [ + "Arc2TransactionNote", + "BaseArc2Note", + "JsonFormatArc2Note", + "SendParams", + "StringFormatArc2Note", + "TransactionNote", + "TransactionNoteData", + "TransactionWrapper", +] + + +# Define specific types for different formats +class BaseArc2Note(TypedDict): + """Base ARC-0002 transaction note structure""" + + dapp_name: str + + +class StringFormatArc2Note(BaseArc2Note): + """ARC-0002 note for string-based formats (m/b/u)""" + + format: Literal["m", "b", "u"] + data: str + + +class JsonFormatArc2Note(BaseArc2Note): + """ARC-0002 note for JSON format""" + + format: Literal["j"] + data: str | dict[str, Any] | list[Any] | int | None + + +# Combined type for all valid ARC-0002 notes +# See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md +Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note + +TransactionNoteData = str | None | int | list[Any] | dict[str, Any] +TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote + +TxnTypeT = TypeVar("TxnTypeT", bound=algosdk.transaction.Transaction) + + +class TransactionWrapper(algosdk.transaction.Transaction): + """Wrapper around algosdk.transaction.Transaction with optional property validators""" + + def __init__(self, transaction: algosdk.transaction.Transaction) -> None: + self._raw = transaction + + @property + def raw(self) -> algosdk.transaction.Transaction: + return self._raw + + @property + def payment(self) -> algosdk.transaction.PaymentTxn: + return self._return_if_type( + algosdk.transaction.PaymentTxn, + ) + + @property + def keyreg(self) -> algosdk.transaction.KeyregTxn: + return self._return_if_type(algosdk.transaction.KeyregTxn) + + @property + def asset_config(self) -> algosdk.transaction.AssetConfigTxn: + return self._return_if_type(algosdk.transaction.AssetConfigTxn) + + @property + def asset_transfer(self) -> algosdk.transaction.AssetTransferTxn: + return self._return_if_type(algosdk.transaction.AssetTransferTxn) + + @property + def asset_freeze(self) -> algosdk.transaction.AssetFreezeTxn: + return self._return_if_type(algosdk.transaction.AssetFreezeTxn) + + @property + def application_call(self) -> algosdk.transaction.ApplicationCallTxn: + return self._return_if_type(algosdk.transaction.ApplicationCallTxn) + + @property + def state_proof(self) -> algosdk.transaction.StateProofTxn: + return self._return_if_type(algosdk.transaction.StateProofTxn) + + def _return_if_type(self, txn_type: type[TxnTypeT]) -> TxnTypeT: + if isinstance(self._raw, txn_type): + return self._raw + raise ValueError(f"Transaction is not of type {txn_type.__name__}") + + +class SendParams(TypedDict, total=False): + """Parameters for sending a transaction""" + + max_rounds_to_wait: int | None + suppress_log: bool | None + populate_app_call_resources: bool | None + cover_app_call_inner_transaction_fees: bool | None diff --git a/src/algokit_utils/network_clients.py b/src/algokit_utils/network_clients.py index 2de270da..798100de 100644 --- a/src/algokit_utils/network_clients.py +++ b/src/algokit_utils/network_clients.py @@ -1,130 +1,9 @@ -import dataclasses -import os -from typing import Literal -from urllib import parse +import warnings -from algosdk.kmd import KMDClient -from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.indexer import IndexerClient +warnings.warn( + "The legacy v2 network clients module is deprecated and will be removed in a future version.", + DeprecationWarning, + stacklevel=2, +) -__all__ = [ - "AlgoClientConfig", - "get_algod_client", - "get_algonode_config", - "get_default_localnet_config", - "get_indexer_client", - "get_kmd_client_from_algod_client", - "is_localnet", - "is_mainnet", - "is_testnet", - "AlgoClientConfigs", - "get_kmd_client", -] - - -@dataclasses.dataclass -class AlgoClientConfig: - """Connection details for connecting to an {py:class}`algosdk.v2client.algod.AlgodClient` or - {py:class}`algosdk.v2client.indexer.IndexerClient`""" - - server: str - """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" - token: str - """API Token to authenticate with the service""" - - -@dataclasses.dataclass -class AlgoClientConfigs: - algod_config: AlgoClientConfig - indexer_config: AlgoClientConfig - kmd_config: AlgoClientConfig | None - - -def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: - """Returns the client configuration to point to the default LocalNet""" - port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] - return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) - - -def get_algonode_config( - network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str -) -> AlgoClientConfig: - client = "api" if config == "algod" else "idx" - return AlgoClientConfig( - server=f"https://{network}-{client}.algonode.cloud", - token=token, - ) - - -def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: - """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment - - If no configuration provided will use environment variables `ALGOD_SERVER`, `ALGOD_PORT` and `ALGOD_TOKEN`""" - config = config or _get_config_from_environment("ALGOD") - headers = {"X-Algo-API-Token": config.token} - return AlgodClient(config.token, config.server, headers) - - -def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: - """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment - - If no configuration provided will use environment variables `KMD_SERVER`, `KMD_PORT` and `KMD_TOKEN`""" - config = config or _get_config_from_environment("KMD") - return KMDClient(config.token, config.server) # type: ignore[no-untyped-call] - - -def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: - """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. - - If no configuration provided will use environment variables `INDEXER_SERVER`, `INDEXER_PORT` and `INDEXER_TOKEN`""" - config = config or _get_config_from_environment("INDEXER") - headers = {"X-Indexer-API-Token": config.token} - return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call] - - -def is_localnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" - params = client.suggested_params() - return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] - - -def is_mainnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `mainnet-v1`""" - params = client.suggested_params() - return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] - - -def is_testnet(client: AlgodClient) -> bool: - """Returns True if client genesis is `testnet-v1`""" - params = client.suggested_params() - return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] - - -def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: - """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` - - Will use the same address as provided `client` but on port specified by `KMD_PORT` environment variable, - or 4002 by default""" - # We can only use Kmd on the LocalNet otherwise it's not exposed so this makes some assumptions - # (e.g. same token and server as algod and port 4002 by default) - port = os.getenv("KMD_PORT", "4002") - server = _replace_kmd_port(client.algod_address, port) - return KMDClient(client.algod_token, server) # type: ignore[no-untyped-call] - - -def _replace_kmd_port(address: str, port: str) -> str: - parsed_algod = parse.urlparse(address) - kmd_host = parsed_algod.netloc.split(":", maxsplit=1)[0] + f":{port}" - kmd_parsed = parsed_algod._replace(netloc=kmd_host) - return parse.urlunparse(kmd_parsed) - - -def _get_config_from_environment(environment_prefix: str) -> AlgoClientConfig: - server = os.getenv(f"{environment_prefix}_SERVER") - if server is None: - raise Exception(f"Server environment variable not set: {environment_prefix}_SERVER") - port = os.getenv(f"{environment_prefix}_PORT") - if port: - parsed = parse.urlparse(server) - server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() - return AlgoClientConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) +from algokit_utils._legacy_v2.network_clients import * # noqa: F403, E402 diff --git a/src/algokit_utils/protocols/__init__.py b/src/algokit_utils/protocols/__init__.py new file mode 100644 index 00000000..d77d8625 --- /dev/null +++ b/src/algokit_utils/protocols/__init__.py @@ -0,0 +1,2 @@ +from algokit_utils.protocols.account import * # noqa: F403 +from algokit_utils.protocols.typed_clients import * # noqa: F403 diff --git a/src/algokit_utils/protocols/account.py b/src/algokit_utils/protocols/account.py new file mode 100644 index 00000000..b50c94a3 --- /dev/null +++ b/src/algokit_utils/protocols/account.py @@ -0,0 +1,22 @@ +from typing import Protocol, runtime_checkable + +from algosdk.atomic_transaction_composer import TransactionSigner + +__all__ = ["TransactionSignerAccountProtocol"] + + +@runtime_checkable +class TransactionSignerAccountProtocol(Protocol): + """An account that has a transaction signer. + Implemented by SigningAccount, LogicSigAccount, MultiSigAccount and TransactionSignerAccount abstractions. + """ + + @property + def address(self) -> str: + """The address of the account.""" + ... + + @property + def signer(self) -> TransactionSigner: + """The transaction signer for the account.""" + ... diff --git a/src/algokit_utils/protocols/typed_clients.py b/src/algokit_utils/protocols/typed_clients.py new file mode 100644 index 00000000..70eee8a9 --- /dev/null +++ b/src/algokit_utils/protocols/typed_clients.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar + +from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.source_map import SourceMap +from typing_extensions import Self + +from algokit_utils.models import SendParams + +if TYPE_CHECKING: + from algokit_utils.algorand import AlgorandClient + from algokit_utils.applications.app_client import ( + AppClientBareCallCreateParams, + AppClientBareCallParams, + AppClientCompilationParams, + BaseAppClientMethodCallParams, + ) + from algokit_utils.applications.app_deployer import ( + ApplicationLookup, + OnSchemaBreak, + OnUpdate, + ) + from algokit_utils.applications.app_factory import AppFactoryDeployResult + +__all__ = [ + "TypedAppClientProtocol", + "TypedAppFactoryProtocol", +] + + +class TypedAppClientProtocol(Protocol): + @classmethod + def from_creator_and_name( + cls, + *, + creator_address: str, + app_name: str, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + ignore_cache: bool | None = None, + app_lookup_cache: ApplicationLookup | None = None, + algorand: AlgorandClient, + ) -> Self: ... + + @classmethod + def from_network( + cls, + *, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + algorand: AlgorandClient, + ) -> Self: ... + + def __init__( + self, + *, + app_id: int, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + algorand: AlgorandClient, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> None: ... + + +CreateParamsT = TypeVar( # noqa: PLC0105 + "CreateParamsT", + bound="BaseAppClientMethodCallParams | AppClientBareCallCreateParams | None", + contravariant=True, +) +UpdateParamsT = TypeVar( # noqa: PLC0105 + "UpdateParamsT", + bound="BaseAppClientMethodCallParams | AppClientBareCallParams | None", + contravariant=True, +) +DeleteParamsT = TypeVar( # noqa: PLC0105 + "DeleteParamsT", + bound="BaseAppClientMethodCallParams | AppClientBareCallParams | None", + contravariant=True, +) + + +class TypedAppFactoryProtocol(Protocol, Generic[CreateParamsT, UpdateParamsT, DeleteParamsT]): + def __init__( + self, + algorand: AlgorandClient, + **kwargs: Any, + ) -> None: ... + + def deploy( + self, + *, + on_update: OnUpdate | None = None, + on_schema_break: OnSchemaBreak | None = None, + create_params: CreateParamsT | None = None, + update_params: UpdateParamsT | None = None, + delete_params: DeleteParamsT | None = None, + existing_deployments: ApplicationLookup | None = None, + ignore_cache: bool = False, + app_name: str | None = None, + send_params: SendParams | None = None, + compilation_params: AppClientCompilationParams | None = None, + ) -> tuple[TypedAppClientProtocol, AppFactoryDeployResult]: ... diff --git a/src/algokit_utils/transactions/__init__.py b/src/algokit_utils/transactions/__init__.py new file mode 100644 index 00000000..4b59b06e --- /dev/null +++ b/src/algokit_utils/transactions/__init__.py @@ -0,0 +1,3 @@ +from algokit_utils.transactions.transaction_composer import * # noqa: F403 +from algokit_utils.transactions.transaction_creator import * # noqa: F403 +from algokit_utils.transactions.transaction_sender import * # noqa: F403 diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py new file mode 100644 index 00000000..1fcf4125 --- /dev/null +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -0,0 +1,2293 @@ +from __future__ import annotations + +import base64 +import json +import math +import re +from copy import deepcopy +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, TypedDict, Union, cast + +import algosdk +import algosdk.atomic_transaction_composer +import algosdk.v2client.models +from algosdk import logic, transaction +from algosdk.atomic_transaction_composer import ( + AtomicTransactionComposer, + SimulateAtomicTransactionResponse, + TransactionSigner, + TransactionWithSigner, +) +from algosdk.transaction import ApplicationCallTxn, OnComplete, SuggestedParams +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.models.simulate_request import SimulateRequest +from typing_extensions import deprecated + +from algokit_utils.applications.abi import ABIReturn, ABIValue +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method +from algokit_utils.config import config +from algokit_utils.models.state import BoxIdentifier, BoxReference +from algokit_utils.models.transaction import SendParams, TransactionWrapper +from algokit_utils.protocols.account import TransactionSignerAccountProtocol + +if TYPE_CHECKING: + from collections.abc import Callable + + from algosdk.abi import Method + from algosdk.v2client.algod import AlgodClient + from algosdk.v2client.models import SimulateTraceConfig + + from algokit_utils.models.amount import AlgoAmount + from algokit_utils.models.transaction import Arc2TransactionNote + + +__all__ = [ + "AppCallMethodCallParams", + "AppCallParams", + "AppCreateMethodCallParams", + "AppCreateParams", + "AppCreateSchema", + "AppDeleteMethodCallParams", + "AppDeleteParams", + "AppMethodCallTransactionArgument", + "AppUpdateMethodCallParams", + "AppUpdateParams", + "AssetConfigParams", + "AssetCreateParams", + "AssetDestroyParams", + "AssetFreezeParams", + "AssetOptInParams", + "AssetOptOutParams", + "AssetTransferParams", + "BuiltTransactions", + "MethodCallParams", + "OfflineKeyRegistrationParams", + "OnlineKeyRegistrationParams", + "PaymentParams", + "SendAtomicTransactionComposerResults", + "TransactionComposer", + "TransactionComposerBuildResult", + "TxnParams", + "send_atomic_transaction_composer", +] + + +logger = config.logger + +MAX_TRANSACTION_GROUP_SIZE = 16 +MAX_APP_CALL_FOREIGN_REFERENCES = 8 +MAX_APP_CALL_ACCOUNT_REFERENCES = 4 + + +@dataclass(kw_only=True, frozen=True) +class _CommonTxnParams: + sender: str + signer: TransactionSigner | TransactionSignerAccountProtocol | 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, frozen=True) +class AdditionalAtcContext: + max_fees: dict[int, AlgoAmount] | None = None + suggested_params: SuggestedParams | None = None + + +@dataclass(kw_only=True, frozen=True) +class PaymentParams(_CommonTxnParams): + """Parameters for a payment transaction. + + :ivar receiver: The account that will receive the ALGO + :ivar amount: Amount to send + :ivar close_remainder_to: If given, close the sender account and send the remaining balance to this address, + defaults to None + """ + + receiver: str + amount: AlgoAmount + close_remainder_to: str | None = None + + +@dataclass(kw_only=True, frozen=True) +class AssetCreateParams(_CommonTxnParams): + """Parameters for creating a new asset. + + :ivar total: The total amount of the smallest divisible unit to create + :ivar decimals: The amount of decimal places the asset should have, defaults to None + :ivar default_frozen: Whether the asset is frozen by default in the creator address, defaults to None + :ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + :ivar reserve: The address that holds the uncirculated supply, defaults to None + :ivar freeze: The address that can freeze the asset in any account, defaults to None + :ivar clawback: The address that can clawback the asset from any account, defaults to None + :ivar unit_name: The short ticker name for the asset, defaults to None + :ivar asset_name: The full name of the asset, defaults to None + :ivar url: The metadata URL for the asset, defaults to None + :ivar metadata_hash: Hash of the metadata contained in the metadata URL, defaults to None + """ + + total: int + asset_name: str | None = None + unit_name: str | None = None + url: str | None = None + decimals: int | None = None + default_frozen: bool | None = None + manager: str | None = None + reserve: str | None = None + freeze: str | None = None + clawback: str | None = None + metadata_hash: bytes | None = None + + +@dataclass(kw_only=True, frozen=True) +class AssetConfigParams(_CommonTxnParams): + """Parameters for configuring an existing asset. + + :ivar asset_id: ID of the asset + :ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None + :ivar reserve: The address that holds the uncirculated supply, defaults to None + :ivar freeze: The address that can freeze the asset in any account, defaults to None + :ivar clawback: The address that can clawback the asset from any account, defaults to None + """ + + asset_id: int + manager: str | None = None + reserve: str | None = None + freeze: str | None = None + clawback: str | None = None + + +@dataclass(kw_only=True, frozen=True) +class AssetFreezeParams(_CommonTxnParams): + """Parameters for freezing an asset. + + :ivar asset_id: The ID of the asset + :ivar account: The account to freeze or unfreeze + :ivar frozen: Whether the assets in the account should be frozen + """ + + asset_id: int + account: str + frozen: bool + + +@dataclass(kw_only=True, frozen=True) +class AssetDestroyParams(_CommonTxnParams): + """Parameters for destroying an asset. + + :ivar asset_id: ID of the asset + """ + + asset_id: int + + +@dataclass(kw_only=True, frozen=True) +class OnlineKeyRegistrationParams(_CommonTxnParams): + """Parameters for online key registration. + + :ivar vote_key: The root participation public key + :ivar selection_key: The VRF public key + :ivar vote_first: The first round that the participation key is valid + :ivar vote_last: The last round that the participation key is valid + :ivar vote_key_dilution: The dilution for the 2-level participation key + :ivar state_proof_key: The 64 byte state proof public key commitment, defaults to None + """ + + vote_key: str + selection_key: str + vote_first: int + vote_last: int + vote_key_dilution: int + state_proof_key: bytes | None = None + + +@dataclass(kw_only=True, frozen=True) +class OfflineKeyRegistrationParams(_CommonTxnParams): + """Parameters for offline key registration. + + :ivar prevent_account_from_ever_participating_again: Whether to prevent the account from ever participating again + """ + + prevent_account_from_ever_participating_again: bool + + +@dataclass(kw_only=True, frozen=True) +class AssetTransferParams(_CommonTxnParams): + """Parameters for transferring an asset. + + :ivar asset_id: ID of the asset + :ivar amount: Amount of the asset to transfer (smallest divisible unit) + :ivar receiver: The account to send the asset to + :ivar clawback_target: The account to take the asset from, defaults to None + :ivar close_asset_to: The account to close the asset to, defaults to None + """ + + asset_id: int + amount: int + receiver: str + clawback_target: str | None = None + close_asset_to: str | None = None + + +@dataclass(kw_only=True, frozen=True) +class AssetOptInParams(_CommonTxnParams): + """Parameters for opting into an asset. + + :ivar asset_id: ID of the asset + """ + + asset_id: int + + +@dataclass(kw_only=True, frozen=True) +class AssetOptOutParams(_CommonTxnParams): + """Parameters for opting out of an asset. + + :ivar asset_id: ID of the asset + :ivar creator: The creator address of the asset + """ + + asset_id: int + creator: str + + +@dataclass(kw_only=True, frozen=True) +class AppCallParams(_CommonTxnParams): + """Parameters for calling an application. + + :ivar on_complete: The OnComplete action + :ivar app_id: ID of the application, defaults to None + :ivar approval_program: The program to execute for all OnCompletes other than ClearState, defaults to None + :ivar clear_state_program: The program to execute for ClearState OnComplete, defaults to None + :ivar schema: The state schema for the app. This is immutable, defaults to None + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar extra_pages: Number of extra pages required for the programs, defaults to None + :ivar box_references: Box references, defaults to None + """ + + on_complete: OnComplete + app_id: int | None = None + approval_program: str | bytes | None = None + clear_state_program: str | bytes | None = None + schema: dict[str, int] | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + extra_pages: int | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + + +class AppCreateSchema(TypedDict): + global_ints: int + global_byte_slices: int + local_ints: int + local_byte_slices: int + + +@dataclass(kw_only=True, frozen=True) +class AppCreateParams(_CommonTxnParams): + """Parameters for creating an application. + + :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) + or compiled teal (bytes) + :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + :ivar schema: The state schema for the app. This is immutable, defaults to None + :ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar extra_program_pages: Number of extra pages required for the programs, defaults to None + """ + + approval_program: str | bytes + clear_state_program: str | bytes + schema: AppCreateSchema | None = None + on_complete: OnComplete | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppUpdateParams(_CommonTxnParams): + """Parameters for updating an application. + + :ivar app_id: ID of the application + :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) + or compiled teal (bytes) + :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar on_complete: The OnComplete action, defaults to None + """ + + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + on_complete: OnComplete | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppDeleteParams(_CommonTxnParams): + """Parameters for deleting an application. + + :ivar app_id: ID of the application + :ivar args: Application arguments, defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + :ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC + """ + + app_id: int + 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 + on_complete: OnComplete = OnComplete.DeleteApplicationOC + + +@dataclass(kw_only=True, frozen=True) +class _BaseAppMethodCall(_CommonTxnParams): + app_id: int + method: Method + args: list | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + schema: AppCreateSchema | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppMethodCallParams(_CommonTxnParams): + """Parameters for calling an application method. + + :ivar app_id: ID of the application + :ivar method: The ABI method to call + :ivar args: Arguments to the ABI method, defaults to None + :ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + :ivar account_references: Account references, defaults to None + :ivar app_references: App references, defaults to None + :ivar asset_references: Asset references, defaults to None + :ivar box_references: Box references, defaults to None + """ + + app_id: int + method: Method + args: list[bytes] | None = None + on_complete: OnComplete | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppCallMethodCallParams(_BaseAppMethodCall): + """Parameters for a regular ABI method call. + + :ivar app_id: ID of the application + :ivar method: The ABI method to call + :ivar args: Arguments to the ABI method, either an ABI value, transaction with explicit signer, + transaction, another method call, or None + :ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + """ + + app_id: int + on_complete: OnComplete | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppCreateMethodCallParams(_BaseAppMethodCall): + """Parameters for an ABI method call that creates an application. + + :ivar approval_program: The program to execute for all OnCompletes other than ClearState + :ivar clear_state_program: The program to execute for ClearState OnComplete + :ivar schema: The state schema for the app, defaults to None + :ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None + :ivar extra_program_pages: Number of extra pages required for the programs, defaults to None + """ + + approval_program: str | bytes + clear_state_program: str | bytes + schema: AppCreateSchema | None = None + on_complete: OnComplete | None = None + extra_program_pages: int | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppUpdateMethodCallParams(_BaseAppMethodCall): + """Parameters for an ABI method call that updates an application. + + :ivar app_id: ID of the application + :ivar approval_program: The program to execute for all OnCompletes other than ClearState + :ivar clear_state_program: The program to execute for ClearState OnComplete + :ivar on_complete: The OnComplete action, defaults to UpdateApplicationOC + """ + + app_id: int + approval_program: str | bytes + clear_state_program: str | bytes + on_complete: OnComplete = OnComplete.UpdateApplicationOC + + +@dataclass(kw_only=True, frozen=True) +class AppDeleteMethodCallParams(_BaseAppMethodCall): + """Parameters for an ABI method call that deletes an application. + + :ivar app_id: ID of the application + :ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC + """ + + app_id: int + on_complete: OnComplete = OnComplete.DeleteApplicationOC + + +MethodCallParams = ( + AppCallMethodCallParams | AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams +) + + +AppMethodCallTransactionArgument = ( + TransactionWithSigner + | algosdk.transaction.Transaction + | AppCreateMethodCallParams + | AppUpdateMethodCallParams + | AppCallMethodCallParams +) + + +TxnParams = Union[ # noqa: UP007 + PaymentParams, + AssetCreateParams, + AssetConfigParams, + AssetFreezeParams, + AssetDestroyParams, + OnlineKeyRegistrationParams, + AssetTransferParams, + AssetOptInParams, + AssetOptOutParams, + AppCallParams, + AppCreateParams, + AppUpdateParams, + AppDeleteParams, + MethodCallParams, + OfflineKeyRegistrationParams, +] + + +@dataclass(frozen=True, kw_only=True) +class TransactionContext: + """Contextual information for a transaction.""" + + max_fee: AlgoAmount | None = None + abi_method: Method | None = None + + @staticmethod + def empty() -> TransactionContext: + return TransactionContext(max_fee=None, abi_method=None) + + +class TransactionWithContext: + """Combines Transaction with additional context.""" + + def __init__(self, txn: algosdk.transaction.Transaction, context: TransactionContext): + self.txn = txn + self.context = context + + +class TransactionWithSignerAndContext(TransactionWithSigner): + """Combines TransactionWithSigner with additional context.""" + + def __init__(self, txn: algosdk.transaction.Transaction, signer: TransactionSigner, context: TransactionContext): + super().__init__(txn, signer) + self.context = context + + @staticmethod + def from_txn_with_context( + txn_with_context: TransactionWithContext, signer: TransactionSigner + ) -> TransactionWithSignerAndContext: + return TransactionWithSignerAndContext( + txn=txn_with_context.txn, signer=signer, context=txn_with_context.context + ) + + +@dataclass(frozen=True) +class BuiltTransactions: + """Set of transactions built by TransactionComposer. + + :ivar transactions: The built transactions + :ivar method_calls: Any ABIMethod objects associated with any of the transactions in a map keyed by txn id + :ivar signers: Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id + """ + + transactions: list[algosdk.transaction.Transaction] + method_calls: dict[int, Method] + signers: dict[int, TransactionSigner] + + +@dataclass +class TransactionComposerBuildResult: + """Result of building transactions with TransactionComposer. + + :ivar atc: The AtomicTransactionComposer instance + :ivar transactions: The list of transactions with signers + :ivar method_calls: Map of transaction index to ABI method + """ + + atc: AtomicTransactionComposer + transactions: list[TransactionWithSigner] + method_calls: dict[int, Method] + + +@dataclass +class SendAtomicTransactionComposerResults: + """Results from sending an AtomicTransactionComposer transaction group. + + :ivar group_id: The group ID if this was a transaction group + :ivar confirmations: The confirmation info for each transaction + :ivar tx_ids: The transaction IDs that were sent + :ivar transactions: The transactions that were sent + :ivar returns: The ABI return values from any ABI method calls + :ivar simulate_response: The simulation response if simulation was performed, defaults to None + """ + + group_id: str + confirmations: list[algosdk.v2client.algod.AlgodResponseType] + tx_ids: list[str] + transactions: list[TransactionWrapper] + returns: list[ABIReturn] + simulate_response: dict[str, Any] | None = None + + +@dataclass +class ExecutionInfoTxn: + unnamed_resources_accessed: dict | None = None + required_fee_delta: int = 0 + + +@dataclass +class ExecutionInfo: + """Information about transaction execution from simulation.""" + + group_unnamed_resources_accessed: dict[str, Any] | None = None + txns: list[ExecutionInfoTxn] | None = None + + +@dataclass +class _TransactionWithPriority: + txn: algosdk.transaction.Transaction + priority: int + fee_delta: int + index: int + + +MAX_LEASE_LENGTH = 32 +NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() + + +def _encode_lease(lease: str | bytes | None) -> bytes | None: + if lease is None: + return None + elif isinstance(lease, bytes): + if not (1 <= len(lease) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received bytes with length {len(lease)}" + ) + if len(lease) == MAX_LEASE_LENGTH: + return lease + lease32 = bytearray(32) + lease32[: len(lease)] = lease + return bytes(lease32) + elif isinstance(lease, str): + encoded = lease.encode("utf-8") + if not (1 <= len(encoded) <= MAX_LEASE_LENGTH): + raise ValueError( + f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, " + f"but received '{lease}' with length {len(lease)}" + ) + lease32 = bytearray(MAX_LEASE_LENGTH) + lease32[: len(encoded)] = encoded + return bytes(lease32) + else: + raise TypeError(f"Unknown lease type received of {type(lease)}") + + +def _get_group_execution_info( # noqa: C901, PLR0912 + atc: AtomicTransactionComposer, + algod: AlgodClient, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, + additional_atc_context: AdditionalAtcContext | None = None, +) -> ExecutionInfo: + # Create simulation request + suggested_params = additional_atc_context.suggested_params if additional_atc_context else None + max_fees = additional_atc_context.max_fees if additional_atc_context else None + + simulate_request = SimulateRequest( + txn_groups=[], + allow_unnamed_resources=True, + allow_empty_signatures=True, + ) + + # Clone ATC with null signers + empty_signer_atc = atc.clone() + + # Track app call indexes without max fees + app_call_indexes_without_max_fees = [] + + # Copy transactions with null signers + for i, txn in enumerate(empty_signer_atc.txn_list): + txn_with_signer = TransactionWithSigner(txn=txn.txn, signer=NULL_SIGNER) + + if cover_app_call_inner_transaction_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn): + if not suggested_params: + raise ValueError("suggested_params required when cover_app_call_inner_transaction_fees enabled") + + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + if max_fee is None: + app_call_indexes_without_max_fees.append(i) + else: + txn_with_signer.txn.fee = max_fee + + if cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees: + raise ValueError( + f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_transaction_fees` is enabled. " # noqa: E501 + f"Required for transactions: {', '.join(str(i) for i in app_call_indexes_without_max_fees)}" + ) + + # Get fee parameters + per_byte_txn_fee = suggested_params.fee if suggested_params else 0 + min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 # type: ignore[unused-ignore] + + # Simulate transactions + result = empty_signer_atc.simulate(algod, simulate_request) + + group_response = result.simulate_response["txn-groups"][0] + + if group_response.get("failure-message"): + msg = group_response["failure-message"] + if cover_app_call_inner_transaction_fees and "fee too small" in msg: + raise ValueError( + "Fees were too small to resolve execution info via simulate. " + "You may need to increase an app call transaction maxFee." + ) + failed_at = group_response.get("failed-at", [0])[0] + raise ValueError( + f"Error during resource population simulation in transaction {failed_at}: " + f"{group_response['failure-message']}" + ) + + # Build execution info + txn_results = [] + for i, txn_result_raw in enumerate(group_response["txn-results"]): + txn_result = txn_result_raw.get("txn-result") + if not txn_result: + continue + + original_txn = atc.build_group()[i].txn + + required_fee_delta = 0 + if cover_app_call_inner_transaction_fees: + # Calculate parent transaction fee + parent_per_byte_fee = per_byte_txn_fee * (original_txn.estimate_size() + 75) + parent_min_fee = max(parent_per_byte_fee, min_txn_fee) + parent_fee_delta = parent_min_fee - original_txn.fee + + if isinstance(original_txn, algosdk.transaction.ApplicationCallTxn): + # Calculate inner transaction fees recursively + def calculate_inner_fee_delta(inner_txns: list[dict], acc: int = 0) -> int: + for inner_txn in reversed(inner_txns): + current_fee_delta = ( + calculate_inner_fee_delta(inner_txn["inner-txns"], acc) + if inner_txn.get("inner-txns") + else acc + ) + (min_txn_fee - inner_txn["txn"]["txn"].get("fee", 0)) + acc = max(0, current_fee_delta) + return acc + + inner_fee_delta = calculate_inner_fee_delta(txn_result.get("inner-txns", [])) + required_fee_delta = inner_fee_delta + parent_fee_delta + else: + required_fee_delta = parent_fee_delta + + txn_results.append( + ExecutionInfoTxn( + unnamed_resources_accessed=txn_result_raw.get("unnamed-resources-accessed") + if populate_app_call_resources + else None, + required_fee_delta=required_fee_delta, + ) + ) + + return ExecutionInfo( + group_unnamed_resources_accessed=group_response.get("unnamed-resources-accessed") + if populate_app_call_resources + else None, + txns=txn_results, + ) + + +def _find_available_transaction_index( + txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int +) -> int: + """Find index of first transaction that can accommodate the new reference.""" + + def check_transaction(txn: TransactionWithSigner) -> bool: + # Skip if not an application call transaction + if txn.txn.type != "appl": + return False + + # Get current counts (using get() with default 0 for Pythonic null handling) + accounts = len(getattr(txn.txn, "accounts", []) or []) + assets = len(getattr(txn.txn, "foreign_assets", []) or []) + apps = len(getattr(txn.txn, "foreign_apps", []) or []) + boxes = len(getattr(txn.txn, "boxes", []) or []) + + # For account references, only check account limit + if reference_type == "account": + return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + + # For asset holdings or local state, need space for both account and other reference + if reference_type in ("asset_holding", "app_local"): + return ( + accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES + ) + + # For boxes with non-zero app ID, need space for box and app reference + if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0: + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1 + + # Default case - just check total references + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Return first matching index or -1 if none found + return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1) + + +def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: + """Populate application call resources based on simulation results. + + :param atc: The AtomicTransactionComposer containing transactions + :param algod: Algod client for simulation + :return: Modified AtomicTransactionComposer with populated resources + """ + return prepare_group_for_sending(atc, algod, populate_app_call_resources=True) + + +def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 + atc: AtomicTransactionComposer, + algod: AlgodClient, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, + additional_atc_context: AdditionalAtcContext | None = None, +) -> AtomicTransactionComposer: + """Prepare a transaction group for sending by handling execution info and resources. + + :param atc: The AtomicTransactionComposer containing transactions + :param algod: Algod client for simulation + :param populate_app_call_resources: Whether to populate app call resources + :param cover_app_call_inner_transaction_fees: Whether to cover inner txn fees + :param additional_atc_context: Additional context for the AtomicTransactionComposer + :return: Modified AtomicTransactionComposer ready for sending + """ + # Get execution info via simulation + execution_info = _get_group_execution_info( + atc, algod, populate_app_call_resources, cover_app_call_inner_transaction_fees, additional_atc_context + ) + max_fees = additional_atc_context.max_fees if additional_atc_context else None + + group = atc.build_group() + + # Handle transaction fees if needed + if cover_app_call_inner_transaction_fees: + # Sort transactions by fee priority + txns_with_priority: list[_TransactionWithPriority] = [] + for i, txn_info in enumerate(execution_info.txns or []): + if not txn_info: + continue + txn = group[i].txn + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + immutable_fee = max_fee is not None and max_fee == txn.fee + priority_multiplier = ( + 1000 + if ( + txn_info.required_fee_delta > 0 + and (immutable_fee or not isinstance(txn, algosdk.transaction.ApplicationCallTxn)) + ) + else 1 + ) + + txns_with_priority.append( + _TransactionWithPriority( + txn=txn, + index=i, + fee_delta=txn_info.required_fee_delta, + priority=txn_info.required_fee_delta * priority_multiplier + if txn_info.required_fee_delta > 0 + else -1, + ) + ) + + # Sort by priority descending + txns_with_priority.sort(key=lambda x: x.priority, reverse=True) + + # Calculate surplus fees and additional fees needed + surplus_fees = sum( + txn_info.required_fee_delta * -1 + for txn_info in execution_info.txns or [] + if txn_info is not None and txn_info.required_fee_delta < 0 + ) + + additional_fees = {} + + # Distribute surplus fees to cover deficits + for txn_obj in txns_with_priority: + if txn_obj.fee_delta > 0: + if surplus_fees >= txn_obj.fee_delta: + surplus_fees -= txn_obj.fee_delta + else: + additional_fees[txn_obj.index] = txn_obj.fee_delta - surplus_fees + surplus_fees = 0 + + def populate_group_resource( # noqa: PLR0915, PLR0912, C901 + txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str + ) -> None: + """Helper function to populate group-level resources.""" + + def is_appl_below_limit(t: TransactionWithSigner) -> bool: + if not isinstance(t.txn, transaction.ApplicationCallTxn): + return False + + accounts = len(getattr(t.txn, "accounts", []) or []) + assets = len(getattr(t.txn, "foreign_assets", []) or []) + apps = len(getattr(t.txn, "foreign_apps", []) or []) + boxes = len(getattr(t.txn, "boxes", []) or []) + + return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES + + # Handle asset holding and app local references first + if ref_type in ("assetHolding", "appLocal"): + ref_dict = cast(dict[str, Any], reference) + account = ref_dict["account"] + + # First try to find transaction with account already available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and ( + account in (getattr(t.txn, "accounts", []) or []) + or account + in ( + logic.get_application_address(app_id) + for app_id in (getattr(t.txn, "foreign_apps", []) or []) + ) + or any(str(account) in str(v) for v in t.txn.__dict__.values()) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + if ref_type == "assetHolding": + asset_id = ref_dict["asset"] + app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id] + else: + app_id = ref_dict["app"] + app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id] + return + + # Try to find transaction that already has the app/asset available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES + and ( + ( + ref_type == "assetHolding" + and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or []) + ) + or ( + ref_type == "appLocal" + and ( + ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or []) + or t.txn.index == ref_dict["app"] + ) + ) + ) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(account) + app_txn.accounts = accounts + return + + # Handle box references + if ref_type == "box": + box_ref = (reference["app"], base64.b64decode(reference["name"])) # type: ignore[index] + + # Try to find transaction that already has the app available + txn_idx = next( + ( + i + for i, t in enumerate(txns) + if is_appl_below_limit(t) + and isinstance(t.txn, transaction.ApplicationCallTxn) + and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0]) + ), + -1, + ) + + if txn_idx >= 0: + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type] + app_txn.boxes = boxes + return + + # Find available transaction for the resource + txn_idx = _find_available_transaction_index(txns, ref_type, reference) + + if txn_idx == -1: + raise ValueError("No more transactions below reference limit. Add another app call to the group.") + + app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn) + + if ref_type == "account": + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(cast(str, reference)) + app_txn.accounts = accounts + elif ref_type == "app": + app_id = int(cast(str | int, reference)) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(app_id) + app_txn.foreign_apps = foreign_apps + elif ref_type == "box": + boxes = list(getattr(app_txn, "boxes", []) or []) + boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type] + app_txn.boxes = boxes + if box_ref[0] != 0: + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(box_ref[0]) + app_txn.foreign_apps = foreign_apps + elif ref_type == "asset": + asset_id = int(cast(str | int, reference)) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(asset_id) + app_txn.foreign_assets = foreign_assets + elif ref_type == "assetHolding": + ref_dict = cast(dict[str, Any], reference) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + foreign_assets.append(ref_dict["asset"]) + app_txn.foreign_assets = foreign_assets + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + elif ref_type == "appLocal": + ref_dict = cast(dict[str, Any], reference) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_apps.append(ref_dict["app"]) + app_txn.foreign_apps = foreign_apps + accounts = list(getattr(app_txn, "accounts", []) or []) + accounts.append(ref_dict["account"]) + app_txn.accounts = accounts + + # Process transaction-level resources + for i, txn_info in enumerate(execution_info.txns or []): + if not txn_info: + continue + + # Validate no unexpected resources + is_app_txn = isinstance(group[i].txn, algosdk.transaction.ApplicationCallTxn) + resources = txn_info.unnamed_resources_accessed + if resources and is_app_txn: + app_txn = group[i].txn + if resources.get("boxes") or resources.get("extra-box-refs"): + raise ValueError("Unexpected boxes at transaction level") + if resources.get("appLocals"): + raise ValueError("Unexpected app local at transaction level") + if resources.get("assetHoldings"): + raise ValueError("Unexpected asset holding at transaction level") + + # Update application call fields + accounts = list(getattr(app_txn, "accounts", []) or []) + foreign_apps = list(getattr(app_txn, "foreign_apps", []) or []) + foreign_assets = list(getattr(app_txn, "foreign_assets", []) or []) + boxes = list(getattr(app_txn, "boxes", []) or []) + + # Add new resources + accounts.extend(resources.get("accounts", [])) + foreign_apps.extend(resources.get("apps", [])) + foreign_assets.extend(resources.get("assets", [])) + boxes.extend(resources.get("boxes", [])) + + # Validate limits + if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES: + raise ValueError( + f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}" + ) + + total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes) + if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES: + raise ValueError( + f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}" + ) + + # Update transaction + app_txn.accounts = accounts # type: ignore[attr-defined] + app_txn.foreign_apps = foreign_apps # type: ignore[attr-defined] + app_txn.foreign_assets = foreign_assets # type: ignore[attr-defined] + app_txn.boxes = boxes # type: ignore[attr-defined] + + # Update fees if needed + if cover_app_call_inner_transaction_fees and i in additional_fees: + cur_txn = group[i].txn + additional_fee = additional_fees[i] + if not isinstance(cur_txn, algosdk.transaction.ApplicationCallTxn): + raise ValueError( + f"An additional fee of {additional_fee} µALGO is required for non app call transaction {i}" + ) + + transaction_fee = cur_txn.fee + additional_fee + max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + + if max_fee is None or transaction_fee > max_fee: + raise ValueError( + f"Calculated transaction fee {transaction_fee} µALGO is greater " + f"than max of {max_fee or 'undefined'} " + f"for transaction {i}" + ) + cur_txn.fee = transaction_fee + + # Process group-level resources + group_resources = execution_info.group_unnamed_resources_accessed + if group_resources: + # Handle cross-reference resources first + for app_local in group_resources.get("appLocals", []): + populate_group_resource(group, app_local, "appLocal") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != app_local["account"] + ] + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])] + + for asset_holding in group_resources.get("assetHoldings", []): + populate_group_resource(group, asset_holding, "assetHolding") + # Remove processed resources + if "accounts" in group_resources: + group_resources["accounts"] = [ + acc for acc in group_resources["accounts"] if acc != asset_holding["account"] + ] + if "assets" in group_resources: + group_resources["assets"] = [ + asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"]) + ] + + # Handle remaining resources + for account in group_resources.get("accounts", []): + populate_group_resource(group, account, "account") + + for box in group_resources.get("boxes", []): + populate_group_resource(group, box, "box") + if "apps" in group_resources: + group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box["app"])] + + for asset in group_resources.get("assets", []): + populate_group_resource(group, asset, "asset") + + for app in group_resources.get("apps", []): + populate_group_resource(group, app, "app") + + # Handle extra box references + extra_box_refs = group_resources.get("extra-box-refs", 0) + for _ in range(extra_box_refs): + populate_group_resource(group, {"app": 0, "name": ""}, "box") + + # Create new ATC with updated transactions + new_atc = AtomicTransactionComposer() + for txn_with_signer in group: + txn_with_signer.txn.group = None + new_atc.add_transaction(txn_with_signer) + new_atc.method_dict = deepcopy(atc.method_dict) + + return new_atc + + +def send_atomic_transaction_composer( # noqa: C901, PLR0912 + atc: AtomicTransactionComposer, + algod: AlgodClient, + *, + max_rounds_to_wait: int | None = 5, + skip_waiting: bool = False, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + cover_app_call_inner_transaction_fees: bool | None = None, + additional_atc_context: AdditionalAtcContext | None = None, +) -> SendAtomicTransactionComposerResults: + """Send an AtomicTransactionComposer transaction group. + + Executes a group of transactions atomically using the AtomicTransactionComposer. + + :param atc: The AtomicTransactionComposer instance containing the transaction group to send + :param algod: The Algod client to use for sending the transactions + :param max_rounds_to_wait: Maximum number of rounds to wait for confirmation, defaults to 5 + :param skip_waiting: If True, don't wait for transaction confirmation, defaults to False + :param suppress_log: If True, suppress logging, defaults to None + :param populate_app_call_resources: If True, populate app call resources, defaults to None + :param cover_app_call_inner_transaction_fees: If True, cover app call inner transaction fees, defaults to None + :param additional_atc_context: Additional context for the AtomicTransactionComposer + :return: Results from sending the transaction group + :raises Exception: If there is an error sending the transactions + :raises error: If there is an error from the Algorand node + """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response + + try: + # Build transactions + transactions_with_signer = atc.build_group() + + populate_app_call_resources = ( + populate_app_call_resources + if populate_app_call_resources is not None + else config.populate_app_call_resource + ) + + if (populate_app_call_resources or cover_app_call_inner_transaction_fees) and any( + isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer + ): + atc = prepare_group_for_sending( + atc, + algod, + populate_app_call_resources, + cover_app_call_inner_transaction_fees, + additional_atc_context, + ) + + transactions_to_send = [t.txn for t in transactions_with_signer] + + # Get group ID if multiple transactions + group_id = None + if len(transactions_to_send) > 1: + group_id = ( + base64.b64encode(transactions_to_send[0].group).decode("utf-8") if transactions_to_send[0].group else "" + ) + + if not suppress_log: + logger.info( + f"Sending group of {len(transactions_to_send)} transactions ({group_id})", + suppress_log=suppress_log or False, + ) + logger.debug( + f"Transaction IDs ({group_id}): {[t.get_txid() for t in transactions_to_send]}", + suppress_log=suppress_log or False, + ) + + # Simulate if debug enabled + if config.debug and config.trace_all and config.project_root: + simulate_and_persist_response( + atc, + config.project_root, + algod, + config.trace_buffer_size_mb, + ) + + # Execute transactions + result = atc.execute(algod, wait_rounds=max_rounds_to_wait or 5) + + # Log results + if not suppress_log: + if len(transactions_to_send) > 1: + logger.info( + f"Group transaction ({group_id}) sent with {len(transactions_to_send)} transactions", + suppress_log=suppress_log or False, + ) + else: + logger.info( + f"Sent transaction ID {transactions_to_send[0].get_txid()}", + suppress_log=suppress_log or False, + ) + + # Get confirmations if not skipping + confirmations = None + if not skip_waiting: + confirmations = [algod.pending_transaction_info(t.get_txid()) for t in transactions_to_send] + + # Return results + return SendAtomicTransactionComposerResults( + group_id=group_id or "", + confirmations=confirmations or [], + tx_ids=[t.get_txid() for t in transactions_to_send], + transactions=[TransactionWrapper(t) for t in transactions_to_send], + returns=[ABIReturn(r) for r in result.abi_results], + ) + + except Exception as e: + # Handle error with debug info if enabled + if config.debug: + logger.error( + "Received error executing Atomic Transaction Composer and debug flag enabled; " + "attempting simulation to get more information", + suppress_log=suppress_log or False, + ) + + simulate = None + if config.project_root and not config.trace_all: + # Only simulate if trace_all is disabled and project_root is set + simulate = simulate_and_persist_response(atc, config.project_root, algod, config.trace_buffer_size_mb) + else: + simulate = simulate_response(atc, algod) + + traces = [] + if simulate and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget = txn_group.get("app-budget-added") + app_budget_consumed = txn_group.get("app-budget-consumed") + failure_message = txn_group.get("failure-message") + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + + traces.append( + { + "trace": exec_trace, + "app_budget": app_budget, + "app_budget_consumed": app_budget_consumed, + "failure_message": failure_message, + } + ) + + error = Exception(f"Transaction failed: {e}") + error.traces = traces # type: ignore[attr-defined] + raise error from e + + logger.error( + "Received error executing Atomic Transaction Composer, for more information enable the debug flag", + suppress_log=suppress_log or False, + ) + raise e + + +class TransactionComposer: + """A class for composing and managing Algorand transactions. + + Provides a high-level interface for building and executing transaction groups using the Algosdk library. + Supports various transaction types including payments, asset operations, application calls, and key registrations. + + :param algod: An instance of AlgodClient used to get suggested params and send transactions + :param get_signer: A function that takes an address and returns a TransactionSigner for that address + :param get_suggested_params: Optional function to get suggested transaction parameters, + defaults to using algod.suggested_params() + :param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 + :param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None + """ + + def __init__( + self, + algod: AlgodClient, + get_signer: Callable[[str], TransactionSigner], + get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None, + default_validity_window: int | None = None, + app_manager: AppManager | None = None, + ): + # Map of transaction index in the atc to a max logical fee. + # This is set using the value of either maxFee or staticFee. + self._txn_max_fees: dict[int, AlgoAmount] = {} + self._txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = [] + self._atc: AtomicTransactionComposer = AtomicTransactionComposer() + self._algod: AlgodClient = algod + self._default_get_send_params = lambda: self._algod.suggested_params() + self._get_suggested_params = get_suggested_params or self._default_get_send_params + self._get_signer: Callable[[str], TransactionSigner] = get_signer + self._default_validity_window: int = default_validity_window or 10 + self._default_validity_window_is_explicit: bool = default_validity_window is not None + self._app_manager = app_manager or AppManager(algod) + + def add_transaction( + self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None + ) -> TransactionComposer: + """Add a raw transaction to the composer. + + :param transaction: The transaction to add + :param signer: Optional transaction signer, defaults to getting signer from transaction sender + :return: The transaction composer instance for chaining + """ + self._txns.append(TransactionWithSigner(txn=transaction, signer=signer or self._get_signer(transaction.sender))) + return self + + def add_payment(self, params: PaymentParams) -> TransactionComposer: + """Add a payment transaction. + + :param params: The payment transaction parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_create(self, params: AssetCreateParams) -> TransactionComposer: + """Add an asset creation transaction. + + :param params: The asset creation parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_config(self, params: AssetConfigParams) -> TransactionComposer: + """Add an asset configuration transaction. + + :param params: The asset configuration parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_freeze(self, params: AssetFreezeParams) -> TransactionComposer: + """Add an asset freeze transaction. + + :param params: The asset freeze parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_destroy(self, params: AssetDestroyParams) -> TransactionComposer: + """Add an asset destruction transaction. + + :param params: The asset destruction parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_transfer(self, params: AssetTransferParams) -> TransactionComposer: + """Add an asset transfer transaction. + + :param params: The asset transfer parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_opt_in(self, params: AssetOptInParams) -> TransactionComposer: + """Add an asset opt-in transaction. + + :param params: The asset opt-in parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_asset_opt_out(self, params: AssetOptOutParams) -> TransactionComposer: + """Add an asset opt-out transaction. + + :param params: The asset opt-out parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_create(self, params: AppCreateParams) -> TransactionComposer: + """Add an application creation transaction. + + :param params: The application creation parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_update(self, params: AppUpdateParams) -> TransactionComposer: + """Add an application update transaction. + + :param params: The application update parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_delete(self, params: AppDeleteParams) -> TransactionComposer: + """Add an application deletion transaction. + + :param params: The application deletion parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_call(self, params: AppCallParams) -> TransactionComposer: + """Add an application call transaction. + + :param params: The application call parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> TransactionComposer: + """Add an application creation method call transaction. + + :param params: The application creation method call parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> TransactionComposer: + """Add an application update method call transaction. + + :param params: The application update method call parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> TransactionComposer: + """Add an application deletion method call transaction. + + :param params: The application deletion method call parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_app_call_method_call(self, params: AppCallMethodCallParams) -> TransactionComposer: + """Add an application call method call transaction. + + :param params: The application call method call parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> TransactionComposer: + """Add an online key registration transaction. + + :param params: The online key registration parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> TransactionComposer: + """Add an offline key registration transaction. + + :param params: The offline key registration parameters + :return: The transaction composer instance for chaining + """ + self._txns.append(params) + return self + + def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer: + """Add an existing AtomicTransactionComposer's transactions. + + :param atc: The AtomicTransactionComposer to add + :return: The transaction composer instance for chaining + """ + self._txns.append(atc) + return self + + def count(self) -> int: + """Get the total number of transactions. + + :return: The number of transactions + """ + return len(self.build_transactions().transactions) + + def build(self) -> TransactionComposerBuildResult: + """Build the transaction group. + + :return: The built transaction group result + """ + if self._atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING: + suggested_params = self._get_suggested_params() + txn_with_signers: list[TransactionWithSignerAndContext] = [] + + for txn in self._txns: + txn_with_signers.extend(self._build_txn(txn, suggested_params)) + + for ts in txn_with_signers: + self._atc.add_transaction(ts) + if ts.context.abi_method: + self._atc.method_dict[len(self._atc.txn_list) - 1] = ts.context.abi_method + if ts.context.max_fee: + self._txn_max_fees[len(self._atc.txn_list) - 1] = ts.context.max_fee + + return TransactionComposerBuildResult( + atc=self._atc, + transactions=self._atc.build_group(), + method_calls=self._atc.method_dict, + ) + + def rebuild(self) -> TransactionComposerBuildResult: + """Rebuild the transaction group from scratch. + + :return: The rebuilt transaction group result + """ + self._atc = AtomicTransactionComposer() + return self.build() + + def build_transactions(self) -> BuiltTransactions: + """Build and return the transactions without executing them. + + :return: The built transactions result + """ + suggested_params = self._get_suggested_params() + + transactions: list[algosdk.transaction.Transaction] = [] + method_calls: dict[int, Method] = {} + signers: dict[int, TransactionSigner] = {} + + idx = 0 + + for txn in self._txns: + txn_with_signers: list[TransactionWithSigner] = [] + + if isinstance(txn, MethodCallParams): + txn_with_signers.extend(self._build_method_call(txn, suggested_params)) + else: + txn_with_signers.extend(self._build_txn(txn, suggested_params)) + + for ts in txn_with_signers: + transactions.append(ts.txn) + if ts.signer and ts.signer != NULL_SIGNER: + signers[idx] = ts.signer + if isinstance(ts, TransactionWithSignerAndContext) and ts.context.abi_method: + method_calls[idx] = ts.context.abi_method + idx += 1 + + return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) + + @deprecated("Use send() instead") + def execute( + self, + *, + max_rounds_to_wait: int | None = None, + ) -> SendAtomicTransactionComposerResults: + return self.send(SendParams(max_rounds_to_wait=max_rounds_to_wait)) + + def send( + self, + params: SendParams | None = None, + ) -> SendAtomicTransactionComposerResults: + """Send the transaction group to the network. + + :param params: Parameters for the send operation + :return: The transaction send results + :raises Exception: If the transaction fails + """ + group = self.build().transactions + + if not params: + has_app_call = any(isinstance(txn.txn, ApplicationCallTxn) for txn in group) + params = SendParams() if has_app_call else SendParams() + + cover_app_call_inner_transaction_fees = params.get("cover_app_call_inner_transaction_fees") + populate_app_call_resources = params.get("populate_app_call_resources") + wait_rounds = params.get("max_rounds_to_wait") + sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_transaction_fees else None + + if wait_rounds is None: + last_round = max(txn.txn.last_valid_round for txn in group) + assert sp is not None + first_round = sp.first + wait_rounds = last_round - first_round + 1 + + try: + return send_atomic_transaction_composer( + self._atc, + self._algod, + max_rounds_to_wait=wait_rounds, + suppress_log=params.get("suppress_log"), + populate_app_call_resources=populate_app_call_resources, + cover_app_call_inner_transaction_fees=cover_app_call_inner_transaction_fees, + additional_atc_context=AdditionalAtcContext( + suggested_params=sp, + max_fees=self._txn_max_fees, + ), + ) + except algosdk.error.AlgodHTTPError as e: + raise Exception(f"Transaction failed: {e}") from e + + def _handle_simulate_error(self, simulate_response: SimulateAtomicTransactionResponse) -> None: + # const failedGroup = simulateResponse?.txnGroups[0] + failed_group = simulate_response.simulate_response.get("txn-groups", [{}])[0] + failure_message = failed_group.get("failure-message") + failed_at = [str(x) for x in failed_group.get("failed-at", [])] + if failure_message: + error_message = ( + f"Transaction failed at transaction(s) {', '.join(failed_at) if failed_at else 'N/A'} in the group. " + f"{failure_message}" + ) + raise Exception(error_message) + + def simulate( + self, + allow_more_logs: bool | None = None, + allow_empty_signatures: bool | None = None, + allow_unnamed_resources: bool | None = None, + extra_opcode_budget: int | None = None, + exec_trace_config: SimulateTraceConfig | None = None, + simulation_round: int | None = None, + skip_signatures: bool | None = None, + ) -> SendAtomicTransactionComposerResults: + """Simulate transaction group execution with configurable validation rules. + + :param allow_more_logs: Whether to allow more logs than the standard limit + :param allow_empty_signatures: Whether to allow transactions with empty signatures + :param allow_unnamed_resources: Whether to allow unnamed resources + :param extra_opcode_budget: Additional opcode budget to allocate + :param exec_trace_config: Configuration for execution tracing + :param simulation_round: Round number to simulate at + :param skip_signatures: Whether to skip signature validation + :return: The simulation results + """ + from algokit_utils._debugging import simulate_and_persist_response, simulate_response + + atc = AtomicTransactionComposer() if skip_signatures else self._atc + + if skip_signatures: + allow_empty_signatures = True + transactions = self.build_transactions() + for txn in transactions.transactions: + atc.add_transaction(TransactionWithSigner(txn=txn, signer=NULL_SIGNER)) + atc.method_dict = transactions.method_calls + else: + self.build() + + if config.debug and config.project_root and config.trace_all: + 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, + simulation_round, + ) + self._handle_simulate_error(response) + return SendAtomicTransactionComposerResults( + confirmations=response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][ + "txn-results" + ], + transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=[ABIReturn(r) for r in response.abi_results], + ) + + response = simulate_response( + atc, + self._algod, + allow_more_logs, + allow_empty_signatures, + allow_unnamed_resources, + extra_opcode_budget, + exec_trace_config, + simulation_round, + ) + self._handle_simulate_error(response) + 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=[ABIReturn(r) for r in response.abi_results], + ) + + @staticmethod + def arc2_note(note: Arc2TransactionNote) -> bytes: + """Create an encoded transaction note that follows the ARC-2 spec. + + https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md + + :param note: The ARC-2 note to encode + :return: The encoded note bytes + :raises ValueError: If the dapp_name is invalid + """ + + pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$" + if not re.match(pattern, note["dapp_name"]): + raise ValueError( + "dapp_name must be 5-32 chars, start with alphanumeric, " + "and contain only alphanumeric, _, /, @, ., or -" + ) + + data = note["data"] + if note["format"] == "j" and isinstance(data, (dict | list)): + # Ensure JSON data uses double quotes + data = json.dumps(data) + + arc2_payload = f"{note['dapp_name']}:{note['format']}{data}" + return arc2_payload.encode("utf-8") + + def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSignerAndContext]: + group = atc.build_group() + + txn_with_signers = [] + for idx, ts in enumerate(group): + ts.txn.group = None + if atc.method_dict.get(idx): + txn_with_signers.append( + TransactionWithSignerAndContext( + txn=ts.txn, + signer=ts.signer, + context=TransactionContext(abi_method=atc.method_dict.get(idx)), + ) + ) + else: + txn_with_signers.append( + TransactionWithSignerAndContext( + txn=ts.txn, + signer=ts.signer, + context=TransactionContext(abi_method=None), + ) + ) + + return txn_with_signers + + def _common_txn_build_step( # noqa: C901 + self, + build_txn: Callable[[dict], algosdk.transaction.Transaction], + params: _CommonTxnParams, + txn_params: dict, + ) -> TransactionWithContext: + # Clone suggested params + txn_params["sp"] = ( + algosdk.transaction.SuggestedParams(**txn_params["sp"].__dict__) if "sp" in txn_params else None + ) + + if params.lease: + txn_params["lease"] = _encode_lease(params.lease) + if params.rekey_to: + txn_params["rekey_to"] = params.rekey_to + if params.note: + txn_params["note"] = params.note + + if txn_params["sp"]: + if params.first_valid_round: + txn_params["sp"].first = params.first_valid_round + + if params.last_valid_round: + txn_params["sp"].last = params.last_valid_round + else: + # If the validity window isn't set in this transaction or by default and we are pointing at + # LocalNet set a bigger window to avoid dead transactions + from algokit_utils.clients import ClientManager + + is_localnet = ClientManager.genesis_id_is_localnet(txn_params["sp"].gen) + window = params.validity_window or ( + 1000 + if is_localnet and not self._default_validity_window_is_explicit + else self._default_validity_window + ) + txn_params["sp"].last = txn_params["sp"].first + window + + if params.static_fee is not None and txn_params["sp"]: + txn_params["sp"].fee = params.static_fee.micro_algos + txn_params["sp"].flat_fee = True + + if isinstance(txn_params.get("method"), Arc56Method): + txn_params["method"] = txn_params["method"].to_abi_method() + + txn = build_txn(txn_params) + + if params.extra_fee: + txn.fee += params.extra_fee.micro_algos + + if params.max_fee and txn.fee > params.max_fee.micro_algos: + raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}") + use_max_fee = params.max_fee and params.max_fee.micro_algo > ( + params.static_fee.micro_algo if params.static_fee else 0 + ) + logical_max_fee = params.max_fee if use_max_fee else params.static_fee + + return TransactionWithContext( + txn=txn, + context=TransactionContext(max_fee=logical_max_fee), + ) + + def _build_method_call( # noqa: C901, PLR0912, PLR0915 + self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> list[TransactionWithSignerAndContext]: + method_args: list[ABIValue | TransactionWithSigner] = [] + txns_for_group: list[TransactionWithSignerAndContext] = [] + + if params.args: + for arg in reversed(params.args): + if arg is None and len(txns_for_group) > 0: + # Pull last transaction from group as placeholder + placeholder_transaction = txns_for_group.pop() + method_args.append(placeholder_transaction) + continue + if self._is_abi_value(arg): + method_args.append(arg) + continue + + if isinstance(arg, TransactionWithSigner): + method_args.append(arg) + continue + + if isinstance(arg, algosdk.transaction.Transaction): + # Wrap in TransactionWithSigner + signer = ( + params.signer.signer + if isinstance(params.signer, TransactionSignerAccountProtocol) + else params.signer + ) + method_args.append( + TransactionWithSignerAndContext( + txn=arg, + signer=signer if signer is not None else self._get_signer(params.sender), + context=TransactionContext(abi_method=None), + ) + ) + continue + match arg: + case ( + AppCreateMethodCallParams() + | AppCallMethodCallParams() + | AppUpdateMethodCallParams() + | AppDeleteMethodCallParams() + ): + temp_txn_with_signers = self._build_method_call(arg, suggested_params) + # Add all transactions except the last one in reverse order + txns_for_group.extend(temp_txn_with_signers[:-1]) + # Add the last transaction to method_args + method_args.append(temp_txn_with_signers[-1]) + continue + case AppCallParams(): + txn = self._build_app_call(arg, suggested_params) + 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() | OfflineKeyRegistrationParams(): + txn = self._build_key_reg(arg, suggested_params) + case _: + raise ValueError(f"Unsupported method arg transaction type: {arg!s}") + + signer = ( + params.signer.signer + if isinstance(params.signer, TransactionSignerAccountProtocol) + else params.signer + ) + method_args.append( + TransactionWithSignerAndContext( + txn=txn.txn, + signer=signer or self._get_signer(params.sender), + context=TransactionContext(abi_method=params.method), + ) + ) + + continue + + method_atc = AtomicTransactionComposer() + max_fees: dict[int, AlgoAmount] = {} + + # Process in reverse order + for arg in reversed(txns_for_group): + atc_index = method_atc.get_tx_count() - 1 + + if isinstance(arg, TransactionWithSignerAndContext) and arg.context: + if arg.context.abi_method: + method_atc.method_dict[atc_index] = arg.context.abi_method + + if arg.context.max_fee is not None: + max_fees[atc_index] = arg.context.max_fee + + # Process method args that are transactions with ABI method info + for i, arg in enumerate(reversed([a for a in method_args if isinstance(a, TransactionWithSignerAndContext)])): + atc_index = method_atc.get_tx_count() + i + if arg.context: + if arg.context.abi_method: + method_atc.method_dict[atc_index] = arg.context.abi_method + if arg.context.max_fee is not None: + max_fees[atc_index] = arg.context.max_fee + + app_id = params.app_id or 0 + approval_program = getattr(params, "approval_program", None) + clear_program = getattr(params, "clear_state_program", None) + extra_pages = None + + if app_id == 0: + extra_pages = getattr(params, "extra_program_pages", None) + if extra_pages is None and approval_program is not None: + approval_len, clear_len = len(approval_program), len(clear_program or b"") + extra_pages = ( + int(math.floor((approval_len + clear_len) / algosdk.constants.APP_PAGE_MAX_SIZE)) + if approval_len + else 0 + ) + + txn_params = { + "app_id": app_id, + "method": params.method, + "sender": params.sender, + "sp": suggested_params, + "signer": params.signer + if params.signer is not None + else self._get_signer(params.sender) or algosdk.atomic_transaction_composer.EmptySigner(), + "method_args": list(reversed(method_args)), + "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, + "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, + "global_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("global_ints", 0), + num_byte_slices=params.schema.get("global_byte_slices", 0), + ) + if params.schema + else None, + "local_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("local_ints", 0), + num_byte_slices=params.schema.get("local_byte_slices", 0), + ) + if params.schema + else None, + "approval_program": approval_program, + "clear_program": clear_program, + "extra_pages": extra_pages, + } + + def _add_method_call_and_return_txn(x: dict) -> algosdk.transaction.Transaction: + method_atc.add_method_call(**x) + return method_atc.build_group()[-1].txn + + result = self._common_txn_build_step(lambda x: _add_method_call_and_return_txn(x), params, txn_params) + + build_atc_resp = self._build_atc(method_atc) + response = [] + for i, v in enumerate(build_atc_resp): + max_fee = result.context.max_fee if i == method_atc.get_tx_count() - 1 else max_fees.get(i) + context = TransactionContext(abi_method=v.context.abi_method, max_fee=max_fee) + response.append(TransactionWithSignerAndContext(txn=v.txn, signer=v.signer, context=context)) + + return response + + def _build_payment( + self, params: PaymentParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "receiver": params.receiver, + "amt": params.amount.micro_algos, + "close_remainder_to": params.close_remainder_to, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.PaymentTxn(**x), params, txn_params) + + def _build_asset_create( + self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "total": params.total, + "default_frozen": params.default_frozen or False, + "unit_name": params.unit_name or "", + "asset_name": params.asset_name or "", + "manager": params.manager, + "reserve": params.reserve, + "freeze": params.freeze, + "clawback": params.clawback, + "url": params.url or "", + "metadata_hash": params.metadata_hash, + "decimals": params.decimals or 0, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetCreateTxn(**x), params, txn_params) + + def _build_app_call( + self, + params: AppCallParams | AppUpdateParams | AppCreateParams | AppDeleteParams, + suggested_params: algosdk.transaction.SuggestedParams, + ) -> TransactionWithContext: + app_id = getattr(params, "app_id", 0) + + approval_program = None + clear_program = None + + if isinstance(params, AppUpdateParams | AppCreateParams): + if isinstance(params.approval_program, str): + approval_program = self._app_manager.compile_teal(params.approval_program).compiled_base64_to_bytes + elif isinstance(params.approval_program, bytes): + approval_program = params.approval_program + + if isinstance(params.clear_state_program, str): + clear_program = self._app_manager.compile_teal(params.clear_state_program).compiled_base64_to_bytes + elif isinstance(params.clear_state_program, bytes): + clear_program = params.clear_state_program + + approval_program_len = len(approval_program) if approval_program else 0 + clear_program_len = len(clear_program) if clear_program else 0 + + sdk_params = { + "sender": params.sender, + "sp": suggested_params, + "app_args": params.args, + "on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC, + "accounts": params.account_references, + "foreign_apps": params.app_references, + "foreign_assets": params.asset_references, + "boxes": params.box_references, + "approval_program": approval_program, + "clear_program": clear_program, + } + + txn_params = {**sdk_params, "index": app_id} + + if not app_id and isinstance(params, AppCreateParams): + if not sdk_params["approval_program"] or not sdk_params["clear_program"]: + raise ValueError("approval_program and clear_program are required for application creation") + + if not params.schema: + raise ValueError("schema is required for application creation") + + txn_params = { + **txn_params, + "global_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("global_ints", 0), + num_byte_slices=params.schema.get("global_byte_slices", 0), + ), + "local_schema": algosdk.transaction.StateSchema( + num_uints=params.schema.get("local_ints", 0), + num_byte_slices=params.schema.get("local_byte_slices", 0), + ), + "extra_pages": params.extra_program_pages + or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE) + if params.extra_program_pages + else 0, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.ApplicationCallTxn(**x), params, txn_params) + + def _build_asset_config( + self, params: AssetConfigParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + "manager": params.manager, + "reserve": params.reserve, + "freeze": params.freeze, + "clawback": params.clawback, + "strict_empty_address_check": False, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetConfigTxn(**x), params, txn_params) + + def _build_asset_destroy( + self, params: AssetDestroyParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetDestroyTxn(**x), params, txn_params) + + def _build_asset_freeze( + self, params: AssetFreezeParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "index": params.asset_id, + "target": params.account, + "new_freeze_state": params.frozen, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetFreezeTxn(**x), params, txn_params) + + def _build_asset_transfer( + self, params: AssetTransferParams, suggested_params: algosdk.transaction.SuggestedParams + ) -> TransactionWithContext: + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "receiver": params.receiver, + "amt": params.amount, + "index": params.asset_id, + "close_assets_to": params.close_asset_to, + "revocation_target": params.clawback_target, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.AssetTransferTxn(**x), params, txn_params) + + def _build_key_reg( + self, + params: OnlineKeyRegistrationParams | OfflineKeyRegistrationParams, + suggested_params: algosdk.transaction.SuggestedParams, + ) -> TransactionWithContext: + if isinstance(params, OnlineKeyRegistrationParams): + txn_params = { + "sender": params.sender, + "sp": suggested_params, + "votekey": params.vote_key, + "selkey": params.selection_key, + "votefst": params.vote_first, + "votelst": params.vote_last, + "votekd": params.vote_key_dilution, + "rekey_to": params.rekey_to, + "nonpart": False, + "sprfkey": params.state_proof_key, + } + + return self._common_txn_build_step(lambda x: algosdk.transaction.KeyregTxn(**x), params, txn_params) + + return self._common_txn_build_step( + lambda x: algosdk.transaction.KeyregTxn(**x), + params, + { + "sender": params.sender, + "sp": suggested_params, + "nonpart": params.prevent_account_from_ever_participating_again, + "votekey": None, + "selkey": None, + "votefst": None, + "votelst": None, + "votekd": None, + }, + ) + + def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool: + if isinstance(x, list | tuple): + return len(x) == 0 or all(self._is_abi_value(item) for item in x) + + return isinstance(x, bool | int | float | str | bytes) + + def _build_txn( # noqa: C901, PLR0912, PLR0911 + self, + txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer, + suggested_params: algosdk.transaction.SuggestedParams, + ) -> list[TransactionWithSignerAndContext]: + match txn: + case TransactionWithSigner(): + return [ + TransactionWithSignerAndContext(txn=txn.txn, signer=txn.signer, context=TransactionContext.empty()) + ] + case AtomicTransactionComposer(): + return self._build_atc(txn) + case algosdk.transaction.Transaction(): + signer = self._get_signer(txn.sender) + return [TransactionWithSignerAndContext(txn=txn, signer=signer, context=TransactionContext.empty())] + case ( + AppCreateMethodCallParams() + | AppCallMethodCallParams() + | AppUpdateMethodCallParams() + | AppDeleteMethodCallParams() + ): + return self._build_method_call(txn, suggested_params) + + signer = txn.signer.signer if isinstance(txn.signer, TransactionSignerAccountProtocol) else txn.signer # type: ignore[assignment] + signer = signer or self._get_signer(txn.sender) + + match txn: + case PaymentParams(): + payment = self._build_payment(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(payment, signer)] + case AssetCreateParams(): + asset_create = self._build_asset_create(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_create, signer)] + case AppCallParams() | AppUpdateParams() | AppCreateParams() | AppDeleteParams(): + app_call = self._build_app_call(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(app_call, signer)] + case AssetConfigParams(): + asset_config = self._build_asset_config(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_config, signer)] + case AssetDestroyParams(): + asset_destroy = self._build_asset_destroy(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_destroy, signer)] + case AssetFreezeParams(): + asset_freeze = self._build_asset_freeze(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_freeze, signer)] + case AssetTransferParams(): + asset_transfer = self._build_asset_transfer(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] + case AssetOptInParams(): + asset_transfer = self._build_asset_transfer( + AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params + ) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] + case AssetOptOutParams(): + txn_dict = txn.__dict__ + creator = txn_dict.pop("creator") + asset_transfer = self._build_asset_transfer( + AssetTransferParams(**txn_dict, receiver=txn.sender, amount=0, close_asset_to=creator), + suggested_params, + ) + return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)] + case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams(): + key_reg = self._build_key_reg(txn, suggested_params) + return [TransactionWithSignerAndContext.from_txn_with_context(key_reg, signer)] + case _: + raise ValueError(f"Unsupported txn: {txn}") diff --git a/src/algokit_utils/transactions/transaction_creator.py b/src/algokit_utils/transactions/transaction_creator.py new file mode 100644 index 00000000..cff47510 --- /dev/null +++ b/src/algokit_utils/transactions/transaction_creator.py @@ -0,0 +1,156 @@ +from collections.abc import Callable +from typing import TypeVar + +from algosdk.transaction import Transaction + +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCallParams, + AppCreateMethodCallParams, + AppCreateParams, + AppDeleteMethodCallParams, + AppDeleteParams, + AppUpdateMethodCallParams, + AppUpdateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + BuiltTransactions, + OfflineKeyRegistrationParams, + OnlineKeyRegistrationParams, + PaymentParams, + TransactionComposer, +) + +__all__ = [ + "AlgorandClientTransactionCreator", +] + +TxnParam = TypeVar("TxnParam") +TxnResult = TypeVar("TxnResult") + + +class AlgorandClientTransactionCreator: + """A creator for Algorand transactions. + + Provides methods to create various types of Algorand transactions including payments, + asset operations, application calls and key registrations. + + :param new_group: A lambda that starts a new TransactionComposer transaction group + """ + + def __init__(self, new_group: Callable[[], TransactionComposer]) -> None: + self._new_group = new_group + + def _transaction( + self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] + ) -> Callable[[TxnParam], Transaction]: + def create_transaction(params: TxnParam) -> Transaction: + composer = self._new_group() + result = c(composer)(params).build_transactions() + return result.transactions[-1] + + return create_transaction + + def _transactions( + self, c: Callable[[TransactionComposer], Callable[[TxnParam], TransactionComposer]] + ) -> Callable[[TxnParam], BuiltTransactions]: + def create_transactions(params: TxnParam) -> BuiltTransactions: + composer = self._new_group() + return c(composer)(params).build_transactions() + + return create_transactions + + @property + def payment(self) -> Callable[[PaymentParams], Transaction]: + """Create a payment transaction to transfer Algo between accounts.""" + return self._transaction(lambda c: c.add_payment) + + @property + def asset_create(self) -> Callable[[AssetCreateParams], Transaction]: + """Create a create Algorand Standard Asset transaction.""" + return self._transaction(lambda c: c.add_asset_create) + + @property + def asset_config(self) -> Callable[[AssetConfigParams], Transaction]: + """Create an asset config transaction to reconfigure an existing Algorand Standard Asset.""" + return self._transaction(lambda c: c.add_asset_config) + + @property + def asset_freeze(self) -> Callable[[AssetFreezeParams], Transaction]: + """Create an Algorand Standard Asset freeze transaction.""" + return self._transaction(lambda c: c.add_asset_freeze) + + @property + def asset_destroy(self) -> Callable[[AssetDestroyParams], Transaction]: + """Create an Algorand Standard Asset destroy transaction.""" + return self._transaction(lambda c: c.add_asset_destroy) + + @property + def asset_transfer(self) -> Callable[[AssetTransferParams], Transaction]: + """Create an Algorand Standard Asset transfer transaction.""" + return self._transaction(lambda c: c.add_asset_transfer) + + @property + def asset_opt_in(self) -> Callable[[AssetOptInParams], Transaction]: + """Create an Algorand Standard Asset opt-in transaction.""" + return self._transaction(lambda c: c.add_asset_opt_in) + + @property + def asset_opt_out(self) -> Callable[[AssetOptOutParams], Transaction]: + """Create an asset opt-out transaction.""" + return self._transaction(lambda c: c.add_asset_opt_out) + + @property + def app_create(self) -> Callable[[AppCreateParams], Transaction]: + """Create an application create transaction.""" + return self._transaction(lambda c: c.add_app_create) + + @property + def app_update(self) -> Callable[[AppUpdateParams], Transaction]: + """Create an application update transaction.""" + return self._transaction(lambda c: c.add_app_update) + + @property + def app_delete(self) -> Callable[[AppDeleteParams], Transaction]: + """Create an application delete transaction.""" + return self._transaction(lambda c: c.add_app_delete) + + @property + def app_call(self) -> Callable[[AppCallParams], Transaction]: + """Create an application call transaction.""" + return self._transaction(lambda c: c.add_app_call) + + @property + def app_create_method_call(self) -> Callable[[AppCreateMethodCallParams], BuiltTransactions]: + """Create an application create call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_create_method_call) + + @property + def app_update_method_call(self) -> Callable[[AppUpdateMethodCallParams], BuiltTransactions]: + """Create an application update call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_update_method_call) + + @property + def app_delete_method_call(self) -> Callable[[AppDeleteMethodCallParams], BuiltTransactions]: + """Create an application delete call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_delete_method_call) + + @property + def app_call_method_call(self) -> Callable[[AppCallMethodCallParams], BuiltTransactions]: + """Create an application call with ABI method call transaction.""" + return self._transactions(lambda c: c.add_app_call_method_call) + + @property + def online_key_registration(self) -> Callable[[OnlineKeyRegistrationParams], Transaction]: + """Create an online key registration transaction.""" + return self._transaction(lambda c: c.add_online_key_registration) + + @property + def offline_key_registration(self) -> Callable[[OfflineKeyRegistrationParams], Transaction]: + """Create an offline key registration transaction.""" + return self._transaction(lambda c: c.add_offline_key_registration) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py new file mode 100644 index 00000000..d6794331 --- /dev/null +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -0,0 +1,574 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import algosdk +import algosdk.atomic_transaction_composer +from algosdk.transaction import Transaction +from typing_extensions import Self + +from algokit_utils.applications.abi import ABIReturn +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.config import config +from algokit_utils.models.transaction import SendParams, TransactionWrapper +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCallParams, + AppCreateMethodCallParams, + AppCreateParams, + AppDeleteMethodCallParams, + AppDeleteParams, + AppUpdateMethodCallParams, + AppUpdateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OfflineKeyRegistrationParams, + OnlineKeyRegistrationParams, + PaymentParams, + SendAtomicTransactionComposerResults, + TransactionComposer, + TxnParams, +) + +__all__ = [ + "AlgorandClientTransactionSender", + "SendAppCreateTransactionResult", + "SendAppTransactionResult", + "SendAppUpdateTransactionResult", + "SendSingleAssetCreateTransactionResult", + "SendSingleTransactionResult", +] + +logger = config.logger + + +TxnParamsT = TypeVar("TxnParamsT", bound=TxnParams) + + +@dataclass(frozen=True, kw_only=True) +class SendSingleTransactionResult: + """Base class for transaction results. + + Represents the result of sending a single transaction. + """ + + transaction: TransactionWrapper # Last transaction + confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation + + # Fields from SendAtomicTransactionComposerResults + group_id: str + tx_id: str | None = None + tx_ids: list[str] # Full array of transaction IDs + transactions: list[TransactionWrapper] + confirmations: list[algosdk.v2client.algod.AlgodResponseType] + returns: list[ABIReturn] | None = None + + @classmethod + def from_composer_result(cls, result: SendAtomicTransactionComposerResults, index: int = -1) -> Self: + # Get base parameters + base_params = { + "transaction": result.transactions[index], + "confirmation": result.confirmations[index], + "group_id": result.group_id, + "tx_id": result.tx_ids[index], + "tx_ids": result.tx_ids, + "transactions": [result.transactions[index]], + "confirmations": result.confirmations, + "returns": result.returns, + } + + # For asset creation, extract asset_id from confirmation + if cls is SendSingleAssetCreateTransactionResult: + base_params["asset_id"] = result.confirmations[index]["asset-index"] # type: ignore[call-overload] + # For app creation, extract app_id and calculate app_address + elif cls is SendAppCreateTransactionResult: + app_id = result.confirmations[index]["application-index"] # type: ignore[call-overload] + base_params.update( + { + "app_id": app_id, + "app_address": algosdk.logic.get_application_address(app_id), + "abi_return": result.returns[index] if result.returns else None, # type: ignore[dict-item] + } + ) + # For regular app transactions, just add abi_return + elif cls is SendAppTransactionResult: + base_params["abi_return"] = result.returns[index] if result.returns else None # type: ignore[assignment] + + return cls(**base_params) # type: ignore[arg-type] + + +@dataclass(frozen=True, kw_only=True) +class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): + """Result of creating a new ASA (Algorand Standard Asset). + + Contains the asset ID of the newly created asset. + """ + + asset_id: int + + +ABIReturnT = TypeVar("ABIReturnT") + + +@dataclass(frozen=True) +class SendAppTransactionResult(SendSingleTransactionResult, Generic[ABIReturnT]): + """Result of an application transaction. + + Contains the ABI return value if applicable. + """ + + abi_return: ABIReturnT | None = None + + +@dataclass(frozen=True) +class SendAppUpdateTransactionResult(SendAppTransactionResult[ABIReturnT]): + """Result of updating an application. + + Contains the compiled approval and clear programs. + """ + + compiled_approval: Any | None = None + compiled_clear: Any | None = None + + +@dataclass(frozen=True, kw_only=True) +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult[ABIReturnT]): + """Result of creating a new application. + + Contains the app ID and address of the newly created application. + """ + + app_id: int + app_address: str + + +class AlgorandClientTransactionSender: + """Orchestrates sending transactions for AlgorandClient. + + Provides methods to send various types of transactions including payments, + asset operations, and application calls. + """ + + def __init__( + self, + new_group: Callable[[], TransactionComposer], + asset_manager: AssetManager, + app_manager: AppManager, + algod_client: algosdk.v2client.algod.AlgodClient, + ) -> None: + self._new_group = new_group + self._asset_manager = asset_manager + self._app_manager = app_manager + self._algod = algod_client + + def new_group(self) -> TransactionComposer: + """Create a new transaction group. + + :return: A new TransactionComposer instance + """ + return self._new_group() + + def _send( + self, + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, SendParams | None], SendSingleTransactionResult]: + def send_transaction(params: TxnParamsT, send_params: SendParams | None = None) -> SendSingleTransactionResult: + composer = self.new_group() + c(composer)(params) + + if pre_log: + transaction = composer.build().transactions[-1].txn + logger.debug(pre_log(params, transaction)) + + raw_result = composer.send( + send_params, + ) + 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, + confirmation=raw_result.confirmations[-1], + transaction=raw_result_dict["transactions"][-1], + tx_id=raw_result.tx_ids[-1], + ) + + if post_log: + logger.debug(post_log(params, result)) + + return result + + return send_transaction + + def _send_app_call( + self, + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, SendParams | None], SendAppTransactionResult[ABIReturn]]: + def send_app_call( + params: TxnParamsT, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + result = self._send(c, pre_log, post_log)(params, send_params) + return SendAppTransactionResult[ABIReturn]( + **result.__dict__, + abi_return=AppManager.get_abi_return(result.confirmation, getattr(params, "method", None)), + ) + + return send_app_call + + def _send_app_update_call( + self, + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, SendParams | None], SendAppUpdateTransactionResult[ABIReturn]]: + def send_app_update_call( + params: TxnParamsT, send_params: SendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: + result = self._send_app_call(c, pre_log, post_log)(params, send_params) + + if not isinstance( + params, AppCreateParams | AppUpdateParams | AppCreateMethodCallParams | AppUpdateMethodCallParams + ): + raise TypeError("Invalid parameter type") + + compiled_approval = ( + self._app_manager.get_compilation_result(params.approval_program) + if isinstance(params.approval_program, str) + else None + ) + compiled_clear = ( + self._app_manager.get_compilation_result(params.clear_state_program) + if isinstance(params.clear_state_program, str) + else None + ) + + return SendAppUpdateTransactionResult[ABIReturn]( + **result.__dict__, + compiled_approval=compiled_approval, + compiled_clear=compiled_clear, + ) + + return send_app_update_call + + def _send_app_create_call( + self, + c: Callable[[TransactionComposer], Callable[[TxnParamsT], TransactionComposer]], + pre_log: Callable[[TxnParamsT, Transaction], str] | None = None, + post_log: Callable[[TxnParamsT, SendSingleTransactionResult], str] | None = None, + ) -> Callable[[TxnParamsT, SendParams | None], SendAppCreateTransactionResult[ABIReturn]]: + def send_app_create_call( + params: TxnParamsT, send_params: SendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: + result = self._send_app_update_call(c, pre_log, post_log)(params, send_params) + app_id = int(result.confirmation["application-index"]) # type: ignore[call-overload] + + return SendAppCreateTransactionResult[ABIReturn]( + **result.__dict__, + app_id=app_id, + app_address=algosdk.logic.get_application_address(app_id), + ) + + return send_app_create_call + + def _get_method_call_for_log(self, method: algosdk.abi.Method, args: list[Any]) -> str: + """Helper function to format method call logs similar to TypeScript version""" + args_str = str([str(a) if not isinstance(a, bytes | bytearray) else a.hex() for a in args]) + return f"{method.name}({args_str})" + + def payment(self, params: PaymentParams, send_params: SendParams | None = None) -> SendSingleTransactionResult: + """Send a payment transaction to transfer Algo between accounts. + + :param params: Payment transaction parameters + :param send_params: Send parameters + :return: Result of the payment transaction + """ + return self._send( + lambda c: c.add_payment, + pre_log=lambda params, transaction: ( + f"Sending {params.amount} from {params.sender} to {params.receiver} " + f"via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def asset_create( + self, params: AssetCreateParams, send_params: SendParams | None = None + ) -> SendSingleAssetCreateTransactionResult: + """Create a new Algorand Standard Asset. + + :param params: Asset creation parameters + :param send_params: Send parameters + :return: Result containing the new asset ID + """ + result = self._send( + lambda c: c.add_asset_create, + post_log=lambda params, result: ( + f"Created asset{f' {params.asset_name}' if hasattr(params, 'asset_name') else ''}" + f"{f' ({params.unit_name})' if hasattr(params, 'unit_name') else ''} with " + f"{params.total} units and {getattr(params, 'decimals', 0)} decimals created by " + f"{params.sender} with ID {result.confirmation['asset-index']} via transaction " # type: ignore[call-overload] + f"{result.tx_ids[-1]}" + ), + )(params, send_params) + + return SendSingleAssetCreateTransactionResult( + **result.__dict__, + asset_id=int(result.confirmation["asset-index"]), # type: ignore[call-overload] + ) + + def asset_config( + self, params: AssetConfigParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Configure an existing Algorand Standard Asset. + + :param params: Asset configuration parameters + :param send_params: Send parameters + :return: Result of the configuration transaction + """ + return self._send( + lambda c: c.add_asset_config, + pre_log=lambda params, transaction: ( + f"Configuring asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def asset_freeze( + self, params: AssetFreezeParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Freeze or unfreeze an Algorand Standard Asset for an account. + + :param params: Asset freeze parameters + :param send_params: Send parameters + :return: Result of the freeze transaction + """ + return self._send( + lambda c: c.add_asset_freeze, + pre_log=lambda params, transaction: ( + f"Freezing asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def asset_destroy( + self, params: AssetDestroyParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Destroys an Algorand Standard Asset. + + :param params: Asset destruction parameters + :param send_params: Send parameters + :return: Result of the destroy transaction + """ + return self._send( + lambda c: c.add_asset_destroy, + pre_log=lambda params, transaction: ( + f"Destroying asset with ID {params.asset_id} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def asset_transfer( + self, params: AssetTransferParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Transfer an Algorand Standard Asset. + + :param params: Asset transfer parameters + :param send_params: Send parameters + :return: Result of the transfer transaction + """ + return self._send( + lambda c: c.add_asset_transfer, + pre_log=lambda params, transaction: ( + f"Transferring {params.amount} units of asset with ID {params.asset_id} from " + f"{params.sender} to {params.receiver} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def asset_opt_in( + self, params: AssetOptInParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Opt an account into an Algorand Standard Asset. + + :param params: Asset opt-in parameters + :param send_params: Send parameters + :return: Result of the opt-in transaction + """ + return self._send( + lambda c: c.add_asset_opt_in, + pre_log=lambda params, transaction: ( + f"Opting in {params.sender} to asset with ID {params.asset_id} via transaction " + f"{transaction.get_txid()}" + ), + )(params, send_params) + + def asset_opt_out( + self, + *, + params: AssetOptOutParams, + send_params: SendParams | None = None, + ensure_zero_balance: bool = True, + ) -> SendSingleTransactionResult: + """Opt an account out of an Algorand Standard Asset. + + :param params: Asset opt-out parameters + :param send_params: Send parameters + :param ensure_zero_balance: Check if account has zero balance before opt-out, defaults to True + :raises ValueError: If account has non-zero balance or is not opted in + :return: Result of the opt-out transaction + """ + if ensure_zero_balance: + try: + account_asset_info = self._asset_manager.get_account_information(params.sender, params.asset_id) + balance = account_asset_info.balance + if balance != 0: + raise ValueError( + f"Account {params.sender} does not have a zero balance for Asset " + f"{params.asset_id}; can't opt-out." + ) + except Exception as e: + raise ValueError( + f"Account {params.sender} is not opted-in to Asset {params.asset_id}; " "can't opt-out." + ) from e + + if not hasattr(params, "creator"): + asset_info = self._asset_manager.get_by_id(params.asset_id) + params = AssetOptOutParams( + **params.__dict__, + creator=asset_info.creator, + ) + + creator = params.__dict__.get("creator") + return self._send( + lambda c: c.add_asset_opt_out, + pre_log=lambda params, transaction: ( + f"Opting {params.sender} out of asset with ID {params.asset_id} to creator " + f"{creator} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def app_create( + self, params: AppCreateParams, send_params: SendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: + """Create a new application. + + :param params: Application creation parameters + :param send_params: Send parameters + :return: Result containing the new application ID and address + """ + return self._send_app_create_call(lambda c: c.add_app_create)(params, send_params) + + def app_update( + self, params: AppUpdateParams, send_params: SendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: + """Update an application. + + :param params: Application update parameters + :param send_params: Send parameters + :return: Result containing the compiled programs + """ + return self._send_app_update_call(lambda c: c.add_app_update)(params, send_params) + + def app_delete( + self, params: AppDeleteParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Delete an application. + + :param params: Application deletion parameters + :param send_params: Send parameters + :return: Result of the deletion transaction + """ + return self._send_app_call(lambda c: c.add_app_delete)(params, send_params) + + def app_call( + self, params: AppCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Call an application. + + :param params: Application call parameters + :param send_params: Send parameters + :return: Result containing any ABI return value + """ + return self._send_app_call(lambda c: c.add_app_call)(params, send_params) + + def app_create_method_call( + self, params: AppCreateMethodCallParams, send_params: SendParams | None = None + ) -> SendAppCreateTransactionResult[ABIReturn]: + """Call an application's create method. + + :param params: Method call parameters for application creation + :param send_params: Send parameters + :return: Result containing the new application ID and address + """ + return self._send_app_create_call(lambda c: c.add_app_create_method_call)(params, send_params) + + def app_update_method_call( + self, params: AppUpdateMethodCallParams, send_params: SendParams | None = None + ) -> SendAppUpdateTransactionResult[ABIReturn]: + """Call an application's update method. + + :param params: Method call parameters for application update + :param send_params: Send parameters + :return: Result containing the compiled programs + """ + return self._send_app_update_call(lambda c: c.add_app_update_method_call)(params, send_params) + + def app_delete_method_call( + self, params: AppDeleteMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Call an application's delete method. + + :param params: Method call parameters for application deletion + :param send_params: Send parameters + :return: Result of the deletion transaction + """ + return self._send_app_call(lambda c: c.add_app_delete_method_call)(params, send_params) + + def app_call_method_call( + self, params: AppCallMethodCallParams, send_params: SendParams | None = None + ) -> SendAppTransactionResult[ABIReturn]: + """Call an application's call method. + + :param params: Method call parameters + :param send_params: Send parameters + :return: Result containing any ABI return value + """ + return self._send_app_call(lambda c: c.add_app_call_method_call)(params, send_params) + + def online_key_registration( + self, params: OnlineKeyRegistrationParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Register an online key. + + :param params: Key registration parameters + :param send_params: Send parameters + :return: Result of the registration transaction + """ + return self._send( + lambda c: c.add_online_key_registration, + pre_log=lambda params, transaction: ( + f"Registering online key for {params.sender} via transaction {transaction.get_txid()}" + ), + )(params, send_params) + + def offline_key_registration( + self, params: OfflineKeyRegistrationParams, send_params: SendParams | None = None + ) -> SendSingleTransactionResult: + """Register an offline key. + + :param params: Key registration parameters + :param send_params: Send parameters + :return: Result of the registration transaction + """ + return self._send( + lambda c: c.add_offline_key_registration, + pre_log=lambda params, transaction: ( + f"Registering offline key for {params.sender} via transaction {transaction.get_txid()}" + ), + )(params, send_params) diff --git a/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt new file mode 100644 index 00000000..bdedcb69 --- /dev/null +++ b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps.approved.txt @@ -0,0 +1 @@ +{"txn-group-sources": [{"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}, {"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}]} \ No newline at end of file diff --git a/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt new file mode 100644 index 00000000..bdedcb69 --- /dev/null +++ b/tests/_snapshots/test_debug_utils.approvals/test_build_teal_sourcemaps_without_sources.approved.txt @@ -0,0 +1 @@ +{"txn-group-sources": [{"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}, {"sourcemap-location": "dummy", "hash": "EC1P8unO+zjVbdF8XZOs1rp+uaGNk7vXtZ/IYsN/sug="}]} \ No newline at end of file diff --git a/tests/accounts/__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..3f4b55e0 --- /dev/null +++ b/tests/accounts/test_account_manager.py @@ -0,0 +1,107 @@ +import algosdk +import pytest + +from algokit_utils import SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.amount import AlgoAmount +from tests.conftest import get_unique_name + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), 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_without_pending_rewards >= 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 info.amount is not None + assert info.min_balance is not None + assert info.address is not None + assert info.address == account.address diff --git a/tests/applications/__init__.py b/tests/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/applications/_snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt b/tests/applications/_snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt new file mode 100644 index 00000000..3795ccbf --- /dev/null +++ b/tests/applications/_snapshots/test_app_manager.approvals/test_comment_stripping.approved.txt @@ -0,0 +1,30 @@ + + +op arg +op "arg" +op "//" +op " //comment " +op "\" //" +op "// \" //" +op "" + +op 123 +op 123 +op "" +op "//" +op "//" +pushbytes base64(//8=) +pushbytes b64(//8=) + +pushbytes base64(//8=) +pushbytes b64(//8=) +pushbytes "base64(//8=)" +pushbytes "b64(//8=)" + +pushbytes base64 //8= +pushbytes b64 //8= + +pushbytes base64 //8= +pushbytes b64 //8= +pushbytes "base64 //8=" +pushbytes "b64 //8=" diff --git a/tests/applications/_snapshots/test_app_manager.approvals/test_template_substitution.approved.txt b/tests/applications/_snapshots/test_app_manager.approvals/test_template_substitution.approved.txt new file mode 100644 index 00000000..6cbde085 --- /dev/null +++ b/tests/applications/_snapshots/test_app_manager.approvals/test_template_substitution.approved.txt @@ -0,0 +1,21 @@ + +test 123 // TMPL_INT +test 123 +no change +test 0x414243 // TMPL_STR +0x414243 +0x414243 // TMPL_INT +0x414243 // foo // +0x414243 // bar +test "TMPL_STR" // not replaced +test "TMPL_STRING" // not replaced +test TMPL_STRING // not replaced +test TMPL_STRI // not replaced +test 0x414243 123 123 0x414243 // TMPL_STR TMPL_INT TMPL_INT TMPL_STR +test 123 0x414243 TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING +test 123 123 TMPL_STRING TMPL_STRING TMPL_STRING 123 TMPL_STRING //keep +0x414243 0x414243 0x414243 +TMPL_STRING +test NOTTMPL_STR // not replaced +NOTTMPL_STR // not replaced +0x414243 // replaced \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt new file mode 100644 index 00000000..d16ea4c2 --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_instance.approved.txt @@ -0,0 +1,58 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + } + ], + "name": "hello", + "returns": { + "type": "string" + }, + "events": [] + } + ], + "name": "HelloWorld", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + } +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt new file mode 100644 index 00000000..15f2f121 --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_arc32_json.approved.txt @@ -0,0 +1,58 @@ +{ + "arcs": [], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "string", + "name": "name" + } + ], + "name": "hello", + "returns": { + "type": "string" + }, + "events": [] + } + ], + "name": "HelloWorld", + "state": { + "keys": { + "box": {}, + "global": {}, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 0, + "ints": 0 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5hcHByb3ZhbF9wcm9ncmFtOgogICAgaW50Y2Jsb2NrIDAgMQogICAgY2FsbHN1YiBfX3B1eWFfYXJjNF9yb3V0ZXJfXwogICAgcmV0dXJuCgoKLy8gc21hcnRfY29udHJhY3RzLmhlbGxvX3dvcmxkLmNvbnRyYWN0LkhlbGxvV29ybGQuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHByb3RvIDAgMQogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A1CiAgICBwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyBtZXRob2QgImhlbGxvKHN0cmluZylzdHJpbmciCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyCiAgICBpbnRjXzAgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19oZWxsb19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjYKICAgIC8vIEBhYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGlzIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjUKICAgIC8vIGNsYXNzIEhlbGxvV29ybGQoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGV4dHJhY3QgMiAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NgogICAgLy8gQGFiaW1ldGhvZCgpCiAgICBjYWxsc3ViIGhlbGxvCiAgICBkdXAKICAgIGxlbgogICAgaXRvYgogICAgZXh0cmFjdCA2IDIKICAgIHN3YXAKICAgIGNvbmNhdAogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUKICAgIHN3YXAKICAgIGNvbmNhdAogICAgbG9nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy9oZWxsb193b3JsZC9jb250cmFjdC5weTo1CiAgICAvLyBjbGFzcyBIZWxsb1dvcmxkKEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUA5CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGlzIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDk6CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6NQogICAgLy8gY2xhc3MgSGVsbG9Xb3JsZChBUkM0Q29udHJhY3QpOgogICAgaW50Y18wIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy5oZWxsb193b3JsZC5jb250cmFjdC5IZWxsb1dvcmxkLmhlbGxvKG5hbWU6IGJ5dGVzKSAtPiBieXRlczoKaGVsbG86CiAgICAvLyBzbWFydF9jb250cmFjdHMvaGVsbG9fd29ybGQvY29udHJhY3QucHk6Ni03CiAgICAvLyBAYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBoZWxsbyhzZWxmLCBuYW1lOiBTdHJpbmcpIC0+IFN0cmluZzoKICAgIHByb3RvIDEgMQogICAgLy8gc21hcnRfY29udHJhY3RzL2hlbGxvX3dvcmxkL2NvbnRyYWN0LnB5OjgKICAgIC8vIHJldHVybiAiSGVsbG8yLCAiICsgbmFtZQogICAgcHVzaGJ5dGVzICJIZWxsbzIsICIKICAgIGZyYW1lX2RpZyAtMQogICAgY29uY2F0CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMuaGVsbG9fd29ybGQuY29udHJhY3QuSGVsbG9Xb3JsZC5jbGVhcl9zdGF0ZV9wcm9ncmFtOgogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=" + } +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt new file mode 100644 index 00000000..4d490fec --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_dict.approved.txt @@ -0,0 +1,510 @@ +{ + "arcs": [ + 22, + 28 + ], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "name": "set_governor", + "returns": { + "type": "void" + }, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token.", + "name": "seed" + }, + { + "type": "asset", + "desc": "One of the two assets this pool should allow swapping between.", + "name": "a_asset" + }, + { + "type": "asset", + "desc": "The other of the two assets this pool should allow swapping between.", + "name": "b_asset" + } + ], + "name": "bootstrap", + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens.", + "name": "a_xfer" + }, + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens.", + "name": "b_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the pool token so that we may distribute it.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "name": "b_asset" + } + ], + "name": "mint", + "returns": { + "type": "void" + }, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem", + "name": "pool_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of the pool token so we may inspect balance.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "name": "b_asset" + } + ], + "name": "burn", + "returns": { + "type": "void" + }, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B", + "name": "swap_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "name": "b_asset" + } + ], + "name": "swap", + "returns": { + "type": "void" + }, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "readonly": false, + "recommendations": {} + } + ], + "name": "ConstantProductAMM", + "state": { + "keys": { + "box": {}, + "global": { + "asset_a": { + "key": "YXNzZXRfYQ==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "asset_b": { + "key": "YXNzZXRfYg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "governor": { + "key": "Z292ZXJub3I=", + "keyType": "AVMString", + "valueType": "AVMBytes" + }, + "pool_token": { + "key": "cG9vbF90b2tlbg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "ratio": { + "key": "cmF0aW8=", + "keyType": "AVMString", + "valueType": "AVMUint64" + } + }, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 1, + "ints": 4 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "networks": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "sourceInfo": { + "approval": { + "pcOffsetMethod": "none", + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ] + }, + "clear": { + "pcOffsetMethod": "none", + "sourceInfo": [] + } + }, + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt new file mode 100644 index 00000000..4d490fec --- /dev/null +++ b/tests/applications/_snapshots/test_arc56.approvals/test_arc56_from_json.approved.txt @@ -0,0 +1,510 @@ +{ + "arcs": [ + 22, + 28 + ], + "bareActions": { + "call": [], + "create": [ + "NoOp" + ] + }, + "methods": [ + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "name": "set_governor", + "returns": { + "type": "void" + }, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "pay", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token.", + "name": "seed" + }, + { + "type": "asset", + "desc": "One of the two assets this pool should allow swapping between.", + "name": "a_asset" + }, + { + "type": "asset", + "desc": "The other of the two assets this pool should allow swapping between.", + "name": "b_asset" + } + ], + "name": "bootstrap", + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens.", + "name": "a_xfer" + }, + { + "type": "axfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens.", + "name": "b_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the pool token so that we may distribute it.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "name": "b_asset" + } + ], + "name": "mint", + "returns": { + "type": "void" + }, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem", + "name": "pool_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "cG9vbF90b2tlbg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of the pool token so we may inspect balance.", + "name": "pool_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "name": "b_asset" + } + ], + "name": "burn", + "returns": { + "type": "void" + }, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "readonly": false, + "recommendations": {} + }, + { + "actions": { + "call": [ + "NoOp" + ], + "create": [] + }, + "args": [ + { + "type": "axfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B", + "name": "swap_xfer" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYQ==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "name": "a_asset" + }, + { + "type": "asset", + "defaultValue": { + "data": "YXNzZXRfYg==", + "source": "global", + "type": "AVMString" + }, + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "name": "b_asset" + } + ], + "name": "swap", + "returns": { + "type": "void" + }, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "readonly": false, + "recommendations": {} + } + ], + "name": "ConstantProductAMM", + "state": { + "keys": { + "box": {}, + "global": { + "asset_a": { + "key": "YXNzZXRfYQ==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "asset_b": { + "key": "YXNzZXRfYg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "governor": { + "key": "Z292ZXJub3I=", + "keyType": "AVMString", + "valueType": "AVMBytes" + }, + "pool_token": { + "key": "cG9vbF90b2tlbg==", + "keyType": "AVMString", + "valueType": "AVMUint64" + }, + "ratio": { + "key": "cmF0aW8=", + "keyType": "AVMString", + "valueType": "AVMUint64" + } + }, + "local": {} + }, + "maps": { + "box": {}, + "global": {}, + "local": {} + }, + "schema": { + "global": { + "bytes": 1, + "ints": 4 + }, + "local": { + "bytes": 0, + "ints": 0 + } + } + }, + "structs": {}, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "networks": {}, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "sourceInfo": { + "approval": { + "pcOffsetMethod": "none", + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ] + }, + "clear": { + "pcOffsetMethod": "none", + "sourceInfo": [] + } + }, + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py new file mode 100644 index 00000000..6aa6e8e5 --- /dev/null +++ b/tests/applications/test_app_client.py @@ -0,0 +1,716 @@ +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.algorand import AlgorandClient +from algokit_utils.applications.abi import ABIType +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallParams, + AppClientParams, + FundAppAccountParams, +) +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.app_spec.arc56 import Arc56Contract, Network +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.models.state import BoxReference +from algokit_utils.transactions.transaction_composer import AppCreateParams, PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), 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" / "app_spec.arc32.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: SigningAccount, hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = hello_world_arc32_app_spec.global_state_schema + local_schema = hello_world_arc32_app_spec.local_state_schema + 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_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_byte_slices": 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" / "app_spec.arc32.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def testing_app_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "testing_app" / "app_spec.arc32.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_arc32_app_id( + algorand: AlgorandClient, funded_account: SigningAccount, testing_app_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_arc32_app_spec.global_state_schema + local_schema = testing_app_arc32_app_spec.local_state_schema + 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_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client( + algorand: AlgorandClient, + funded_account: SigningAccount, + 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: SigningAccount, + 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" / "app_spec.arc32.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def testing_app_puya_arc32_app_id( + algorand: AlgorandClient, funded_account: SigningAccount, testing_app_puya_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = testing_app_puya_arc32_app_spec.global_state_schema + local_schema = testing_app_puya_arc32_app_spec.local_state_schema + + 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_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "global_ints": int(global_schema.num_uints) if global_schema.num_uints else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def test_app_client_puya( + algorand: AlgorandClient, + funded_account: SigningAccount, + 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, + ) + ) + + +def test_clone_overriding_default_sender_and_inheriting_app_name( + algorand: AlgorandClient, + funded_account: SigningAccount, + 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: SigningAccount, + 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 + + # Test for explicit None when closning + cloned_app_client = app_client.clone(app_name=None) + assert cloned_app_client.app_name == app_client.app_name + + +def test_clone_inheriting_app_name_based_on_default_handling( + algorand: AlgorandClient, + funded_account: SigningAccount, + 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 = Arc56Contract.from_arc32(hello_world_arc32_app_spec) + arc56_app_spec.networks = {"localnet": Network(app_id=hello_world_arc32_app_id)} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + ) + + assert app_client + + +def test_construct_transaction_with_boxes(test_app_client: AppClient) -> None: + call = test_app_client.create_transaction.call( + AppClientMethodCallParams( + 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( + AppClientMethodCallParams( + 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: SigningAccount, 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( + AppClientMethodCallParams( + method="call_abi_txn", + args=[payment_txn, "test"], + ) + ) + + assert result.confirmation + assert len(result.transactions) == 2 + response = AppManager.get_abi_return( + result.confirmation, test_app_client.app_spec.get_arc56_method("call_abi_txn").to_abi_method() + ) + expected_return = f"Sent {amount.micro_algos}. test" + assert result.abi_return == expected_return + assert response + assert response.value == result.abi_return + + +def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( + algorand: AlgorandClient, test_app_client: AppClient, funded_account: SigningAccount +) -> None: + # Create a payment transaction with a random amount + amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + 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( + AppClientMethodCallParams( + 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: SigningAccount +) -> 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( + AppClientMethodCallParams( + 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: SigningAccount +) -> 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( + AppClientMethodCallParams( + 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], + test_app_client.app_spec.get_arc56_method("call_abi_foreign_refs").to_abi_method(), + ) + assert result.abi_return + assert str(result.abi_return).startswith("App: 345, Asset: 567, Account: ") + assert expected_return + assert expected_return.value == result.abi_return + + +def test_retrieve_state(test_app_client: AppClient, funded_account: SigningAccount) -> None: + # Test global state + test_app_client.send.call(AppClientMethodCallParams(method="set_global", args=[1, 2, "asdf", bytes([1, 2, 3, 4])])) + global_state = test_app_client.get_global_state() + + 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(AppClientMethodCallParams(method="opt_in")) + test_app_client.send.call(AppClientMethodCallParams(method="set_local", args=[1, 2, "asdf", bytes([1, 2, 3, 4])])) + local_state = test_app_client.get_local_state(funded_account.address) + + assert "local_int1" in local_state + 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( + AppClientMethodCallParams( + method="set_box", + args=[box_name1, "value1"], + box_references=[box_name1], + ) + ) + test_app_client.send.call( + AppClientMethodCallParams( + 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 == b"value1" + assert box1_value == box1.value + + box2 = next(b for b in box_values if b.name.name_base64 == box_name2_base64) + assert box2.value == b"value2" + + # 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( + AppClientMethodCallParams( + 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( + AppClientMethodCallParams( + 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: + # 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( + AppClientMethodCallParams( + method=method, + args=["box1", arg_value], + box_references=[box_reference], + ) + ) + + # Encode the expected value using the specified ABI type + expected_value = ABIType.from_string(value_type).encode(arg_value) + + # Retrieve the actual box value + actual_box_value = test_app_client_puya.get_box_value(box_reference) + + # 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 + + +def test_abi_with_default_arg_method( + algorand: AlgorandClient, + funded_account: SigningAccount, + testing_app_arc32_app_id: int, + testing_app_arc32_app_spec: ApplicationSpecification, +) -> None: + arc56_app_spec = Arc56Contract.from_arc32(testing_app_arc32_app_spec) + arc56_app_spec.networks = {"localnet": Network(app_id=testing_app_arc32_app_id)} + app_client = AppClient.from_network( + algorand=algorand, + app_spec=arc56_app_spec, + default_sender=funded_account.address, + default_signer=funded_account.signer, + ) + # app_client.send. + app_client.send.opt_in(AppClientMethodCallParams(method="opt_in")) + app_client.send.call( + AppClientMethodCallParams( + 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( + AppClientMethodCallParams(method=method_signature, args=[defined_value]) + ) + + assert defined_value_result.abi_return == "Local state, defined value" + + # Test with default value + default_value_result = app_client.send.call(AppClientMethodCallParams(method=method_signature, args=[None])) + assert default_value_result + assert default_value_result.abi_return == "Local state, banana" + + +def test_exposing_logic_error(test_app_client_with_sourcemaps: AppClient) -> None: + with pytest.raises(LogicError) as exc_info: + test_app_client_with_sourcemaps.send.call(AppClientMethodCallParams(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..5a783fef --- /dev/null +++ b/tests/applications/test_app_factory.py @@ -0,0 +1,548 @@ +from pathlib import Path + +import algosdk +import pytest +from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete + +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_client import ( + AppClient, + AppClientMethodCallCreateParams, + AppClientMethodCallParams, + AppClientParams, +) +from algokit_utils.applications.app_deployer import OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_factory import ( + AppFactory, + AppFactoryCreateMethodCallParams, + AppFactoryCreateParams, +) +from algokit_utils.errors import LogicError +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), 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" / "app_spec.arc32.json").read_text() + + +@pytest.fixture +def factory(algorand: AlgorandClient, funded_account: SigningAccount, app_spec: str) -> AppFactory: + """Create AppFactory fixture""" + return algorand.client.get_app_factory(app_spec=app_spec, default_sender=funded_account.address) + + +@pytest.fixture +def arc56_factory( + algorand: AlgorandClient, + funded_account: SigningAccount, +) -> AppFactory: + """Create AppFactory fixture""" + arc56_raw_spec = ( + Path(__file__).parent.parent / "artifacts" / "testing_app_arc56" / "app_spec.arc56.json" + ).read_text() + return algorand.client.get_app_factory(app_spec=arc56_raw_spec, default_sender=funded_account.address) + + +def test_create_app(factory: AppFactory) -> None: + """Test creating an app using the factory""" + app_client, result = factory.send.bare.create( + params=AppFactoryCreateParams(), + compilation_params={ + "deploy_time_params": { + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + } + }, + ) + + assert app_client.app_id > 0 + 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, + compilation_params={ + "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=AppFactoryCreateParams( + on_complete=OnComplete.OptInOC, + ), + compilation_params={ + "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( + on_schema_break=OnSchemaBreak.Fail, + on_update=OnUpdate.Fail, + compilation_params={ + "deletable": False, + "updatable": False, + "deploy_time_params": { + "VALUE": 1, + }, + }, + ) + + +def test_deploy_app_create(factory: AppFactory) -> None: + app_client, deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + }, + ) + + assert deploy_result.operation_performed == OperationPerformed.Create + assert deploy_result.create_result + assert deploy_result.create_result.app_id > 0 + assert app_client.app_id == deploy_result.create_result.app_id + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_create_abi(factory: AppFactory) -> None: + app_client, deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + }, + create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), + ) + + assert deploy_result.operation_performed == OperationPerformed.Create + create_result = deploy_result.create_result + assert create_result is not None + assert deploy_result.app.app_id > 0 + app_index = create_result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_id == deploy_result.app.app_id == app_index + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_update(factory: AppFactory) -> None: + app_client, create_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "updatable": True, + }, + ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_result + + updated_app_client, update_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, + }, + on_update=OnUpdate.UpdateApp, + ) + assert update_deploy_result.operation_performed == OperationPerformed.Update + assert update_deploy_result.update_result + + assert create_deploy_result.app.app_id == update_deploy_result.app.app_id + assert create_deploy_result.app.app_address == update_deploy_result.app.app_address + assert create_deploy_result.create_result.confirmation + assert create_deploy_result.app.updatable + assert create_deploy_result.app.updatable == update_deploy_result.app.updatable + assert create_deploy_result.app.updated_round != update_deploy_result.app.updated_round + assert create_deploy_result.app.created_round == update_deploy_result.app.created_round + assert update_deploy_result.update_result.confirmation + confirmed_round = update_deploy_result.update_result.confirmation["confirmed-round"] # type: ignore[call-overload] + assert update_deploy_result.app.updated_round == confirmed_round + + +def test_deploy_app_update_abi(factory: AppFactory) -> None: + _, create_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "updatable": True, + }, + ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_result + created_app = create_deploy_result.create_result + + _, update_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, + }, + on_update=OnUpdate.UpdateApp, + update_params=AppClientMethodCallParams(method="update_abi", args=["args_io"]), + ) + + assert update_deploy_result.operation_performed == OperationPerformed.Update + assert update_deploy_result.update_result + assert update_deploy_result.app.app_id == created_app.app_id + assert update_deploy_result.app.app_address == created_app.app_address + assert update_deploy_result.update_result.confirmation is not None + assert update_deploy_result.app.created_round == create_deploy_result.app.created_round + assert update_deploy_result.app.updated_round != update_deploy_result.app.created_round + assert ( + update_deploy_result.app.updated_round == update_deploy_result.update_result.confirmation["confirmed-round"] # type: ignore[call-overload] + ) + assert update_deploy_result.update_result.transaction.application_call + assert update_deploy_result.update_result.transaction.application_call.on_complete == OnComplete.UpdateApplicationOC + assert update_deploy_result.update_result.abi_return == "args_io" + + +def test_deploy_app_replace(factory: AppFactory) -> None: + _, create_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "deletable": True, + }, + ) + assert create_deploy_result.operation_performed == OperationPerformed.Create + assert create_deploy_result.create_result + + _, replace_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, + }, + on_update=OnUpdate.ReplaceApp, + ) + + assert replace_deploy_result.operation_performed == OperationPerformed.Replace + assert replace_deploy_result.app.app_id > create_deploy_result.app.app_id + assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address( + replace_deploy_result.app.app_id + ) + assert replace_deploy_result.create_result is not None + assert replace_deploy_result.delete_result is not None + assert replace_deploy_result.delete_result.confirmation is not None + assert ( + len(replace_deploy_result.create_result.transactions) + len(replace_deploy_result.delete_result.transactions) + == 2 + ) + assert replace_deploy_result.delete_result.transaction.application_call + assert replace_deploy_result.delete_result.transaction.application_call.index == create_deploy_result.app.app_id + assert ( + replace_deploy_result.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + ) + + +def test_deploy_app_replace_abi(factory: AppFactory) -> None: + _, create_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 1, + }, + "deletable": True, + }, + send_params={ + "populate_app_call_resources": False, + }, + ) + + replaced_app_client, replace_deploy_result = factory.deploy( + compilation_params={ + "deploy_time_params": { + "VALUE": 2, + }, + "deletable": True, + }, + on_update=OnUpdate.ReplaceApp, + create_params=AppClientMethodCallCreateParams(method="create_abi", args=["arg_io"]), + delete_params=AppClientMethodCallParams(method="delete_abi", args=["arg2_io"]), + ) + + assert replace_deploy_result.operation_performed == OperationPerformed.Replace + assert replace_deploy_result.app.app_id > create_deploy_result.app.app_id + assert replace_deploy_result.app.app_address == algosdk.logic.get_application_address(replaced_app_client.app_id) + assert replace_deploy_result.create_result is not None + assert replace_deploy_result.delete_result is not None + assert replace_deploy_result.delete_result.confirmation is not None + assert ( + len(replace_deploy_result.create_result.transactions) + len(replace_deploy_result.delete_result.transactions) + == 2 + ) + assert replace_deploy_result.delete_result.transaction.application_call + assert replace_deploy_result.delete_result.transaction.application_call.index == create_deploy_result.app.app_id + assert ( + replace_deploy_result.delete_result.transaction.application_call.on_complete == OnComplete.DeleteApplicationOC + ) + assert replace_deploy_result.create_result.abi_return == "arg_io" + assert replace_deploy_result.delete_result.abi_return == "arg2_io" + + +def test_create_then_call_app(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + compilation_params={ + "updatable": True, + "deletable": True, + "deploy_time_params": { + "VALUE": 1, + }, + }, + ) + + call = app_client.send.call(AppClientMethodCallParams(method="call_abi", args=["test"])) + assert call.abi_return == "Hello, test" + + +def test_call_app_with_rekey(funded_account: SigningAccount, algorand: AlgorandClient, factory: AppFactory) -> None: + rekey_to = algorand.account.random() + + app_client, _ = factory.send.bare.create( + compilation_params={ + "updatable": True, + "deletable": True, + "deploy_time_params": { + "VALUE": 1, + }, + }, + ) + + app_client.send.opt_in(AppClientMethodCallParams(method="opt_in", rekey_to=rekey_to.address)) + + # If the rekey didn't work this will throw + rekeyed_account = algorand.account.rekeyed(sender=funded_account.address, account=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"], + ), + compilation_params={ + "deploy_time_params": { + "UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + }, + ) + + assert call_return.abi_return + assert call_return.abi_return == "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( + compilation_params={ + "deploy_time_params": deploy_time_params, + }, + ) + + call_return = app_client.send.update( + AppClientMethodCallParams( + method="update_abi", + args=["string_io"], + ), + compilation_params={ + "deploy_time_params": deploy_time_params, + }, + ) + + assert call_return.abi_return == "string_io" + # assert call_return.compiled_approval is not None # TODO: centralize approval/clear compilation + + +def test_delete_app_with_abi(factory: AppFactory) -> None: + app_client, _ = factory.send.bare.create( + compilation_params={ + "deploy_time_params": { + "UPDATABLE": 0, + "DELETABLE": 1, + "VALUE": 1, + }, + }, + ) + + call_return = app_client.send.delete( + AppClientMethodCallParams( + method="delete_abi", + args=["string_io"], + ) + ) + + assert call_return.abi_return == "string_io" + + +def test_export_import_sourcemaps( + factory: AppFactory, + algorand: AlgorandClient, + funded_account: SigningAccount, +) -> None: + # Export source maps from original client + app_client, _ = factory.deploy(compilation_params={"deploy_time_params": {"VALUE": 1}}) + old_sourcemaps = app_client.export_source_maps() + + # Create new client instance + new_client = AppClient( + AppClientParams( + app_id=app_client.app_id, + default_sender=funded_account.address, + default_signer=funded_account.signer, + algorand=algorand, + app_spec=app_client.app_spec, + ) + ) + + # Test error handling before importing source maps + with pytest.raises(LogicError) as exc_info: + new_client.send.call(AppClientMethodCallParams(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(AppClientMethodCallParams(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: + app_client, _ = arc56_factory.deploy( + create_params=AppClientMethodCallCreateParams(method="createApplication"), + compilation_params={ + "deploy_time_params": { + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 123, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + }, + ) + + with pytest.raises(Exception, match="this is an error"): + app_client.send.call(AppClientMethodCallParams(method="throwError")) + + +def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset( + arc56_factory: AppFactory, + algorand: AlgorandClient, + funded_account: SigningAccount, +) -> None: + # Deploy app with template parameters + app_client, _ = arc56_factory.deploy( + create_params=AppClientMethodCallCreateParams(method="createApplication"), + compilation_params={ + "deploy_time_params": { + "bytes64TmplVar": "0" * 64, + "uint64TmplVar": 0, + "bytes32TmplVar": "0" * 32, + "bytesTmplVar": "foo", + }, + }, + ) + app_id = app_client.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=app_client.app_spec, + ) + ) + + # Test error handling + with pytest.raises(LogicError) as exc_info: + app_client.send.call(AppClientMethodCallParams(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 new file mode 100644 index 00000000..660c7039 --- /dev/null +++ b/tests/applications/test_app_manager.py @@ -0,0 +1,87 @@ +import pytest + +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from tests.conftest import check_output_stability + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), 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: + program = """ +test TMPL_INT // TMPL_INT +test TMPL_INT +no change +test TMPL_STR // TMPL_STR +TMPL_STR +TMPL_STR // TMPL_INT +TMPL_STR // foo // +TMPL_STR // bar +test "TMPL_STR" // not replaced +test "TMPL_STRING" // not replaced +test TMPL_STRING // not replaced +test TMPL_STRI // not replaced +test TMPL_STR TMPL_INT TMPL_INT TMPL_STR // TMPL_STR TMPL_INT TMPL_INT TMPL_STR +test TMPL_INT TMPL_STR TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING +test TMPL_INT TMPL_INT TMPL_STRING TMPL_STRING TMPL_STRING TMPL_INT TMPL_STRING //keep +TMPL_STR TMPL_STR TMPL_STR +TMPL_STRING +test NOTTMPL_STR // not replaced +NOTTMPL_STR // not replaced +TMPL_STR // replaced +""" + result = AppManager.replace_template_variables(program, {"INT": 123, "STR": "ABC"}) + check_output_stability(result) + + +def test_comment_stripping() -> None: + program = r""" +//comment +op arg //comment +op "arg" //comment +op "//" //comment +op " //comment " //comment +op "\" //" //comment +op "// \" //" //comment +op "" //comment +// +op 123 +op 123 // something +op "" // more comments +op "//" //op "//" +op "//" +pushbytes base64(//8=) +pushbytes b64(//8=) + +pushbytes base64(//8=) // pushbytes base64(//8=) +pushbytes b64(//8=) // pushbytes b64(//8=) +pushbytes "base64(//8=)" // pushbytes "base64(//8=)" +pushbytes "b64(//8=)" // pushbytes "b64(//8=)" + +pushbytes base64 //8= +pushbytes b64 //8= + +pushbytes base64 //8= // pushbytes base64 //8= +pushbytes b64 //8= // pushbytes b64 //8= +pushbytes "base64 //8=" // pushbytes "base64 //8=" +pushbytes "b64 //8=" // pushbytes "b64 //8=" + +""" + result = AppManager.strip_teal_comments(program) + check_output_stability(result) diff --git a/tests/applications/test_arc56.py b/tests/applications/test_arc56.py new file mode 100644 index 00000000..e81f51f3 --- /dev/null +++ b/tests/applications/test_arc56.py @@ -0,0 +1,45 @@ +import json +from pathlib import Path + +from algokit_utils.applications.app_spec.arc56 import Arc56Contract +from tests.conftest import check_output_stability +from tests.utils import load_app_spec + +TEST_ARC32_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" +TEST_ARC56_SPEC_FILE_PATH = Path(__file__).parent.parent / "artifacts" / "amm_arc56_example" / "amm.arc56.json" + + +def test_arc56_from_arc32_json() -> None: + arc56_app_spec = Arc56Contract.from_arc32(TEST_ARC32_SPEC_FILE_PATH.read_text()) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json(indent=4)) + + +def test_arc56_from_arc32_instance() -> None: + arc32_app_spec = load_app_spec( + TEST_ARC32_SPEC_FILE_PATH, arc=32, deletable=True, updatable=True, template_values={"VERSION": 1} + ) + + arc56_app_spec = Arc56Contract.from_arc32(arc32_app_spec) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json(indent=4)) + + +def test_arc56_from_json() -> None: + arc56_app_spec = Arc56Contract.from_json(TEST_ARC56_SPEC_FILE_PATH.read_text()) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json(indent=4)) + + +def test_arc56_from_dict() -> None: + arc56_app_spec = Arc56Contract.from_dict(json.loads(TEST_ARC56_SPEC_FILE_PATH.read_text())) + + assert arc56_app_spec + + check_output_stability(arc56_app_spec.to_json(indent=4)) diff --git a/tests/artifacts/amm_arc56_example/amm.arc56.json b/tests/artifacts/amm_arc56_example/amm.arc56.json new file mode 100644 index 00000000..42a8e366 --- /dev/null +++ b/tests/artifacts/amm_arc56_example/amm.arc56.json @@ -0,0 +1,510 @@ +{ + "name": "ConstantProductAMM", + "structs": {}, + "methods": [ + { + "name": "set_governor", + "args": [ + { + "type": "account", + "name": "new_governor" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "sets the governor of the contract, may only be called by the current governor", + "events": [], + "recommendations": {} + }, + { + "name": "bootstrap", + "args": [ + { + "type": "pay", + "name": "seed", + "desc": "Initial Payment transaction to the app account so it can opt in to assets and create pool token." + }, + { + "type": "asset", + "name": "a_asset", + "desc": "One of the two assets this pool should allow swapping between." + }, + { + "type": "asset", + "name": "b_asset", + "desc": "The other of the two assets this pool should allow swapping between." + } + ], + "returns": { + "type": "uint64", + "desc": "The asset id of the pool token created." + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "bootstraps the contract by opting into the assets and creating the pool token.\nNote this method will fail if it is attempted more than once on the same contract since the assets and pool token application state values are marked as static and cannot be overridden.", + "events": [], + "recommendations": {} + }, + { + "name": "mint", + "args": [ + { + "type": "axfer", + "name": "a_xfer", + "desc": "Asset Transfer Transaction of asset A as a deposit to the pool in exchange for pool tokens." + }, + { + "type": "axfer", + "name": "b_xfer", + "desc": "Asset Transfer Transaction of asset B as a deposit to the pool in exchange for pool tokens." + }, + { + "type": "asset", + "name": "pool_asset", + "desc": "The asset ID of the pool token so that we may distribute it.", + "defaultValue": { + "source": "global", + "data": "cG9vbF90b2tlbg==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "a_asset", + "desc": "The asset ID of the Asset A so that we may inspect our balance.", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "The asset ID of the Asset B so that we may inspect our balance.", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "mint pool tokens given some amount of asset A and asset B.\nGiven some amount of Asset A and Asset B in the transfers, mint some number of pool tokens commensurate with the pools current balance and circulating supply of pool tokens.", + "events": [], + "recommendations": {} + }, + { + "name": "burn", + "args": [ + { + "type": "axfer", + "name": "pool_xfer", + "desc": "Asset Transfer Transaction of the pool token for the amount the sender wishes to redeem" + }, + { + "type": "asset", + "name": "pool_asset", + "desc": "Asset ID of the pool token so we may inspect balance.", + "defaultValue": { + "source": "global", + "data": "cG9vbF90b2tlbg==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "a_asset", + "desc": "Asset ID of Asset A so we may inspect balance and distribute it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "Asset ID of Asset B so we may inspect balance and distribute it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "burn pool tokens to get back some amount of asset A and asset B", + "events": [], + "recommendations": {} + }, + { + "name": "swap", + "args": [ + { + "type": "axfer", + "name": "swap_xfer", + "desc": "Asset Transfer Transaction of either Asset A or Asset B" + }, + { + "type": "asset", + "name": "a_asset", + "desc": "Asset ID of asset A so we may inspect balance and possibly transfer it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYQ==", + "type": "AVMString" + } + }, + { + "type": "asset", + "name": "b_asset", + "desc": "Asset ID of asset B so we may inspect balance and possibly transfer it", + "defaultValue": { + "source": "global", + "data": "YXNzZXRfYg==", + "type": "AVMString" + } + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "desc": "Swap some amount of either asset A or asset B for the other", + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 4, + "bytes": 1 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": { + "asset_a": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "YXNzZXRfYQ==" + }, + "asset_b": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "YXNzZXRfYg==" + }, + "governor": { + "keyType": "AVMString", + "valueType": "AVMBytes", + "key": "Z292ZXJub3I=" + }, + "pool_token": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "cG9vbF90b2tlbg==" + }, + "ratio": { + "keyType": "AVMString", + "valueType": "AVMUint64", + "key": "cmF0aW8=" + } + }, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 131, + 165, + 205, + 256, + 300 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 347 + ], + "errorMessage": "Only the account set in global_state.governor may call this method" + }, + { + "pc": [ + 744, + 757, + 770 + ], + "errorMessage": "account opted into asset" + }, + { + "pc": [ + 384, + 589, + 615, + 836, + 943 + ], + "errorMessage": "amount minimum not met" + }, + { + "pc": [ + 357 + ], + "errorMessage": "application has already been bootstrapped" + }, + { + "pc": [ + 540, + 582, + 814, + 929 + ], + "errorMessage": "asset a incorrect" + }, + { + "pc": [ + 390 + ], + "errorMessage": "asset a must be less than asset b" + }, + { + "pc": [ + 548, + 607, + 822, + 937 + ], + "errorMessage": "asset b incorrect" + }, + { + "pc": [ + 406, + 425 + ], + "errorMessage": "asset exists" + }, + { + "pc": [ + 970 + ], + "errorMessage": "asset id incorrect" + }, + { + "pc": [ + 532, + 806, + 846 + ], + "errorMessage": "asset pool incorrect" + }, + { + "pc": [ + 524, + 798, + 921 + ], + "errorMessage": "bootstrap method needs to be called first" + }, + { + "pc": [ + 323 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 134, + 168, + 208, + 259, + 303 + ], + "errorMessage": "can only call when not creating" + }, + { + "pc": [ + 403, + 466, + 536, + 580, + 754, + 810, + 890, + 925, + 955, + 1040 + ], + "errorMessage": "check self.asset_a exists" + }, + { + "pc": [ + 422, + 477, + 544, + 605, + 767, + 818, + 901, + 933, + 959, + 985 + ], + "errorMessage": "check self.asset_b exists" + }, + { + "pc": [ + 345 + ], + "errorMessage": "check self.governor exists" + }, + { + "pc": [ + 355, + 488, + 523, + 528, + 662, + 741, + 797, + 802, + 844, + 920 + ], + "errorMessage": "check self.pool_token exists" + }, + { + "pc": [ + 366 + ], + "errorMessage": "group size not 2" + }, + { + "pc": [ + 374, + 572, + 597, + 830 + ], + "errorMessage": "receiver not app address" + }, + { + "pc": [ + 656, + 1012 + ], + "errorMessage": "send amount too low" + }, + { + "pc": [ + 556, + 564, + 854, + 951 + ], + "errorMessage": "sender invalid" + }, + { + "pc": [ + 144, + 178, + 219, + 229 + ], + "errorMessage": "transaction type is axfer" + }, + { + "pc": [ + 269 + ], + "errorMessage": "transaction type is pay" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9fYWxnb3B5X2VudHJ5cG9pbnRfd2l0aF9pbml0KCkgLT4gdWludDY0OgptYWluOgogICAgaW50Y2Jsb2NrIDAgMSAxMDAwIDQgMTAwMDAwMDAwMDAKICAgIGJ5dGVjYmxvY2sgImFzc2V0X2EiICJhc3NldF9iIiAicG9vbF90b2tlbiIgImdvdmVybm9yIiAicmF0aW8iCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYm56IG1haW5fYWZ0ZXJfaWZfZWxzZUAyCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzItMzMKICAgIC8vICMgVGhlIGFzc2V0IGlkIG9mIGFzc2V0IEEKICAgIC8vIHNlbGYuYXNzZXRfYSA9IEFzc2V0KCkKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNC0zNQogICAgLy8gIyBUaGUgYXNzZXQgaWQgb2YgYXNzZXQgQgogICAgLy8gc2VsZi5hc3NldF9iID0gQXNzZXQoKQogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGludGNfMCAvLyAwCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM2LTM3CiAgICAvLyAjIFRoZSBjdXJyZW50IGdvdmVybm9yIG9mIHRoaXMgY29udHJhY3QsIGFsbG93ZWQgdG8gZG8gYWRtaW4gdHlwZSBhY3Rpb25zCiAgICAvLyBzZWxmLmdvdmVybm9yID0gVHhuLnNlbmRlcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICB0eG4gU2VuZGVyCiAgICBhcHBfZ2xvYmFsX3B1dAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM4LTM5CiAgICAvLyAjIFRoZSBhc3NldCBpZCBvZiB0aGUgUG9vbCBUb2tlbiwgdXNlZCB0byB0cmFjayBzaGFyZSBvZiBwb29sIHRoZSBob2xkZXIgbWF5IHJlY292ZXIKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IEFzc2V0KCkKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBpbnRjXzAgLy8gMAogICAgYXBwX2dsb2JhbF9wdXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo0MC00MQogICAgLy8gIyBUaGUgcmF0aW8gYmV0d2VlbiBhc3NldHMgKEEqU2NhbGUvQikKICAgIC8vIHNlbGYucmF0aW8gPSBVSW50NjQoMCkKICAgIGJ5dGVjIDQgLy8gInJhdGlvIgogICAgaW50Y18wIC8vIDAKICAgIGFwcF9nbG9iYWxfcHV0CgptYWluX2FmdGVyX2lmX2Vsc2VAMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdAMTAKICAgIHB1c2hieXRlc3MgMHgwOGE5NTZmNyAweDZiNTlkOTY1IDB4NWNiZjFlMmQgMHgxNDM2YzJhYyAweDRhODhlMDU1IC8vIG1ldGhvZCAic2V0X2dvdmVybm9yKGFjY291bnQpdm9pZCIsIG1ldGhvZCAiYm9vdHN0cmFwKHBheSxhc3NldCxhc3NldCl1aW50NjQiLCBtZXRob2QgIm1pbnQoYXhmZXIsYXhmZXIsYXNzZXQsYXNzZXQsYXNzZXQpdm9pZCIsIG1ldGhvZCAiYnVybihheGZlcixhc3NldCxhc3NldCxhc3NldCl2b2lkIiwgbWV0aG9kICJzd2FwKGF4ZmVyLGFzc2V0LGFzc2V0KXZvaWQiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3NldF9nb3Zlcm5vcl9yb3V0ZUA1IG1haW5fYm9vdHN0cmFwX3JvdXRlQDYgbWFpbl9taW50X3JvdXRlQDcgbWFpbl9idXJuX3JvdXRlQDggbWFpbl9zd2FwX3JvdXRlQDkKCm1haW5fYWZ0ZXJfaWZfZWxzZUAxMjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzAgLy8gMAogICAgcmV0dXJuCgptYWluX3N3YXBfcm91dGVAOToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjA5CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG4gR3JvdXBJbmRleAogICAgaW50Y18xIC8vIDEKICAgIC0KICAgIGR1cAogICAgZ3R4bnMgVHlwZUVudW0KICAgIGludGNfMyAvLyBheGZlcgogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIGF4ZmVyCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwNC0yMDkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgKICAgIC8vICAgICBkZWZhdWx0X2FyZ3M9ewogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgc3dhcAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX3JvdXRlQDg6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDctMTUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgY2FsbHN1YiBidXJuCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgptYWluX21pbnRfcm91dGVANzoKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBwdXNoaW50IDIgLy8gMgogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18zIC8vIGF4ZmVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHRyYW5zYWN0aW9uIHR5cGUgaXMgYXhmZXIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmFzIEFzc2V0cwogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo4MS04NwogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIGNhbGxzdWIgbWludAogICAgaW50Y18xIC8vIDEKICAgIHJldHVybgoKbWFpbl9ib290c3RyYXBfcm91dGVANjoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBHcm91cEluZGV4CiAgICBpbnRjXzEgLy8gMQogICAgLQogICAgZHVwCiAgICBndHhucyBUeXBlRW51bQogICAgaW50Y18xIC8vIHBheQogICAgPT0KICAgIGFzc2VydCAvLyB0cmFuc2FjdGlvbiB0eXBlIGlzIHBheQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYXMgQXNzZXRzCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hcyBBc3NldHMKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYm9vdHN0cmFwCiAgICBpdG9iCiAgICBwdXNoYnl0ZXMgMHgxNTFmN2M3NQogICAgc3dhcAogICAgY29uY2F0CiAgICBsb2cKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fc2V0X2dvdmVybm9yX3JvdXRlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6NDMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZCgpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIGFtbS9jb250cmFjdC5weToyNwogICAgLy8gY2xhc3MgQ29uc3RhbnRQcm9kdWN0QU1NKEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hcyBBY2NvdW50cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgY2FsbHN1YiBzZXRfZ292ZXJub3IKICAgIGludGNfMSAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDEwOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3CiAgICAvLyBjbGFzcyBDb25zdGFudFByb2R1Y3RBTU0oQVJDNENvbnRyYWN0KToKICAgIHR4biBPbkNvbXBsZXRpb24KICAgIGJueiBtYWluX2FmdGVyX2lmX2Vsc2VAMTIKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzEgLy8gMQogICAgcmV0dXJuCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zZXRfZ292ZXJub3IobmV3X2dvdmVybm9yOiBieXRlcykgLT4gdm9pZDoKc2V0X2dvdmVybm9yOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjQzLTQ0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIHNldF9nb3Zlcm5vcihzZWxmLCBuZXdfZ292ZXJub3I6IEFjY291bnQpIC0+IE5vbmU6CiAgICBwcm90byAxIDAKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NgogICAgLy8gc2VsZi5fY2hlY2tfaXNfZ292ZXJub3IoKQogICAgY2FsbHN1YiBfY2hlY2tfaXNfZ292ZXJub3IKICAgIC8vIGFtbS9jb250cmFjdC5weTo0NwogICAgLy8gc2VsZi5nb3Zlcm5vciA9IG5ld19nb3Zlcm5vcgogICAgYnl0ZWNfMyAvLyAiZ292ZXJub3IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jaGVja19pc19nb3Zlcm5vcigpIC0+IHZvaWQ6Cl9jaGVja19pc19nb3Zlcm5vcjoKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjItMjYzCiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jaGVja19pc19nb3Zlcm5vcihzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY1CiAgICAvLyBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18zIC8vICJnb3Zlcm5vciIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5nb3Zlcm5vciBleGlzdHMKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjY0LTI2NgogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBUeG4uc2VuZGVyID09IHNlbGYuZ292ZXJub3IKICAgIC8vICksICJPbmx5IHRoZSBhY2NvdW50IHNldCBpbiBnbG9iYWxfc3RhdGUuZ292ZXJub3IgbWF5IGNhbGwgdGhpcyBtZXRob2QiCiAgICBhc3NlcnQgLy8gT25seSB0aGUgYWNjb3VudCBzZXQgaW4gZ2xvYmFsX3N0YXRlLmdvdmVybm9yIG1heSBjYWxsIHRoaXMgbWV0aG9kCiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJvb3RzdHJhcChzZWVkOiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB1aW50NjQ6CmJvb3RzdHJhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weTo0OS01MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIC8vIGRlZiBib290c3RyYXAoc2VsZiwgc2VlZDogZ3R4bi5QYXltZW50VHJhbnNhY3Rpb24sIGFfYXNzZXQ6IEFzc2V0LCBiX2Fzc2V0OiBBc3NldCkgLT4gVUludDY0OgogICAgcHJvdG8gMyAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjYKICAgIC8vIGFzc2VydCBub3Qgc2VsZi5wb29sX3Rva2VuLCAiYXBwbGljYXRpb24gaGFzIGFscmVhZHkgYmVlbiBib290c3RyYXBwZWQiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgIQogICAgYXNzZXJ0IC8vIGFwcGxpY2F0aW9uIGhhcyBhbHJlYWR5IGJlZW4gYm9vdHN0cmFwcGVkCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjcKICAgIC8vIHNlbGYuX2NoZWNrX2lzX2dvdmVybm9yKCkKICAgIGNhbGxzdWIgX2NoZWNrX2lzX2dvdmVybm9yCiAgICAvLyBhbW0vY29udHJhY3QucHk6NjgKICAgIC8vIGFzc2VydCBHbG9iYWwuZ3JvdXBfc2l6ZSA9PSAyLCAiZ3JvdXAgc2l6ZSBub3QgMiIKICAgIGdsb2JhbCBHcm91cFNpemUKICAgIHB1c2hpbnQgMiAvLyAyCiAgICA9PQogICAgYXNzZXJ0IC8vIGdyb3VwIHNpemUgbm90IDIKICAgIC8vIGFtbS9jb250cmFjdC5weTo2OQogICAgLy8gYXNzZXJ0IHNlZWQucmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgUmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjcxCiAgICAvLyBhc3NlcnQgc2VlZC5hbW91bnQgPj0gMzAwXzAwMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiICAjIDAuMyBBbGdvcwogICAgZnJhbWVfZGlnIC0zCiAgICBndHhucyBBbW91bnQKICAgIHB1c2hpbnQgMzAwMDAwIC8vIDMwMDAwMAogICAgPj0KICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzIKICAgIC8vIGFzc2VydCBhX2Fzc2V0LmlkIDwgYl9hc3NldC5pZCwgImFzc2V0IGEgbXVzdCBiZSBsZXNzIHRoYW4gYXNzZXQgYiIKICAgIGZyYW1lX2RpZyAtMgogICAgZnJhbWVfZGlnIC0xCiAgICA8CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBtdXN0IGJlIGxlc3MgdGhhbiBhc3NldCBiCiAgICAvLyBhbW0vY29udHJhY3QucHk6NzMKICAgIC8vIHNlbGYuYXNzZXRfYSA9IGFfYXNzZXQKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBmcmFtZV9kaWcgLTIKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzQKICAgIC8vIHNlbGYuYXNzZXRfYiA9IGJfYXNzZXQKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBmcmFtZV9kaWcgLTEKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxLTI3OQogICAgLy8gaXR4bi5Bc3NldENvbmZpZygKICAgIC8vICAgICBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICAvLyAgICAgdW5pdF9uYW1lPWIiZGJ0IiwKICAgIC8vICAgICB0b3RhbD1UT1RBTF9TVVBQTFksCiAgICAvLyAgICAgZGVjaW1hbHM9MywKICAgIC8vICAgICBtYW5hZ2VyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgcmVzZXJ2ZT1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gKQogICAgLy8gLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcyCiAgICAvLyBhc3NldF9uYW1lPWIiRFBULSIgKyBzZWxmLmFzc2V0X2EudW5pdF9uYW1lICsgYiItIiArIHNlbGYuYXNzZXRfYi51bml0X25hbWUsCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBwdXNoYnl0ZXMgMHg0NDUwNTQyZAogICAgc3dhcAogICAgY29uY2F0CiAgICBwdXNoYnl0ZXMgMHgyZAogICAgY29uY2F0CiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfcGFyYW1zX2dldCBBc3NldFVuaXROYW1lCiAgICBhc3NlcnQgLy8gYXNzZXQgZXhpc3RzCiAgICBjb25jYXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzYKICAgIC8vIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjc3CiAgICAvLyByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBkdXAKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXRSZXNlcnZlCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0TWFuYWdlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3NQogICAgLy8gZGVjaW1hbHM9MywKICAgIHB1c2hpbnQgMyAvLyAzCiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVjaW1hbHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzQKICAgIC8vIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjczCiAgICAvLyB1bml0X25hbWU9YiJkYnQiLAogICAgcHVzaGJ5dGVzIDB4NjQ2Mjc0CiAgICBpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKICAgIGl0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjcxCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgcHVzaGludCAzIC8vIGFjZmcKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGludGNfMCAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI3MS0yNzkKICAgIC8vIGl0eG4uQXNzZXRDb25maWcoCiAgICAvLyAgICAgYXNzZXRfbmFtZT1iIkRQVC0iICsgc2VsZi5hc3NldF9hLnVuaXRfbmFtZSArIGIiLSIgKyBzZWxmLmFzc2V0X2IudW5pdF9uYW1lLAogICAgLy8gICAgIHVuaXRfbmFtZT1iImRidCIsCiAgICAvLyAgICAgdG90YWw9VE9UQUxfU1VQUExZLAogICAgLy8gICAgIGRlY2ltYWxzPTMsCiAgICAvLyAgICAgbWFuYWdlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIHJlc2VydmU9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICkKICAgIC8vIC5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIGFtbS9jb250cmFjdC5weTo3NQogICAgLy8gc2VsZi5wb29sX3Rva2VuID0gc2VsZi5fY3JlYXRlX3Bvb2xfdG9rZW4oKQogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIC8vIGFtbS9jb250cmFjdC5weToyNzEtMjgwCiAgICAvLyBpdHhuLkFzc2V0Q29uZmlnKAogICAgLy8gICAgIGFzc2V0X25hbWU9YiJEUFQtIiArIHNlbGYuYXNzZXRfYS51bml0X25hbWUgKyBiIi0iICsgc2VsZi5hc3NldF9iLnVuaXRfbmFtZSwKICAgIC8vICAgICB1bml0X25hbWU9YiJkYnQiLAogICAgLy8gICAgIHRvdGFsPVRPVEFMX1NVUFBMWSwKICAgIC8vICAgICBkZWNpbWFscz0zLAogICAgLy8gICAgIG1hbmFnZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICByZXNlcnZlPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyApCiAgICAvLyAuc3VibWl0KCkKICAgIC8vIC5jcmVhdGVkX2Fzc2V0CiAgICBpdHhuIENyZWF0ZWRBc3NldElECiAgICAvLyBhbW0vY29udHJhY3QucHk6NzUKICAgIC8vIHNlbGYucG9vbF90b2tlbiA9IHNlbGYuX2NyZWF0ZV9wb29sX3Rva2VuKCkKICAgIGFwcF9nbG9iYWxfcHV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6NzcKICAgIC8vIHNlbGYuX2RvX29wdF9pbihzZWxmLmFzc2V0X2EpCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9hIGV4aXN0cwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NgogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgc3dhcAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4OAogICAgLy8gYW1vdW50PVVJbnQ2NCgwKSwKICAgIGludGNfMCAvLyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjg1LTI4OQogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIoCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBhc3NldD1hc3NldCwKICAgIC8vICAgICBhbW91bnQ9VUludDY0KDApLAogICAgLy8gKQogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5Ojc4CiAgICAvLyBzZWxmLl9kb19vcHRfaW4oc2VsZi5hc3NldF9iKQogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIC8vIGFtbS9jb250cmFjdC5weToyODYKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIHN3YXAKICAgIC8vIGFtbS9jb250cmFjdC5weToyODgKICAgIC8vIGFtb3VudD1VSW50NjQoMCksCiAgICBpbnRjXzAgLy8gMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI4NS0yODkKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYW1vdW50PVVJbnQ2NCgwKSwKICAgIC8vICkKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weTo3OQogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5pZAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5kb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcjogYnl0ZXMsIGFzc2V0OiB1aW50NjQsIGFtb3VudDogdWludDY0KSAtPiB2b2lkOgpkb19hc3NldF90cmFuc2ZlcjoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTYtMzU3CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIGRvX2Fzc2V0X3RyYW5zZmVyKCosIHJlY2VpdmVyOiBBY2NvdW50LCBhc3NldDogQXNzZXQsIGFtb3VudDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMyAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBc3NldFJlY2VpdmVyCiAgICBmcmFtZV9kaWcgLTEKICAgIGl0eG5fZmllbGQgQXNzZXRBbW91bnQKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBYZmVyQXNzZXQKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTgKICAgIC8vIGl0eG4uQXNzZXRUcmFuc2ZlcigKICAgIGludGNfMyAvLyBheGZlcgogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaW50Y18wIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzU4LTM2MgogICAgLy8gaXR4bi5Bc3NldFRyYW5zZmVyKAogICAgLy8gICAgIHhmZXJfYXNzZXQ9YXNzZXQsCiAgICAvLyAgICAgYXNzZXRfYW1vdW50PWFtb3VudCwKICAgIC8vICAgICBhc3NldF9yZWNlaXZlcj1yZWNlaXZlciwKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLm1pbnQoYV94ZmVyOiB1aW50NjQsIGJfeGZlcjogdWludDY0LCBwb29sX2Fzc2V0OiB1aW50NjQsIGFfYXNzZXQ6IHVpbnQ2NCwgYl9hc3NldDogdWludDY0KSAtPiB2b2lkOgptaW50OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjgxLTk1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgInBvb2xfYXNzZXQiOiAicG9vbF90b2tlbiIsCiAgICAvLyAgICAgICAgICJhX2Fzc2V0IjogImFzc2V0X2EiLAogICAgLy8gICAgICAgICAiYl9hc3NldCI6ICJhc3NldF9iIiwKICAgIC8vICAgICB9LAogICAgLy8gKQogICAgLy8gZGVmIG1pbnQoCiAgICAvLyAgICAgc2VsZiwKICAgIC8vICAgICBhX3hmZXI6IGd0eG4uQXNzZXRUcmFuc2ZlclRyYW5zYWN0aW9uLAogICAgLy8gICAgIGJfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgcG9vbF9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byA1IDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTEzLTExNAogICAgLy8gIyB3ZWxsLWZvcm1lZCBtaW50CiAgICAvLyBhc3NlcnQgcG9vbF9hc3NldCA9PSBzZWxmLnBvb2xfdG9rZW4sICJhc3NldCBwb29sIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICBmcmFtZV9kaWcgLTMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMTUKICAgIC8vIGFzc2VydCBhX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzAgLy8gImFzc2V0X2EiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYSBleGlzdHMKICAgIGZyYW1lX2RpZyAtMgogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBhIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExNgogICAgLy8gYXNzZXJ0IGJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgZnJhbWVfZGlnIC0xCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGIgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTE3CiAgICAvLyBhc3NlcnQgYV94ZmVyLnNlbmRlciA9PSBUeG4uc2VuZGVyLCAic2VuZGVyIGludmFsaWQiCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIFNlbmRlcgogICAgdHhuIFNlbmRlcgogICAgPT0KICAgIGFzc2VydCAvLyBzZW5kZXIgaW52YWxpZAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjExOAogICAgLy8gYXNzZXJ0IGJfeGZlci5zZW5kZXIgPT0gVHhuLnNlbmRlciwgInNlbmRlciBpbnZhbGlkIgogICAgZnJhbWVfZGlnIC00CiAgICBndHhucyBTZW5kZXIKICAgIHR4biBTZW5kZXIKICAgID09CiAgICBhc3NlcnQgLy8gc2VuZGVyIGludmFsaWQKICAgIC8vIGFtbS9jb250cmFjdC5weToxMjIKICAgIC8vIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICBmcmFtZV9kaWcgLTUKICAgIGd0eG5zIEFzc2V0UmVjZWl2ZXIKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICA9PQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyMC0xMjMKICAgIC8vICMgdmFsaWQgYXNzZXQgYSB4ZmVyCiAgICAvLyBhc3NlcnQgKAogICAgLy8gICAgIGFfeGZlci5hc3NldF9yZWNlaXZlciA9PSBHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzCiAgICAvLyApLCAicmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzIgogICAgYXNzZXJ0IC8vIHJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyNAogICAgLy8gYXNzZXJ0IGFfeGZlci54ZmVyX2Fzc2V0ID09IHNlbGYuYXNzZXRfYSwgImFzc2V0IGEgaW5jb3JyZWN0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBYZmVyQXNzZXQKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IGEgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI1CiAgICAvLyBhc3NlcnQgYV94ZmVyLmFzc2V0X2Ftb3VudCA+IDAsICJhbW91bnQgbWluaW11bSBub3QgbWV0IgogICAgZnJhbWVfZGlnIC01CiAgICBndHhucyBBc3NldEFtb3VudAogICAgZHVwbiAyCiAgICBhc3NlcnQgLy8gYW1vdW50IG1pbmltdW0gbm90IG1ldAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEyOQogICAgLy8gYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTI3LTEzMAogICAgLy8gIyB2YWxpZCBhc3NldCBiIHhmZXIKICAgIC8vIGFzc2VydCAoCiAgICAvLyAgICAgYl94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIC8vICksICJyZWNlaXZlciBub3QgYXBwIGFkZHJlc3MiCiAgICBhc3NlcnQgLy8gcmVjZWl2ZXIgbm90IGFwcCBhZGRyZXNzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTMxCiAgICAvLyBhc3NlcnQgYl94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5hc3NldF9iLCAiYXNzZXQgYiBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYiBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzIKICAgIC8vIGFzc2VydCBiX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM1CiAgICAvLyBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICBzd2FwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTM2CiAgICAvLyBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weToxMzcKICAgIC8vIGJfYmFsYW5jZT1zZWxmLl9jdXJyZW50X2JfYmFsYW5jZSgpLAogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIGNvdmVyIDIKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzEKICAgIC8vIGlzX2luaXRpYWxfbWludCA9IGFfYmFsYW5jZSA9PSBhX2Ftb3VudCBhbmQgYl9iYWxhbmNlID09IGJfYW1vdW50CiAgICA9PQogICAgYnogbWludF9ib29sX2ZhbHNlQDQKICAgIGZyYW1lX2RpZyA2CiAgICBmcmFtZV9kaWcgMwogICAgPT0KICAgIGJ6IG1pbnRfYm9vbF9mYWxzZUA0CiAgICBpbnRjXzEgLy8gMQoKbWludF9ib29sX21lcmdlQDU6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzMyCiAgICAvLyBpZiBpc19pbml0aWFsX21pbnQ6CiAgICBieiBtaW50X2FmdGVyX2lmX2Vsc2VANwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzMwogICAgLy8gcmV0dXJuIG9wLnNxcnQoYV9hbW91bnQgKiBiX2Ftb3VudCkgLSBTQ0FMRQogICAgZnJhbWVfZGlnIDIKICAgIGZyYW1lX2RpZyAzCiAgICAqCiAgICBzcXJ0CiAgICBpbnRjXzIgLy8gMTAwMAogICAgLQoKbWludF9hZnRlcl9pbmxpbmVkX2V4YW1wbGVzLmFtbS5jb250cmFjdC50b2tlbnNfdG9fbWludEAxMDoKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDEKICAgIC8vIGFzc2VydCB0b19taW50ID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQzLTE0NAogICAgLy8gIyBtaW50IHRva2VucwogICAgLy8gZG9fYXNzZXRfdHJhbnNmZXIocmVjZWl2ZXI9VHhuLnNlbmRlciwgYXNzZXQ9c2VsZi5wb29sX3Rva2VuLCBhbW91bnQ9dG9fbWludCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18yIC8vICJwb29sX3Rva2VuIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLnBvb2xfdG9rZW4gZXhpc3RzCiAgICB1bmNvdmVyIDIKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToxNDUKICAgIC8vIHNlbGYuX3VwZGF0ZV9yYXRpbygpCiAgICBjYWxsc3ViIF91cGRhdGVfcmF0aW8KICAgIHJldHN1YgoKbWludF9hZnRlcl9pZl9lbHNlQDc6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM0CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgZnJhbWVfZGlnIDQKICAgIC0KICAgIC8vIGFtbS9jb250cmFjdC5weTozMzUKICAgIC8vIGFfcmF0aW8gPSBTQ0FMRSAqIGFfYW1vdW50IC8vIChhX2JhbGFuY2UgLSBhX2Ftb3VudCkKICAgIGludGNfMiAvLyAxMDAwCiAgICBmcmFtZV9kaWcgMgogICAgZHVwCiAgICBjb3ZlciAyCiAgICAqCiAgICBmcmFtZV9kaWcgNQogICAgdW5jb3ZlciAyCiAgICAtCiAgICAvCiAgICBkdXAKICAgIGZyYW1lX2J1cnkgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjMzNgogICAgLy8gYl9yYXRpbyA9IFNDQUxFICogYl9hbW91bnQgLy8gKGJfYmFsYW5jZSAtIGJfYW1vdW50KQogICAgaW50Y18yIC8vIDEwMDAKICAgIGZyYW1lX2RpZyAzCiAgICBkdXAKICAgIGNvdmVyIDIKICAgICoKICAgIGZyYW1lX2RpZyA2CiAgICB1bmNvdmVyIDIKICAgIC0KICAgIC8KICAgIGR1cAogICAgZnJhbWVfYnVyeSAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzM3CiAgICAvLyBpZiBhX3JhdGlvIDwgYl9yYXRpbzoKICAgIDwKICAgIGJ6IG1pbnRfZWxzZV9ib2R5QDkKICAgIC8vIGFtbS9jb250cmFjdC5weTozMzgKICAgIC8vIHJldHVybiBhX3JhdGlvICogaXNzdWVkIC8vIFNDQUxFCiAgICBmcmFtZV9kaWcgMAogICAgKgogICAgaW50Y18yIC8vIDEwMDAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToxMzQtMTQwCiAgICAvLyB0b19taW50ID0gdG9rZW5zX3RvX21pbnQoCiAgICAvLyAgICAgcG9vbF9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCksCiAgICAvLyAgICAgYl9iYWxhbmNlPXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICAvLyAgICAgYV9hbW91bnQ9YV94ZmVyLmFzc2V0X2Ftb3VudCwKICAgIC8vICAgICBiX2Ftb3VudD1iX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gKQogICAgYiBtaW50X2FmdGVyX2lubGluZWRfZXhhbXBsZXMuYW1tLmNvbnRyYWN0LnRva2Vuc190b19taW50QDEwCgptaW50X2Vsc2VfYm9keUA5OgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0MAogICAgLy8gcmV0dXJuIGJfcmF0aW8gKiBpc3N1ZWQgLy8gU0NBTEUKICAgIGZyYW1lX2RpZyAxCiAgICAqCiAgICBpbnRjXzIgLy8gMTAwMAogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjEzNC0xNDAKICAgIC8vIHRvX21pbnQgPSB0b2tlbnNfdG9fbWludCgKICAgIC8vICAgICBwb29sX2JhbGFuY2U9c2VsZi5fY3VycmVudF9wb29sX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2JhbGFuY2U9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIC8vICAgICBiX2JhbGFuY2U9c2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKSwKICAgIC8vICAgICBhX2Ftb3VudD1hX3hmZXIuYXNzZXRfYW1vdW50LAogICAgLy8gICAgIGJfYW1vdW50PWJfeGZlci5hc3NldF9hbW91bnQsCiAgICAvLyApCiAgICBiIG1pbnRfYWZ0ZXJfaW5saW5lZF9leGFtcGxlcy5hbW0uY29udHJhY3QudG9rZW5zX3RvX21pbnRAMTAKCm1pbnRfYm9vbF9mYWxzZUA0OgogICAgaW50Y18wIC8vIDAKICAgIGIgbWludF9ib29sX21lcmdlQDUKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl9jdXJyZW50X3Bvb2xfYmFsYW5jZSgpIC0+IHVpbnQ2NDoKX2N1cnJlbnRfcG9vbF9iYWxhbmNlOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MS0yOTIKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX2N1cnJlbnRfcG9vbF9iYWxhbmNlKHNlbGYpIC0+IFVJbnQ2NDoKICAgIHByb3RvIDAgMQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI5MwogICAgLy8gcmV0dXJuIHNlbGYucG9vbF90b2tlbi5iYWxhbmNlKEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MpCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2V0X2hvbGRpbmdfZ2V0IEFzc2V0QmFsYW5jZQogICAgYXNzZXJ0IC8vIGFjY291bnQgb3B0ZWQgaW50byBhc3NldAogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5fY3VycmVudF9hX2JhbGFuY2UoKSAtPiB1aW50NjQ6Cl9jdXJyZW50X2FfYmFsYW5jZToKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTUtMjk2CiAgICAvLyBAc3Vicm91dGluZQogICAgLy8gZGVmIF9jdXJyZW50X2FfYmFsYW5jZShzZWxmKSAtPiBVSW50NjQ6CiAgICBwcm90byAwIDEKICAgIC8vIGFtbS9jb250cmFjdC5weToyOTcKICAgIC8vIHJldHVybiBzZWxmLmFzc2V0X2EuYmFsYW5jZShHbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzKQogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKICAgIGFzc2VydCAvLyBhY2NvdW50IG9wdGVkIGludG8gYXNzZXQKICAgIHJldHN1YgoKCi8vIGV4YW1wbGVzLmFtbS5jb250cmFjdC5Db25zdGFudFByb2R1Y3RBTU0uX2N1cnJlbnRfYl9iYWxhbmNlKCkgLT4gdWludDY0OgpfY3VycmVudF9iX2JhbGFuY2U6CiAgICAvLyBhbW0vY29udHJhY3QucHk6Mjk5LTMwMAogICAgLy8gQHN1YnJvdXRpbmUKICAgIC8vIGRlZiBfY3VycmVudF9iX2JhbGFuY2Uoc2VsZikgLT4gVUludDY0OgogICAgcHJvdG8gMCAxCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzAxCiAgICAvLyByZXR1cm4gc2VsZi5hc3NldF9iLmJhbGFuY2UoR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcykKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMSAvLyAiYXNzZXRfYiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwogICAgYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCiAgICBhc3NlcnQgLy8gYWNjb3VudCBvcHRlZCBpbnRvIGFzc2V0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLl91cGRhdGVfcmF0aW8oKSAtPiB2b2lkOgpfdXBkYXRlX3JhdGlvOgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1NS0yNTYKICAgIC8vIEBzdWJyb3V0aW5lCiAgICAvLyBkZWYgX3VwZGF0ZV9yYXRpbyhzZWxmKSAtPiBOb25lOgogICAgcHJvdG8gMCAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjU3CiAgICAvLyBhX2JhbGFuY2UgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1OAogICAgLy8gYl9iYWxhbmNlID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyNjAKICAgIC8vIHNlbGYucmF0aW8gPSBhX2JhbGFuY2UgKiBTQ0FMRSAvLyBiX2JhbGFuY2UKICAgIHN3YXAKICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICAvCiAgICBieXRlYyA0IC8vICJyYXRpbyIKICAgIHN3YXAKICAgIGFwcF9nbG9iYWxfcHV0CiAgICByZXRzdWIKCgovLyBleGFtcGxlcy5hbW0uY29udHJhY3QuQ29uc3RhbnRQcm9kdWN0QU1NLmJ1cm4ocG9vbF94ZmVyOiB1aW50NjQsIHBvb2xfYXNzZXQ6IHVpbnQ2NCwgYV9hc3NldDogdWludDY0LCBiX2Fzc2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm46CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTQ3LTE2MAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKAogICAgLy8gICAgIGRlZmF1bHRfYXJncz17CiAgICAvLyAgICAgICAgICJwb29sX2Fzc2V0IjogInBvb2xfdG9rZW4iLAogICAgLy8gICAgICAgICAiYV9hc3NldCI6ICJhc3NldF9hIiwKICAgIC8vICAgICAgICAgImJfYXNzZXQiOiAiYXNzZXRfYiIsCiAgICAvLyAgICAgfSwKICAgIC8vICkKICAgIC8vIGRlZiBidXJuKAogICAgLy8gICAgIHNlbGYsCiAgICAvLyAgICAgcG9vbF94ZmVyOiBndHhuLkFzc2V0VHJhbnNmZXJUcmFuc2FjdGlvbiwKICAgIC8vICAgICBwb29sX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBhX2Fzc2V0OiBBc3NldCwKICAgIC8vICAgICBiX2Fzc2V0OiBBc3NldCwKICAgIC8vICkgLT4gTm9uZToKICAgIHByb3RvIDQgMAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI1MwogICAgLy8gYXNzZXJ0IHNlbGYucG9vbF90b2tlbiwgImJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgIGFzc2VydCAvLyBib290c3RyYXAgbWV0aG9kIG5lZWRzIHRvIGJlIGNhbGxlZCBmaXJzdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3MgogICAgLy8gYXNzZXJ0IHBvb2xfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgZnJhbWVfZGlnIC0zCiAgICA9PQogICAgYXNzZXJ0IC8vIGFzc2V0IHBvb2wgaW5jb3JyZWN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTczCiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzQKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE3NwogICAgLy8gcG9vbF94ZmVyLmFzc2V0X3JlY2VpdmVyID09IEdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgQXNzZXRSZWNlaXZlcgogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgID09CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTc2LTE3OAogICAgLy8gYXNzZXJ0ICgKICAgIC8vICAgICBwb29sX3hmZXIuYXNzZXRfcmVjZWl2ZXIgPT0gR2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcwogICAgLy8gKSwgInJlY2VpdmVyIG5vdCBhcHAgYWRkcmVzcyIKICAgIGFzc2VydCAvLyByZWNlaXZlciBub3QgYXBwIGFkZHJlc3MKICAgIC8vIGFtbS9jb250cmFjdC5weToxNzkKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuYXNzZXRfYW1vdW50ID4gMCwgImFtb3VudCBtaW5pbXVtIG5vdCBtZXQiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIEFzc2V0QW1vdW50CiAgICBkdXAKICAgIGFzc2VydCAvLyBhbW91bnQgbWluaW11bSBub3QgbWV0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgwCiAgICAvLyBhc3NlcnQgcG9vbF94ZmVyLnhmZXJfYXNzZXQgPT0gc2VsZi5wb29sX3Rva2VuLCAiYXNzZXQgcG9vbCBpbmNvcnJlY3QiCiAgICBmcmFtZV9kaWcgLTQKICAgIGd0eG5zIFhmZXJBc3NldAogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzIgLy8gInBvb2xfdG9rZW4iCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYucG9vbF90b2tlbiBleGlzdHMKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgcG9vbCBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToxODEKICAgIC8vIGFzc2VydCBwb29sX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtNAogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTgzLTE4NQogICAgLy8gIyBHZXQgdGhlIHRvdGFsIG51bWJlciBvZiB0b2tlbnMgaXNzdWVkCiAgICAvLyAjICFpbXBvcnRhbnQ6IHRoaXMgaGFwcGVucyBwcmlvciB0byByZWNlaXZpbmcgdGhlIGN1cnJlbnQgYXhmZXIgb2YgcG9vbCB0b2tlbnMKICAgIC8vIHBvb2xfYmFsYW5jZSA9IHNlbGYuX2N1cnJlbnRfcG9vbF9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfcG9vbF9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTg4CiAgICAvLyBzdXBwbHk9c2VsZi5fY3VycmVudF9hX2JhbGFuY2UoKSwKICAgIGNhbGxzdWIgX2N1cnJlbnRfYV9iYWxhbmNlCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzQ1CiAgICAvLyBpc3N1ZWQgPSBUT1RBTF9TVVBQTFkgLSBwb29sX2JhbGFuY2UgLSBhbW91bnQKICAgIGludGMgNCAvLyAxMDAwMDAwMDAwMAogICAgdW5jb3ZlciAyCiAgICAtCiAgICBkaWcgMgogICAgLQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHN3YXAKICAgIGRpZyAyCiAgICAqCiAgICBkaWcgMQogICAgLwogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjE5MwogICAgLy8gc3VwcGx5PXNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCksCiAgICBjYWxsc3ViIF9jdXJyZW50X2JfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjM0NgogICAgLy8gcmV0dXJuIHN1cHBseSAqIGFtb3VudCAvLyBpc3N1ZWQKICAgIHVuY292ZXIgMwogICAgKgogICAgdW5jb3ZlciAyCiAgICAvCiAgICAvLyBhbW0vY29udHJhY3QucHk6MTk3LTE5OAogICAgLy8gIyBTZW5kIGJhY2sgY29tbWVuc3VyYXRlIGFtdCBvZiBhCiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1zZWxmLmFzc2V0X2EsIGFtb3VudD1hX2FtdCkKICAgIHR4biBTZW5kZXIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICB1bmNvdmVyIDMKICAgIGNhbGxzdWIgZG9fYXNzZXRfdHJhbnNmZXIKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDAtMjAxCiAgICAvLyAjIFNlbmQgYmFjayBjb21tZW5zdXJhdGUgYW10IG9mIGIKICAgIC8vIGRvX2Fzc2V0X3RyYW5zZmVyKHJlY2VpdmVyPVR4bi5zZW5kZXIsIGFzc2V0PXNlbGYuYXNzZXRfYiwgYW1vdW50PWJfYW10KQogICAgdHhuIFNlbmRlcgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIwMgogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgoKLy8gZXhhbXBsZXMuYW1tLmNvbnRyYWN0LkNvbnN0YW50UHJvZHVjdEFNTS5zd2FwKHN3YXBfeGZlcjogdWludDY0LCBhX2Fzc2V0OiB1aW50NjQsIGJfYXNzZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKc3dhcDoKICAgIC8vIGFtbS9jb250cmFjdC5weToyMDQtMjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoCiAgICAvLyAgICAgZGVmYXVsdF9hcmdzPXsKICAgIC8vICAgICAgICAgImFfYXNzZXQiOiAiYXNzZXRfYSIsCiAgICAvLyAgICAgICAgICJiX2Fzc2V0IjogImFzc2V0X2IiLAogICAgLy8gICAgIH0sCiAgICAvLyApCiAgICAvLyBkZWYgc3dhcCgKICAgIC8vICAgICBzZWxmLAogICAgLy8gICAgIHN3YXBfeGZlcjogZ3R4bi5Bc3NldFRyYW5zZmVyVHJhbnNhY3Rpb24sCiAgICAvLyAgICAgYV9hc3NldDogQXNzZXQsCiAgICAvLyAgICAgYl9hc3NldDogQXNzZXQsCiAgICAvLyApIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIHB1c2hieXRlcyAiIgogICAgZHVwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjUzCiAgICAvLyBhc3NlcnQgc2VsZi5wb29sX3Rva2VuLCAiYm9vdHN0cmFwIG1ldGhvZCBuZWVkcyB0byBiZSBjYWxsZWQgZmlyc3QiCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMiAvLyAicG9vbF90b2tlbiIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5wb29sX3Rva2VuIGV4aXN0cwogICAgYXNzZXJ0IC8vIGJvb3RzdHJhcCBtZXRob2QgbmVlZHMgdG8gYmUgY2FsbGVkIGZpcnN0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjI1CiAgICAvLyBhc3NlcnQgYV9hc3NldCA9PSBzZWxmLmFzc2V0X2EsICJhc3NldCBhIGluY29ycmVjdCIKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBmcmFtZV9kaWcgLTIKICAgID09CiAgICBhc3NlcnQgLy8gYXNzZXQgYSBpbmNvcnJlY3QKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjYKICAgIC8vIGFzc2VydCBiX2Fzc2V0ID09IHNlbGYuYXNzZXRfYiwgImFzc2V0IGIgaW5jb3JyZWN0IgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgYXNzZXJ0IC8vIGNoZWNrIHNlbGYuYXNzZXRfYiBleGlzdHMKICAgIGZyYW1lX2RpZyAtMQogICAgPT0KICAgIGFzc2VydCAvLyBhc3NldCBiIGluY29ycmVjdAogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIyOAogICAgLy8gYXNzZXJ0IHN3YXBfeGZlci5hc3NldF9hbW91bnQgPiAwLCAiYW1vdW50IG1pbmltdW0gbm90IG1ldCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgQXNzZXRBbW91bnQKICAgIGR1cAogICAgYXNzZXJ0IC8vIGFtb3VudCBtaW5pbXVtIG5vdCBtZXQKICAgIC8vIGFtbS9jb250cmFjdC5weToyMjkKICAgIC8vIGFzc2VydCBzd2FwX3hmZXIuc2VuZGVyID09IFR4bi5zZW5kZXIsICJzZW5kZXIgaW52YWxpZCIKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgU2VuZGVyCiAgICB0eG4gU2VuZGVyCiAgICA9PQogICAgYXNzZXJ0IC8vIHNlbmRlciBpbnZhbGlkCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMyCiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYToKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18wIC8vICJhc3NldF9hIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM2CiAgICAvLyBjYXNlIHNlbGYuYXNzZXRfYjoKICAgIGludGNfMCAvLyAwCiAgICBieXRlY18xIC8vICJhc3NldF9iIgogICAgYXBwX2dsb2JhbF9nZXRfZXgKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2IgZXhpc3RzCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxCiAgICAvLyBtYXRjaCBzd2FwX3hmZXIueGZlcl9hc3NldDoKICAgIGZyYW1lX2RpZyAtMwogICAgZ3R4bnMgWGZlckFzc2V0CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjMxLTI0MQogICAgLy8gbWF0Y2ggc3dhcF94ZmVyLnhmZXJfYXNzZXQ6CiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2E6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICAvLyAgICAgY2FzZSBzZWxmLmFzc2V0X2I6CiAgICAvLyAgICAgICAgIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYV9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIC8vICAgICAgICAgb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9iCiAgICAvLyAgICAgY2FzZSBfOgogICAgLy8gICAgICAgICBhc3NlcnQgRmFsc2UsICJhc3NldCBpZCBpbmNvcnJlY3QiCiAgICBtYXRjaCBzd2FwX3N3aXRjaF9jYXNlXzBAMSBzd2FwX3N3aXRjaF9jYXNlXzFAMgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0MQogICAgLy8gYXNzZXJ0IEZhbHNlLCAiYXNzZXQgaWQgaW5jb3JyZWN0IgogICAgZXJyIC8vIGFzc2V0IGlkIGluY29ycmVjdAoKc3dhcF9zd2l0Y2hfY2FzZV8xQDI6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM3CiAgICAvLyBpbl9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgZnJhbWVfYnVyeSAwCiAgICAvLyBhbW0vY29udHJhY3QucHk6MjM4CiAgICAvLyBvdXRfc3VwcGx5ID0gc2VsZi5fY3VycmVudF9iX2JhbGFuY2UoKQogICAgY2FsbHN1YiBfY3VycmVudF9iX2JhbGFuY2UKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzkKICAgIC8vIG91dF9hc3NldCA9IHNlbGYuYXNzZXRfYgogICAgaW50Y18wIC8vIDAKICAgIGJ5dGVjXzEgLy8gImFzc2V0X2IiCiAgICBhcHBfZ2xvYmFsX2dldF9leAogICAgc3dhcAogICAgZnJhbWVfYnVyeSAxCiAgICBhc3NlcnQgLy8gY2hlY2sgc2VsZi5hc3NldF9iIGV4aXN0cwoKc3dhcF9zd2l0Y2hfY2FzZV9uZXh0QDQ6CiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUxCiAgICAvLyBpbl90b3RhbCA9IFNDQUxFICogKGluX3N1cHBseSAtIGluX2Ftb3VudCkgKyAoaW5fYW1vdW50ICogRkFDVE9SKQogICAgZnJhbWVfZGlnIDAKICAgIGZyYW1lX2RpZyAyCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIC0KICAgIGludGNfMiAvLyAxMDAwCiAgICAqCiAgICBzd2FwCiAgICBwdXNoaW50IDk5NSAvLyA5OTUKICAgICoKICAgIHN3YXAKICAgIGRpZyAxCiAgICArCiAgICAvLyBhbW0vY29udHJhY3QucHk6MzUyCiAgICAvLyBvdXRfdG90YWwgPSBpbl9hbW91bnQgKiBGQUNUT1IgKiBvdXRfc3VwcGx5CiAgICBzd2FwCiAgICB1bmNvdmVyIDIKICAgICoKICAgIC8vIGFtbS9jb250cmFjdC5weTozNTMKICAgIC8vIHJldHVybiBvdXRfdG90YWwgLy8gaW5fdG90YWwKICAgIHN3YXAKICAgIC8KICAgIC8vIGFtbS9jb250cmFjdC5weToyNDYKICAgIC8vIGFzc2VydCB0b19zd2FwID4gMCwgInNlbmQgYW1vdW50IHRvbyBsb3ciCiAgICBkdXAKICAgIGFzc2VydCAvLyBzZW5kIGFtb3VudCB0b28gbG93CiAgICAvLyBhbW0vY29udHJhY3QucHk6MjQ4CiAgICAvLyBkb19hc3NldF90cmFuc2ZlcihyZWNlaXZlcj1UeG4uc2VuZGVyLCBhc3NldD1vdXRfYXNzZXQsIGFtb3VudD10b19zd2FwKQogICAgdHhuIFNlbmRlcgogICAgZnJhbWVfZGlnIDEKICAgIHVuY292ZXIgMgogICAgY2FsbHN1YiBkb19hc3NldF90cmFuc2ZlcgogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjI0OQogICAgLy8gc2VsZi5fdXBkYXRlX3JhdGlvKCkKICAgIGNhbGxzdWIgX3VwZGF0ZV9yYXRpbwogICAgcmV0c3ViCgpzd2FwX3N3aXRjaF9jYXNlXzBAMToKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzMKICAgIC8vIGluX3N1cHBseSA9IHNlbGYuX2N1cnJlbnRfYl9iYWxhbmNlKCkKICAgIGNhbGxzdWIgX2N1cnJlbnRfYl9iYWxhbmNlCiAgICBmcmFtZV9idXJ5IDAKICAgIC8vIGFtbS9jb250cmFjdC5weToyMzQKICAgIC8vIG91dF9zdXBwbHkgPSBzZWxmLl9jdXJyZW50X2FfYmFsYW5jZSgpCiAgICBjYWxsc3ViIF9jdXJyZW50X2FfYmFsYW5jZQogICAgLy8gYW1tL2NvbnRyYWN0LnB5OjIzNQogICAgLy8gb3V0X2Fzc2V0ID0gc2VsZi5hc3NldF9hCiAgICBpbnRjXzAgLy8gMAogICAgYnl0ZWNfMCAvLyAiYXNzZXRfYSIKICAgIGFwcF9nbG9iYWxfZ2V0X2V4CiAgICBzd2FwCiAgICBmcmFtZV9idXJ5IDEKICAgIGFzc2VydCAvLyBjaGVjayBzZWxmLmFzc2V0X2EgZXhpc3RzCiAgICBiIHN3YXBfc3dpdGNoX2Nhc2VfbmV4dEA0Cg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "byteCode": { + "approval": "CiAFAAHoBwSAyK+gJSYFB2Fzc2V0X2EHYXNzZXRfYgpwb29sX3Rva2VuCGdvdmVybm9yBXJhdGlvMRhAABEoImcpImcrMQBnKiJnJwQiZzEbQQDnggUECKlW9wRrWdllBFy/Hi0EFDbCrARKiOBVNhoAjgUAqwB/AEwAJAACIkMxGRREMRhEMRYjCUk4ECUSRDYaARfAMDYaAhfAMIgC7yNDMRkURDEYRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAk8jQzEZFEQxGEQxFoECCUk4ECUSRDEWIwlJOBAlEkQ2GgEXwDA2GgIXwDA2GgMXwDCIAQcjQzEZFEQxGEQxFiMJSTgQIxJENhoBF8AwNhoCF8AwiABAFoAEFR98dUxQsCNDMRkURDEYRDYaARfAHIgADSNDMRlA/z4xGBREI0OKAQCIAAUri/9niYoAADEAIitlRBJEiYoDASIqZUQURIj/6DIEgQISRIv9OAcyChJEi/04CIHgpxIPRIv+i/8MRCiL/mcpi/9nsSIoZURxA0SABERQVC1MUIABLVAiKWVEcQNEUDIKSbIqsimBA7IjIQSyIoADZGJ0siWyJoEDshAisgGzKrQ8ZyIoZUQyCkwiiAAQIillRDIKTCKIAAUiKmVEiYoDALGL/bIUi/+yEov+shElshAisgGziYoFAIAASSIqZUREIiplRIv9EkQiKGVEi/4SRCIpZUSL/xJEi/s4ADEAEkSL/DgAMQASRIv7OBQyChJEi/s4ESIoZUQSRIv7OBJHAkSL/DgUMgoSRIv8OBEiKWVEEkSL/DgSSU4CRIgAckyIAHtJTgKIAIJOAhJBAF6LBosDEkEAViNBABmLAosDC5IkCUlEMQAiKmVETwKI/06IAGWJIQSLBAkkiwJJTgILiwVPAgkKSYwAJIsDSU4CC4sGTwIJCkmMAQxBAAiLAAskCkL/vosBCyQKQv+2IkL/p4oAATIKIiplRHAARImKAAEyCiIoZURwAESJigABMgoiKWVEcABEiYoAAIj/4Ij/6kwkC0wKJwRMZ4mKBAAiKmVERCIqZUSL/RJEIihlRIv+EkQiKWVEi/8SRIv8OBQyChJEi/w4EklEi/w4ESIqZUQSRIv8OAAxABJEiP+DiP+NIQRPAglLAglMSwILSwEKiP+ITwMLTwIKMQAiKGVETwOI/moxACIpZURPAoj+X4j/domKAwCAAEkiKmVERCIoZUSL/hJEIillRIv/EkSL/TgSSUSL/TgAMQASRCIoZUQiKWVEi/04EY4CADgAAQCI/xyMAIj/JCIpZUyMAUSLAIsCSU4CCSQLTIHjBwtMSwEITE8CC0wKSUQxAIsBTwKI/eyI/wOJiP7yjACI/uAiKGVMjAFEQv/G", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 99, + "minor": 99, + "patch": 99 + } + }, + "events": [], + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/artifacts/hello_world/app_spec.arc32.json b/tests/artifacts/hello_world/app_spec.arc32.json new file mode 100644 index 00000000..d84bc32c --- /dev/null +++ b/tests/artifacts/hello_world/app_spec.arc32.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/artifacts/hello_world/approval.teal b/tests/artifacts/hello_world/approval.teal new file mode 100644 index 00000000..d38f6432 --- /dev/null +++ b/tests/artifacts/hello_world/approval.teal @@ -0,0 +1,62 @@ +#pragma version 10 + +smart_contracts.hello_world.contract.HelloWorld.approval_program: + intcblock 0 1 + callsub __puya_arc4_router__ + return + + +// smart_contracts.hello_world.contract.HelloWorld.__puya_arc4_router__() -> uint64: +__puya_arc4_router__: + proto 0 1 + txn NumAppArgs + bz __puya_arc4_router___bare_routing@5 + pushbytes 0x02bece11 // method "hello(string)string" + txna ApplicationArgs 0 + match __puya_arc4_router___hello_route@2 + intc_0 // 0 + retsub + +__puya_arc4_router___hello_route@2: + txn OnCompletion + ! + assert // OnCompletion is NoOp + txn ApplicationID + assert // is not creating + txna ApplicationArgs 1 + extract 2 0 + callsub hello + dup + len + itob + extract 6 2 + swap + concat + pushbytes 0x151f7c75 + swap + concat + log + intc_1 // 1 + retsub + +__puya_arc4_router___bare_routing@5: + txn OnCompletion + bnz __puya_arc4_router___after_if_else@9 + txn ApplicationID + ! + assert // is creating + intc_1 // 1 + retsub + +__puya_arc4_router___after_if_else@9: + intc_0 // 0 + retsub + + +// smart_contracts.hello_world.contract.HelloWorld.hello(name: bytes) -> bytes: +hello: + proto 1 1 + pushbytes "Hello, " + frame_dig -1 + concat + retsub diff --git a/tests/artifacts/hello_world/clear.teal b/tests/artifacts/hello_world/clear.teal new file mode 100644 index 00000000..5a70c80b --- /dev/null +++ b/tests/artifacts/hello_world/clear.teal @@ -0,0 +1,5 @@ +#pragma version 10 + +smart_contracts.hello_world.contract.HelloWorld.clear_state_program: + pushint 1 // 1 + return diff --git a/tests/artifacts/inner-fee/application.json b/tests/artifacts/inner-fee/application.json new file mode 100644 index 00000000..e124cbd4 --- /dev/null +++ b/tests/artifacts/inner-fee/application.json @@ -0,0 +1,202 @@ +{ + "name": "InnerFeeContract", + "structs": {}, + "methods": [ + { + "name": "burn_ops", + "args": [ + { + "type": "uint64", + "name": "op_budget" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "no_op", + "args": [], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_x_inners_with_fees", + "args": [ + { + "type": "uint64", + "name": "app_id" + }, + { + "type": "uint64[]", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_inners_with_fees", + "args": [ + { + "type": "uint64", + "name": "app_id_1" + }, + { + "type": "uint64", + "name": "app_id_2" + }, + { + "type": "(uint64,uint64,uint64,uint64,uint64[])", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + }, + { + "name": "send_inners_with_fees_2", + "args": [ + { + "type": "uint64", + "name": "app_id_1" + }, + { + "type": "uint64", + "name": "app_id_2" + }, + { + "type": "(uint64,uint64,uint64[],uint64,uint64,uint64[])", + "name": "fees" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 0, + "bytes": 0 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 75, + 91, + 100, + 119, + 142 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 170 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 78, + 94, + 103, + 122, + 145 + ], + "errorMessage": "can only call when not creating" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmFwcHJvdmFsX3Byb2dyYW06CiAgICBpbnRjYmxvY2sgMSA2IDAgOAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHgwNjgxMDEKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgcHJvdG8gMCAxCiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDkKICAgIHB1c2hieXRlcyAweGRkMzc4MjQ3IC8vIG1ldGhvZCAiYnVybl9vcHModWludDY0KXZvaWQiCiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBwdXNoYnl0ZXNzIDB4MzQzNjgyY2QgMHgxY2YyZjU5MCAvLyBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0LCh1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjQsdWludDY0W10pKXZvaWQiLCBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0W10sdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIF9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDIgX19wdXlhX2FyYzRfcm91dGVyX19fbm9fb3Bfcm91dGVAMyBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDYKICAgIGludGNfMiAvLyAwCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIGJ1cm5fb3BzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19ub19vcF9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxOAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfcm91dGVANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX2lubmVyc193aXRoX2ZlZXMKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDY6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A5OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTMKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDEzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgaW50Y18yIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHMob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NS02CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBidXJuX29wcyhzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6Ny04CiAgICAvLyAjIFVzZXMgYXBwcm94IDYwIG9wIGJ1ZGdldCBwZXIgaXRlcmF0aW9uCiAgICAvLyBjb3VudCA9IG9wX2J1ZGdldCAvLyA2MAogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDYwIC8vIDYwCiAgICAvCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo5CiAgICAvLyBlbnN1cmVfYnVkZ2V0KG9wX2J1ZGdldCkKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18yIC8vIDAKICAgIGNhbGxzdWIgZW5zdXJlX2J1ZGdldAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBpbnRjXzIgLy8gMAoKYnVybl9vcHNfZm9yX2hlYWRlckAxOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IGJ1cm5fb3BzX2FmdGVyX2ZvckA0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMQogICAgLy8gc3FydCA9IG9wLmJzcXJ0KEJpZ1VJbnQoaSkpCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBpdG9iCiAgICBic3FydAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTIKICAgIC8vIGFzc2VydChzcXJ0ID49IDApICMgUHJldmVudCBvcHRpbWlzZXIgcmVtb3ZpbmcgdGhlIHNxcnQKICAgIHB1c2hieXRlcyAweAogICAgYj49CiAgICBhc3NlcnQKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjEwCiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBidXJuX29wc19mb3JfaGVhZGVyQDEKCmJ1cm5fb3BzX2FmdGVyX2ZvckA0OgogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLnRlc3RfY29udHJhY3QuY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWQ6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgtMTkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZDogVUludDY0LCBmZWVzOiBhcmM0LkR5bmFtaWNBcnJheVthcmM0LlVJbnQ2NF0pIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMiAvLyAwCiAgICBleHRyYWN0X3VpbnQxNgogICAgaW50Y18yIC8vIDAKCnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2Zvcl9oZWFkZXJAMToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1CiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMiAwCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBjb3ZlciAyCiAgICBpbnRjXzMgLy8gOAogICAgKgogICAgaW50Y18zIC8vIDgKICAgIGV4dHJhY3QzIC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjEKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZCwgZmVlPWZlZS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIGludGNfMCAvLyAxCiAgICArCiAgICBmcmFtZV9idXJ5IDEKICAgIGIgc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxCgpzZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19hZnRlcl9mb3JANToKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyMy0yNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2VuZF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZF8xOiBVSW50NjQsIGFwcF9pZF8yOiBVSW50NjQsIGZlZXM6IGFyYzQuVHVwbGVbYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI1CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjYKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozMAogICAgLy8gZmVlPWZlZXNbMl0ubmF0aXZlCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI5CiAgICAvLyByZWNlaXZlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGl0eG5fZmllbGQgUmVjZWl2ZXIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI4CiAgICAvLyBhbW91bnQ9MCwKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEFtb3VudAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjcKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzIKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNF0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbM10ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI0IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0b2IKICAgIGZyYW1lX2RpZyAtMQogICAgcHVzaGludCAzMiAvLyAzMgogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgc3dhcAogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXNfMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0LTM1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXNfMihzZWxmLCBhcHBfaWRfMTogVUludDY0LCBhcHBfaWRfMjogVUludDY0LCBmZWVzOiBhcmM0LlR1cGxlW2FyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdLCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM2CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzcKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbMl0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMV0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDE2IC8vIDE2CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDM0IC8vIDM0CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDIKICAgIGRpZyAyCiAgICBzdWJzdHJpbmczCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMSAvLyBtZXRob2QgInNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjRbXSl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGRpZyAyCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzgKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzkKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNV0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbNF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI2IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTEKICAgIGxlbgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDMKICAgIHVuY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgcmV0c3ViCgoKLy8gX3B1eWFfbGliLnV0aWwuZW5zdXJlX2J1ZGdldChyZXF1aXJlZF9idWRnZXQ6IHVpbnQ2NCwgZmVlX3NvdXJjZTogdWludDY0KSAtPiB2b2lkOgplbnN1cmVfYnVkZ2V0OgogICAgcHJvdG8gMiAwCiAgICBmcmFtZV9kaWcgLTIKICAgIHB1c2hpbnQgMTAgLy8gMTAKICAgICsKCmVuc3VyZV9idWRnZXRfd2hpbGVfdG9wQDE6CiAgICBmcmFtZV9kaWcgMAogICAgZ2xvYmFsIE9wY29kZUJ1ZGdldAogICAgPgogICAgYnogZW5zdXJlX2J1ZGdldF9hZnRlcl93aGlsZUA3CiAgICBpdHhuX2JlZ2luCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgcHVzaGludCA1IC8vIERlbGV0ZUFwcGxpY2F0aW9uCiAgICBpdHhuX2ZpZWxkIE9uQ29tcGxldGlvbgogICAgYnl0ZWNfMiAvLyAweDA2ODEwMQogICAgaXR4bl9maWVsZCBBcHByb3ZhbFByb2dyYW0KICAgIGJ5dGVjXzIgLy8gMHgwNjgxMDEKICAgIGl0eG5fZmllbGQgQ2xlYXJTdGF0ZVByb2dyYW0KICAgIGZyYW1lX2RpZyAtMQogICAgc3dpdGNoIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMEAzIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMUA0CiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMzoKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgYiBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANgoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV8xQDQ6CiAgICBnbG9iYWwgTWluVHhuRmVlCiAgICBpdHhuX2ZpZWxkIEZlZQoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDY6CiAgICBpdHhuX3N1Ym1pdAogICAgYiBlbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxCgplbnN1cmVfYnVkZ2V0X2FmdGVyX3doaWxlQDc6CiAgICByZXRzdWIK", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmNsZWFyX3N0YXRlX3Byb2dyYW06CiAgICBwdXNoaW50IDEgLy8gMQogICAgcmV0dXJuCg==" + }, + "events": [], + "templateVariables": {} +} diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py new file mode 100644 index 00000000..bdc1bf4f --- /dev/null +++ b/tests/artifacts/inner-fee/contract.py @@ -0,0 +1,62 @@ +from algopy import ( + ARC4Contract, + BigUInt, + Global, + UInt64, + arc4, + ensure_budget, + itxn, + op, + urange, +) + + +class InnerFeeContract(ARC4Contract): + @arc4.abimethod + def burn_ops(self, op_budget: UInt64) -> None: + # Uses approx 60 op budget per iteration + count = op_budget // 60 + ensure_budget(op_budget) + for i in urange(count): + sqrt = op.bsqrt(BigUInt(i)) + assert sqrt >= 0 # Prevent optimiser removing the sqrt + + @arc4.abimethod + def no_op(self) -> None: + pass + + @arc4.abimethod + def send_x_inners_with_fees(self, app_id: UInt64, fees: arc4.DynamicArray[arc4.UInt64]) -> None: + for fee in fees: + arc4.abi_call("no_op", app_id=app_id, fee=fee.native) + + @arc4.abimethod + def send_inners_with_fees( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[1].native) + itxn.Payment(amount=0, receiver=Global.current_application_address, fee=fees[2].native).submit() + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) + + @arc4.abimethod + def send_inners_with_fees_2( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[ + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + ], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[3].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) diff --git a/tests/app_client_test.json b/tests/artifacts/legacy_app_client_test/app_client_test.json similarity index 100% rename from tests/app_client_test.json rename to tests/artifacts/legacy_app_client_test/app_client_test.json diff --git a/tests/artifacts/legacy_app_client_test/app_client_test.py b/tests/artifacts/legacy_app_client_test/app_client_test.py new file mode 100644 index 00000000..34e04358 --- /dev/null +++ b/tests/artifacts/legacy_app_client_test/app_client_test.py @@ -0,0 +1,199 @@ +from typing import Literal + +import beaker +import pyteal +from beaker.lib.storage import BoxMapping + + +class State: + greeting = beaker.GlobalStateValue(pyteal.TealType.bytes) + last = beaker.LocalStateValue(pyteal.TealType.bytes, default=pyteal.Bytes("unset")) + box = BoxMapping(pyteal.abi.StaticBytes[Literal[4]], pyteal.abi.String) + + +app = beaker.Application("HelloWorldApp", state=State()) + + +@app.external +def version(*, output: pyteal.abi.Uint64) -> pyteal.Expr: + return output.set(pyteal.Tmpl.Int("TMPL_VERSION")) + + +@app.external(read_only=True) +def readonly(error: pyteal.abi.Uint64) -> pyteal.Expr: + return pyteal.If(error.get(), pyteal.Assert(pyteal.Int(0), comment="An error"), pyteal.Approve()) + + +@app.external() +def set_box(name: pyteal.abi.StaticBytes[Literal[4]], value: pyteal.abi.String) -> pyteal.Expr: + return app.state.box[name.get()].set(value.get()) + + +@app.external +def get_box(name: pyteal.abi.StaticBytes[Literal[4]], *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(app.state.box[name.get()].get()) + + +@app.external(read_only=True) +def get_box_readonly(name: pyteal.abi.StaticBytes[Literal[4]], *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(app.state.box[name.get()].get()) + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated ABI")), + ) + + +@app.update(bare=True, authorize=beaker.Authorize.only_creator()) +def update_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated Bare")), + ) + + +@app.update(authorize=beaker.Authorize.only_creator()) +def update_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes update check"), + pyteal.Assert(pyteal.Tmpl.Int("TMPL_UPDATABLE"), comment="is updatable"), + app.state.greeting.set(pyteal.Bytes("Updated Args")), + ) + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.delete(bare=True, authorize=beaker.Authorize.only_creator()) +def delete_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.delete(authorize=beaker.Authorize.only_creator()) +def delete_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes delete check"), + pyteal.Assert(pyteal.Tmpl.Int("TMPL_DELETABLE"), comment="is deletable"), + ) + + +@app.external(method_config={"opt_in": pyteal.CallConfig.CREATE}) +def create_opt_in() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Opt In")), + pyteal.Approve(), + ) + + +@app.external +def update_greeting(greeting: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(greeting.get()), + ) + + +@app.create(bare=True) +def create_bare() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Hello Bare")), + pyteal.Approve(), + ) + + +@app.create +def create() -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(pyteal.Bytes("Hello ABI")), + pyteal.Approve(), + ) + + +@app.create +def create_args(greeting: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.greeting.set(greeting.get()), + pyteal.Approve(), + ) + + +@app.external(read_only=True) +def hello(name: pyteal.abi.String, *, output: pyteal.abi.String) -> pyteal.Expr: + return output.set(pyteal.Concat(app.state.greeting, pyteal.Bytes(", "), name.get())) + + +@app.external +def hello_remember(name: pyteal.abi.String, *, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(name.get()), output.set(pyteal.Concat(app.state.greeting, pyteal.Bytes(", "), name.get())) + ) + + +@app.external(read_only=True) +def get_last(*, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq(output.set(app.state.last.get())) + + +@app.clear_state +def clear_state() -> pyteal.Expr: + return pyteal.Approve() + + +@app.opt_in +def opt_in() -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(pyteal.Bytes("Opt In ABI")), + pyteal.Approve(), + ) + + +@app.opt_in(bare=True) +def opt_in_bare() -> pyteal.Expr: + return pyteal.Seq( + app.state.last.set(pyteal.Bytes("Opt In Bare")), + pyteal.Approve(), + ) + + +@app.opt_in +def opt_in_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes opt_in check"), + app.state.last.set(pyteal.Bytes("Opt In Args")), + pyteal.Approve(), + ) + + +@app.close_out +def close_out() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Approve(), + ) + + +@app.close_out(bare=True) +def close_out_bare() -> pyteal.Expr: + return pyteal.Seq( + pyteal.Approve(), + ) + + +@app.close_out +def close_out_args(check: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq( + pyteal.Assert(pyteal.Eq(check.get(), pyteal.Bytes("Yes")), comment="passes close_out check"), + pyteal.Approve(), + ) + + +@app.external +def call_with_payment(payment: pyteal.abi.PaymentTransaction, *, output: pyteal.abi.String) -> pyteal.Expr: + return pyteal.Seq(pyteal.Assert(payment.get().amount() > pyteal.Int(0)), output.set("Payment Successful")) diff --git a/tests/artifacts/legacy_hello_world/app_spec.arc32.json b/tests/artifacts/legacy_hello_world/app_spec.arc32.json new file mode 100644 index 00000000..1ddf81b2 --- /dev/null +++ b/tests/artifacts/legacy_hello_world/app_spec.arc32.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/app_algorand_client.json b/tests/artifacts/nested_contract/application.json similarity index 99% rename from tests/app_algorand_client.json rename to tests/artifacts/nested_contract/application.json index de1411cc..1d96d437 100644 --- a/tests/app_algorand_client.json +++ b/tests/artifacts/nested_contract/application.json @@ -176,4 +176,4 @@ } ] } -} \ No newline at end of file +} diff --git a/tests/artifacts/resource-packer/.gitignore b/tests/artifacts/resource-packer/.gitignore new file mode 100644 index 00000000..edd15a59 --- /dev/null +++ b/tests/artifacts/resource-packer/.gitignore @@ -0,0 +1,3 @@ +*.teal +*.json +!*.arc32.json diff --git a/tests/artifacts/resource-packer/ExternalApp.arc32.json b/tests/artifacts/resource-packer/ExternalApp.arc32.json new file mode 100644 index 00000000..88424a70 --- /dev/null +++ b/tests/artifacts/resource-packer/ExternalApp.arc32.json @@ -0,0 +1,140 @@ +{ + "hints": { + "optInToApplication()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "dummy()void": { + "call_config": { + "no_op": "CALL" + } + }, + "error()void": { + "call_config": { + "no_op": "CALL" + } + }, + "boxWithPayment(pay)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createAsset()void": { + "call_config": { + "no_op": "CALL" + } + }, + "senderAssetBalance()void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": { + "localKey": { + "type": "bytes", + "key": "localKey" + } + }, + "reserved": {} + }, + "global": { + "declared": { + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 1 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgovLyBUaGlzIFRFQUwgd2FzIGdlbmVyYXRlZCBieSBURUFMU2NyaXB0IHYwLjg3LjAKLy8gaHR0cHM6Ly9naXRodWIuY29tL2FsZ29yYW5kZm91bmRhdGlvbi9URUFMU2NyaXB0CgovLyBUaGlzIGNvbnRyYWN0IGlzIGNvbXBsaWFudCB3aXRoIGFuZC9vciBpbXBsZW1lbnRzIHRoZSBmb2xsb3dpbmcgQVJDczogWyBBUkM0IF0KCi8vIFRoZSBmb2xsb3dpbmcgdGVuIGxpbmVzIG9mIFRFQUwgaGFuZGxlIGluaXRpYWwgcHJvZ3JhbSBmbG93Ci8vIFRoaXMgcGF0dGVybiBpcyB1c2VkIHRvIG1ha2UgaXQgZWFzeSBmb3IgYW55b25lIHRvIHBhcnNlIHRoZSBzdGFydCBvZiB0aGUgcHJvZ3JhbSBhbmQgZGV0ZXJtaW5lIGlmIGEgc3BlY2lmaWMgYWN0aW9uIGlzIGFsbG93ZWQKLy8gSGVyZSwgYWN0aW9uIHJlZmVycyB0byB0aGUgT25Db21wbGV0ZSBpbiBjb21iaW5hdGlvbiB3aXRoIHdoZXRoZXIgdGhlIGFwcCBpcyBiZWluZyBjcmVhdGVkIG9yIGNhbGxlZAovLyBFdmVyeSBwb3NzaWJsZSBhY3Rpb24gZm9yIHRoaXMgY29udHJhY3QgaXMgcmVwcmVzZW50ZWQgaW4gdGhlIHN3aXRjaCBzdGF0ZW1lbnQKLy8gSWYgdGhlIGFjdGlvbiBpcyBub3QgaW1wbGVtZW50ZWQgaW4gdGhlIGNvbnRyYWN0LCBpdHMgcmVzcGVjdGl2ZSBicmFuY2ggd2lsbCBiZSAiKk5PVF9JTVBMRU1FTlRFRCIgd2hpY2gganVzdCBjb250YWlucyAiZXJyIgp0eG4gQXBwbGljYXRpb25JRAohCmludCA2CioKdHhuIE9uQ29tcGxldGlvbgorCnN3aXRjaCAqY2FsbF9Ob09wICpjYWxsX09wdEluICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKmNyZWF0ZV9Ob09wICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRAoKKk5PVF9JTVBMRU1FTlRFRDoKCWVycgoKLy8gb3B0SW5Ub0FwcGxpY2F0aW9uKCl2b2lkCiphYmlfcm91dGVfb3B0SW5Ub0FwcGxpY2F0aW9uOgoJLy8gZXhlY3V0ZSBvcHRJblRvQXBwbGljYXRpb24oKXZvaWQKCWNhbGxzdWIgb3B0SW5Ub0FwcGxpY2F0aW9uCglpbnQgMQoJcmV0dXJuCgovLyBvcHRJblRvQXBwbGljYXRpb24oKTogdm9pZApvcHRJblRvQXBwbGljYXRpb246Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTIKCS8vIHRoaXMubG9jYWxLZXkodGhpcy50eG4uc2VuZGVyKS52YWx1ZSA9ICdmb28nCgl0eG4gU2VuZGVyCglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglieXRlIDB4NjY2ZjZmIC8vICJmb28iCglhcHBfbG9jYWxfcHV0CglyZXRzdWIKCi8vIGR1bW15KCl2b2lkCiphYmlfcm91dGVfZHVtbXk6CgkvLyBleGVjdXRlIGR1bW15KCl2b2lkCgljYWxsc3ViIGR1bW15CglpbnQgMQoJcmV0dXJuCgovLyBkdW1teSgpOiB2b2lkCmR1bW15OgoJcHJvdG8gMCAwCglyZXRzdWIKCi8vIGVycm9yKCl2b2lkCiphYmlfcm91dGVfZXJyb3I6CgkvLyBleGVjdXRlIGVycm9yKCl2b2lkCgljYWxsc3ViIGVycm9yCglpbnQgMQoJcmV0dXJuCgovLyBlcnJvcigpOiB2b2lkCmVycm9yOgoJcHJvdG8gMCAwCgllcnIKCi8vIGJveFdpdGhQYXltZW50KHBheSl2b2lkCiphYmlfcm91dGVfYm94V2l0aFBheW1lbnQ6CgkvLyBfcGF5bWVudDogcGF5Cgl0eG4gR3JvdXBJbmRleAoJaW50IDEKCS0KCWR1cAoJZ3R4bnMgVHlwZUVudW0KCWludCBwYXkKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGJveFdpdGhQYXltZW50KHBheSl2b2lkCgljYWxsc3ViIGJveFdpdGhQYXltZW50CglpbnQgMQoJcmV0dXJuCgovLyBib3hXaXRoUGF5bWVudChfcGF5bWVudDogUGF5VHhuKTogdm9pZApib3hXaXRoUGF5bWVudDoKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoyMgoJLy8gdGhpcy5ib3hLZXkudmFsdWUgPSAnZm9vJwoJYnl0ZSAweDYyNmY3ODRiNjU3OSAvLyAiYm94S2V5IgoJZHVwCglib3hfZGVsCglwb3AKCWJ5dGUgMHg2NjZmNmYgLy8gImZvbyIKCWJveF9wdXQKCXJldHN1YgoKLy8gY3JlYXRlQXNzZXQoKXZvaWQKKmFiaV9yb3V0ZV9jcmVhdGVBc3NldDoKCS8vIGV4ZWN1dGUgY3JlYXRlQXNzZXQoKXZvaWQKCWNhbGxzdWIgY3JlYXRlQXNzZXQKCWludCAxCglyZXR1cm4KCi8vIGNyZWF0ZUFzc2V0KCk6IHZvaWQKY3JlYXRlQXNzZXQ6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MjYKCS8vIHRoaXMuYXNhLnZhbHVlID0gc2VuZEFzc2V0Q3JlYXRpb24oewoJLy8gICAgICAgY29uZmlnQXNzZXRUb3RhbDogMSwKCS8vICAgICB9KQoJYnl0ZSAweDYxNzM2MSAvLyAiYXNhIgoJaXR4bl9iZWdpbgoJaW50IGFjZmcKCWl0eG5fZmllbGQgVHlwZUVudW0KCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MjcKCS8vIGNvbmZpZ0Fzc2V0VG90YWw6IDEKCWludCAxCglpdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VG90YWwKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglpdHhuIENyZWF0ZWRBc3NldElECglhcHBfZ2xvYmFsX3B1dAoJcmV0c3ViCgovLyBzZW5kZXJBc3NldEJhbGFuY2UoKXZvaWQKKmFiaV9yb3V0ZV9zZW5kZXJBc3NldEJhbGFuY2U6CgkvLyBleGVjdXRlIHNlbmRlckFzc2V0QmFsYW5jZSgpdm9pZAoJY2FsbHN1YiBzZW5kZXJBc3NldEJhbGFuY2UKCWludCAxCglyZXR1cm4KCi8vIHNlbmRlckFzc2V0QmFsYW5jZSgpOiB2b2lkCnNlbmRlckFzc2V0QmFsYW5jZToKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czozMgoJLy8gYXNzZXJ0KCF0aGlzLnR4bi5zZW5kZXIuaXNPcHRlZEluVG9Bc3NldCh0aGlzLmFzYS52YWx1ZSkpCgl0eG4gU2VuZGVyCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCglzd2FwCglwb3AKCSEKCWFzc2VydAoJcmV0c3ViCgoqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uOgoJaW50IDEKCXJldHVybgoKKmNyZWF0ZV9Ob09wOgoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfY3JlYXRlQXBwbGljYXRpb24KCWVycgoKKmNhbGxfTm9PcDoKCW1ldGhvZCAiZHVtbXkoKXZvaWQiCgltZXRob2QgImVycm9yKCl2b2lkIgoJbWV0aG9kICJib3hXaXRoUGF5bWVudChwYXkpdm9pZCIKCW1ldGhvZCAiY3JlYXRlQXNzZXQoKXZvaWQiCgltZXRob2QgInNlbmRlckFzc2V0QmFsYW5jZSgpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfZHVtbXkgKmFiaV9yb3V0ZV9lcnJvciAqYWJpX3JvdXRlX2JveFdpdGhQYXltZW50ICphYmlfcm91dGVfY3JlYXRlQXNzZXQgKmFiaV9yb3V0ZV9zZW5kZXJBc3NldEJhbGFuY2UKCWVycgoKKmNhbGxfT3B0SW46CgltZXRob2QgIm9wdEluVG9BcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoICphYmlfcm91dGVfb3B0SW5Ub0FwcGxpY2F0aW9uCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEw" + }, + "contract": { + "name": "ExternalApp", + "desc": "", + "methods": [ + { + "name": "optInToApplication", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "dummy", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "error", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "boxWithPayment", + "args": [ + { + "name": "_payment", + "type": "pay" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createAsset", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "senderAssetBalance", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ExternalAppV8.arc32.json b/tests/artifacts/resource-packer/ExternalAppV8.arc32.json new file mode 100644 index 00000000..d8f07b6b --- /dev/null +++ b/tests/artifacts/resource-packer/ExternalAppV8.arc32.json @@ -0,0 +1,69 @@ +{ + "hints": { + "dummy()void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": {}, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDkKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuNjMuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsbWVudGVkIGluIHRoZSBjb250cmFjdCwgaXRzIHJlcHNlY3RpdmUgYnJhbmNoIHdpbGwgYmUgIk5PVF9JTVBMTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECmludCAwCj4KaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoIGNyZWF0ZV9Ob09wIE5PVF9JTVBMRU1FTlRFRCBOT1RfSU1QTEVNRU5URUQgTk9UX0lNUExFTUVOVEVEIE5PVF9JTVBMRU1FTlRFRCBOT1RfSU1QTEVNRU5URUQgY2FsbF9Ob09wCgpOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGR1bW15KCl2b2lkCmFiaV9yb3V0ZV9kdW1teToKCS8vIGV4ZWN1dGUgZHVtbXkoKXZvaWQKCWNhbGxzdWIgZHVtbXkKCWludCAxCglyZXR1cm4KCmR1bW15OgoJcHJvdG8gMCAwCglyZXRzdWIKCmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCmNyZWF0ZV9Ob09wOgoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCXR4bmEgQXBwbGljYXRpb25BcmdzIDAKCW1hdGNoIGFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbgoJZXJyCgpjYWxsX05vT3A6CgltZXRob2QgImR1bW15KCl2b2lkIgoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAoJbWF0Y2ggYWJpX3JvdXRlX2R1bW15CgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDk=" + }, + "contract": { + "name": "ExternalAppV8", + "desc": "", + "methods": [ + { + "name": "dummy", + "args": [], + "desc": "", + "returns": { + "type": "void", + "desc": "" + } + }, + { + "name": "createApplication", + "desc": "", + "returns": { + "type": "void", + "desc": "" + }, + "args": [] + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json b/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json new file mode 100644 index 00000000..d5afe92f --- /dev/null +++ b/tests/artifacts/resource-packer/ResourcePackerv8.arc32.json @@ -0,0 +1,173 @@ +{ + "hints": { + "bootstrap()void": { + "call_config": { + "no_op": "CALL" + } + }, + "addressBalance(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "smallBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "mediumBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalAppCall()void": { + "call_config": { + "no_op": "CALL" + } + }, + "assetTotal()void": { + "call_config": { + "no_op": "CALL" + } + }, + "hasAsset(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalLocal(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": { + "externalAppID": { + "type": "uint64", + "key": "externalAppID" + }, + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 2 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuODcuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoICpjYWxsX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpjcmVhdGVfTm9PcCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQKCipOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGJvb3RzdHJhcCgpdm9pZAoqYWJpX3JvdXRlX2Jvb3RzdHJhcDoKCS8vIGV4ZWN1dGUgYm9vdHN0cmFwKCl2b2lkCgljYWxsc3ViIGJvb3RzdHJhcAoJaW50IDEKCXJldHVybgoKLy8gYm9vdHN0cmFwKCk6IHZvaWQKYm9vdHN0cmFwOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjQ5CgkvLyBzZW5kTWV0aG9kQ2FsbDxbXSwgdm9pZD4oewoJLy8gICAgICAgbmFtZTogJ2NyZWF0ZUFwcGxpY2F0aW9uJywKCS8vICAgICAgIGFwcHJvdmFsUHJvZ3JhbTogRXh0ZXJuYWxBcHAuYXBwcm92YWxQcm9ncmFtKCksCgkvLyAgICAgICBjbGVhclN0YXRlUHJvZ3JhbTogRXh0ZXJuYWxBcHAuY2xlYXJQcm9ncmFtKCksCgkvLyAgICAgICBsb2NhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmxvY2FsLm51bUJ5dGVTbGljZSwKCS8vICAgICAgIGdsb2JhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1CeXRlU2xpY2UsCgkvLyAgICAgICBnbG9iYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEuZ2xvYmFsLm51bVVpbnQsCgkvLyAgICAgICBsb2NhbE51bVVpbnQ6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1VaW50LAoJLy8gICAgIH0pCglpdHhuX2JlZ2luCglpbnQgYXBwbAoJaXR4bl9maWVsZCBUeXBlRW51bQoJbWV0aG9kICJjcmVhdGVBcHBsaWNhdGlvbigpdm9pZCIKCWl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjUxCgkvLyBhcHByb3ZhbFByb2dyYW06IEV4dGVybmFsQXBwLmFwcHJvdmFsUHJvZ3JhbSgpCglieXRlIGI2NCBDaUFCQVNZQ0EyWnZid05oYzJFeEdCU0JCZ3N4R1FpTkRBQ0hBTFVBQUFBQUFBQUFBQUI1QUFBQUFBQUFBQUFBQUFDSUFBSWlRNG9BQURFQWdBaHNiMk5oYkV0bGVTaG1pWWdBQWlKRGlnQUFpWWdBQWlKRGlnQUFBREVXSWdsSk9CQWlFa1NJQUFJaVE0b0JBSUFHWW05NFMyVjVTYnhJS0wrSmlBQUNJa09LQUFBcHNZRURzaEFpc2lLQkFMSUJzN1E4WjRtSUFBSWlRNG9BQURFQUtXUndBRXhJRkVTSklrT0FCTGhFZXpZMkdnQ09BZi94QUlBRW93em4vNEFFUk5EYURZQUUxaDVDVllBRXBscXIvb0FFWlZ4ZUFqWWFBSTRGLzJUL2JmOTIvNWIvc0FDQUJBR2pvLzgyR2dDT0FmOC9BQT09CglpdHhuX2ZpZWxkIEFwcHJvdmFsUHJvZ3JhbQoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo1MgoJLy8gY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpCglieXRlIGI2NCBDZz09CglpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjUzCgkvLyBsb2NhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmxvY2FsLm51bUJ5dGVTbGljZQoJaW50IDEKCWl0eG5fZmllbGQgTG9jYWxOdW1CeXRlU2xpY2UKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6NTQKCS8vIGdsb2JhbE51bUJ5dGVTbGljZTogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1CeXRlU2xpY2UKCWludCAwCglpdHhuX2ZpZWxkIEdsb2JhbE51bUJ5dGVTbGljZQoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo1NQoJLy8gZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50CglpbnQgMQoJaXR4bl9maWVsZCBHbG9iYWxOdW1VaW50CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjU2CgkvLyBsb2NhbE51bVVpbnQ6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1VaW50CglpbnQgMAoJaXR4bl9maWVsZCBMb2NhbE51bVVpbnQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjU5CgkvLyB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUgPSB0aGlzLml0eG4uY3JlYXRlZEFwcGxpY2F0aW9uSUQKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWl0eG4gQ3JlYXRlZEFwcGxpY2F0aW9uSUQKCWFwcF9nbG9iYWxfcHV0CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjYxCgkvLyB0aGlzLmFzYS52YWx1ZSA9IHNlbmRBc3NldENyZWF0aW9uKHsKCS8vICAgICAgIGNvbmZpZ0Fzc2V0VG90YWw6IDEsCgkvLyAgICAgfSkKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWl0eG5fYmVnaW4KCWludCBhY2ZnCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjYyCgkvLyBjb25maWdBc3NldFRvdGFsOiAxCglpbnQgMQoJaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCgoJLy8gRmVlIGZpZWxkIG5vdCBzZXQsIGRlZmF1bHRpbmcgdG8gMAoJaW50IDAKCWl0eG5fZmllbGQgRmVlCgoJLy8gU3VibWl0IGlubmVyIHRyYW5zYWN0aW9uCglpdHhuX3N1Ym1pdAoJaXR4biBDcmVhdGVkQXNzZXRJRAoJYXBwX2dsb2JhbF9wdXQKCXJldHN1YgoKLy8gYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfYWRkcmVzc0JhbGFuY2U6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBhZGRyZXNzQmFsYW5jZShhZGRyZXNzKXZvaWQKCWNhbGxzdWIgYWRkcmVzc0JhbGFuY2UKCWludCAxCglyZXR1cm4KCi8vIGFkZHJlc3NCYWxhbmNlKGFkZHI6IEFkZHJlc3MpOiB2b2lkCmFkZHJlc3NCYWxhbmNlOgoJcHJvdG8gMSAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjY3CgkvLyBsb2cocmF3Qnl0ZXMoYWRkci5pc0luTGVkZ2VyKSkKCWZyYW1lX2RpZyAtMSAvLyBhZGRyOiBBZGRyZXNzCglhY2N0X3BhcmFtc19nZXQgQWNjdEJhbGFuY2UKCXN3YXAKCXBvcAoJYnl0ZSAweDAwCglpbnQgMAoJdW5jb3ZlciAyCglzZXRiaXQKCWxvZwoJcmV0c3ViCgovLyBzbWFsbEJveCgpdm9pZAoqYWJpX3JvdXRlX3NtYWxsQm94OgoJLy8gZXhlY3V0ZSBzbWFsbEJveCgpdm9pZAoJY2FsbHN1YiBzbWFsbEJveAoJaW50IDEKCXJldHVybgoKLy8gc21hbGxCb3goKTogdm9pZApzbWFsbEJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo3MQoJLy8gdGhpcy5zbWFsbEJveEtleS52YWx1ZSA9ICcnCglieXRlIDB4NzMgLy8gInMiCglkdXAKCWJveF9kZWwKCXBvcAoJYnl0ZSAweCAvLyAiIgoJYm94X3B1dAoJcmV0c3ViCgovLyBtZWRpdW1Cb3goKXZvaWQKKmFiaV9yb3V0ZV9tZWRpdW1Cb3g6CgkvLyBleGVjdXRlIG1lZGl1bUJveCgpdm9pZAoJY2FsbHN1YiBtZWRpdW1Cb3gKCWludCAxCglyZXR1cm4KCi8vIG1lZGl1bUJveCgpOiB2b2lkCm1lZGl1bUJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czo3NQoJLy8gdGhpcy5tZWRpdW1Cb3hLZXkuY3JlYXRlKDVfMDAwKQoJYnl0ZSAweDZkIC8vICJtIgoJaW50IDVfMDAwCglib3hfY3JlYXRlCglwb3AKCXJldHN1YgoKLy8gZXh0ZXJuYWxBcHBDYWxsKCl2b2lkCiphYmlfcm91dGVfZXh0ZXJuYWxBcHBDYWxsOgoJLy8gZXhlY3V0ZSBleHRlcm5hbEFwcENhbGwoKXZvaWQKCWNhbGxzdWIgZXh0ZXJuYWxBcHBDYWxsCglpbnQgMQoJcmV0dXJuCgovLyBleHRlcm5hbEFwcENhbGwoKTogdm9pZApleHRlcm5hbEFwcENhbGw6Cglwcm90byAwIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6NzkKCS8vIHNlbmRNZXRob2RDYWxsPFtdLCB2b2lkPih7CgkvLyAgICAgICBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUsCgkvLyAgICAgICBuYW1lOiAnZHVtbXknLAoJLy8gICAgIH0pCglpdHhuX2JlZ2luCglpbnQgYXBwbAoJaXR4bl9maWVsZCBUeXBlRW51bQoJbWV0aG9kICJkdW1teSgpdm9pZCIKCWl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjgwCgkvLyBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglyZXRzdWIKCi8vIGFzc2V0VG90YWwoKXZvaWQKKmFiaV9yb3V0ZV9hc3NldFRvdGFsOgoJLy8gZXhlY3V0ZSBhc3NldFRvdGFsKCl2b2lkCgljYWxsc3ViIGFzc2V0VG90YWwKCWludCAxCglyZXR1cm4KCi8vIGFzc2V0VG90YWwoKTogdm9pZAphc3NldFRvdGFsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjg2CgkvLyBhc3NlcnQodGhpcy5hc2EudmFsdWUudG90YWwpCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfcGFyYW1zX2dldCBBc3NldFRvdGFsCglwb3AKCWFzc2VydAoJcmV0c3ViCgovLyBoYXNBc3NldChhZGRyZXNzKXZvaWQKKmFiaV9yb3V0ZV9oYXNBc3NldDoKCS8vIGFkZHI6IGFkZHJlc3MKCXR4bmEgQXBwbGljYXRpb25BcmdzIDEKCWR1cAoJbGVuCglpbnQgMzIKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGhhc0Fzc2V0KGFkZHJlc3Mpdm9pZAoJY2FsbHN1YiBoYXNBc3NldAoJaW50IDEKCXJldHVybgoKLy8gaGFzQXNzZXQoYWRkcjogQWRkcmVzcyk6IHZvaWQKaGFzQXNzZXQ6Cglwcm90byAxIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6OTAKCS8vIGFzc2VydCghYWRkci5pc09wdGVkSW5Ub0Fzc2V0KHRoaXMuYXNhLnZhbHVlKSkKCWZyYW1lX2RpZyAtMSAvLyBhZGRyOiBBZGRyZXNzCglieXRlIDB4NjE3MzYxIC8vICJhc2EiCglhcHBfZ2xvYmFsX2dldAoJYXNzZXRfaG9sZGluZ19nZXQgQXNzZXRCYWxhbmNlCglzd2FwCglwb3AKCSEKCWFzc2VydAoJcmV0c3ViCgovLyBleHRlcm5hbExvY2FsKGFkZHJlc3Mpdm9pZAoqYWJpX3JvdXRlX2V4dGVybmFsTG9jYWw6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBleHRlcm5hbExvY2FsKGFkZHJlc3Mpdm9pZAoJY2FsbHN1YiBleHRlcm5hbExvY2FsCglpbnQgMQoJcmV0dXJuCgovLyBleHRlcm5hbExvY2FsKGFkZHI6IEFkZHJlc3MpOiB2b2lkCmV4dGVybmFsTG9jYWw6Cglwcm90byAxIDAKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6OTQKCS8vIGxvZyh0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUubG9jYWxTdGF0ZShhZGRyLCAnbG9jYWxLZXknKSBhcyBieXRlcykKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglmcmFtZV9kaWcgLTEgLy8gYWRkcjogQWRkcmVzcwoJY292ZXIgMgoJYXBwX2xvY2FsX2dldF9leAoJYXNzZXJ0Cglsb2cKCXJldHN1YgoKKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCipjcmVhdGVfTm9PcDoKCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uCgllcnIKCipjYWxsX05vT3A6CgltZXRob2QgImJvb3RzdHJhcCgpdm9pZCIKCW1ldGhvZCAiYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkIgoJbWV0aG9kICJzbWFsbEJveCgpdm9pZCIKCW1ldGhvZCAibWVkaXVtQm94KCl2b2lkIgoJbWV0aG9kICJleHRlcm5hbEFwcENhbGwoKXZvaWQiCgltZXRob2QgImFzc2V0VG90YWwoKXZvaWQiCgltZXRob2QgImhhc0Fzc2V0KGFkZHJlc3Mpdm9pZCIKCW1ldGhvZCAiZXh0ZXJuYWxMb2NhbChhZGRyZXNzKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2Jvb3RzdHJhcCAqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlICphYmlfcm91dGVfc21hbGxCb3ggKmFiaV9yb3V0ZV9tZWRpdW1Cb3ggKmFiaV9yb3V0ZV9leHRlcm5hbEFwcENhbGwgKmFiaV9yb3V0ZV9hc3NldFRvdGFsICphYmlfcm91dGVfaGFzQXNzZXQgKmFiaV9yb3V0ZV9leHRlcm5hbExvY2FsCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDg=" + }, + "contract": { + "name": "ResourcePackerv8", + "desc": "", + "methods": [ + { + "name": "bootstrap", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "addressBalance", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "smallBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "mediumBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "externalAppCall", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "assetTotal", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "hasAsset", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "externalLocal", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json b/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json new file mode 100644 index 00000000..2aa29555 --- /dev/null +++ b/tests/artifacts/resource-packer/ResourcePackerv9.arc32.json @@ -0,0 +1,173 @@ +{ + "hints": { + "bootstrap()void": { + "call_config": { + "no_op": "CALL" + } + }, + "addressBalance(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "smallBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "mediumBox()void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalAppCall()void": { + "call_config": { + "no_op": "CALL" + } + }, + "assetTotal()void": { + "call_config": { + "no_op": "CALL" + } + }, + "hasAsset(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "externalLocal(address)void": { + "call_config": { + "no_op": "CALL" + } + }, + "createApplication()void": { + "call_config": { + "no_op": "CREATE" + } + } + }, + "bare_call_config": { + "no_op": "NEVER", + "opt_in": "NEVER", + "close_out": "NEVER", + "update_application": "NEVER", + "delete_application": "NEVER" + }, + "schema": { + "local": { + "declared": {}, + "reserved": {} + }, + "global": { + "declared": { + "externalAppID": { + "type": "uint64", + "key": "externalAppID" + }, + "asa": { + "type": "uint64", + "key": "asa" + } + }, + "reserved": {} + } + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 2 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDkKCi8vIFRoaXMgVEVBTCB3YXMgZ2VuZXJhdGVkIGJ5IFRFQUxTY3JpcHQgdjAuODcuMAovLyBodHRwczovL2dpdGh1Yi5jb20vYWxnb3JhbmRmb3VuZGF0aW9uL1RFQUxTY3JpcHQKCi8vIFRoaXMgY29udHJhY3QgaXMgY29tcGxpYW50IHdpdGggYW5kL29yIGltcGxlbWVudHMgdGhlIGZvbGxvd2luZyBBUkNzOiBbIEFSQzQgXQoKLy8gVGhlIGZvbGxvd2luZyB0ZW4gbGluZXMgb2YgVEVBTCBoYW5kbGUgaW5pdGlhbCBwcm9ncmFtIGZsb3cKLy8gVGhpcyBwYXR0ZXJuIGlzIHVzZWQgdG8gbWFrZSBpdCBlYXN5IGZvciBhbnlvbmUgdG8gcGFyc2UgdGhlIHN0YXJ0IG9mIHRoZSBwcm9ncmFtIGFuZCBkZXRlcm1pbmUgaWYgYSBzcGVjaWZpYyBhY3Rpb24gaXMgYWxsb3dlZAovLyBIZXJlLCBhY3Rpb24gcmVmZXJzIHRvIHRoZSBPbkNvbXBsZXRlIGluIGNvbWJpbmF0aW9uIHdpdGggd2hldGhlciB0aGUgYXBwIGlzIGJlaW5nIGNyZWF0ZWQgb3IgY2FsbGVkCi8vIEV2ZXJ5IHBvc3NpYmxlIGFjdGlvbiBmb3IgdGhpcyBjb250cmFjdCBpcyByZXByZXNlbnRlZCBpbiB0aGUgc3dpdGNoIHN0YXRlbWVudAovLyBJZiB0aGUgYWN0aW9uIGlzIG5vdCBpbXBsZW1lbnRlZCBpbiB0aGUgY29udHJhY3QsIGl0cyByZXNwZWN0aXZlIGJyYW5jaCB3aWxsIGJlICIqTk9UX0lNUExFTUVOVEVEIiB3aGljaCBqdXN0IGNvbnRhaW5zICJlcnIiCnR4biBBcHBsaWNhdGlvbklECiEKaW50IDYKKgp0eG4gT25Db21wbGV0aW9uCisKc3dpdGNoICpjYWxsX05vT3AgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpjcmVhdGVfTm9PcCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQgKk5PVF9JTVBMRU1FTlRFRCAqTk9UX0lNUExFTUVOVEVEICpOT1RfSU1QTEVNRU5URUQKCipOT1RfSU1QTEVNRU5URUQ6CgllcnIKCi8vIGJvb3RzdHJhcCgpdm9pZAoqYWJpX3JvdXRlX2Jvb3RzdHJhcDoKCS8vIGV4ZWN1dGUgYm9vdHN0cmFwKCl2b2lkCgljYWxsc3ViIGJvb3RzdHJhcAoJaW50IDEKCXJldHVybgoKLy8gYm9vdHN0cmFwKCk6IHZvaWQKYm9vdHN0cmFwOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExMQoJLy8gc2VuZE1ldGhvZENhbGw8W10sIHZvaWQ+KHsKCS8vICAgICAgIG5hbWU6ICdjcmVhdGVBcHBsaWNhdGlvbicsCgkvLyAgICAgICBhcHByb3ZhbFByb2dyYW06IEV4dGVybmFsQXBwLmFwcHJvdmFsUHJvZ3JhbSgpLAoJLy8gICAgICAgY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpLAoJLy8gICAgICAgbG9jYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1CeXRlU2xpY2UsCgkvLyAgICAgICBnbG9iYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5nbG9iYWwubnVtQnl0ZVNsaWNlLAoJLy8gICAgICAgZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50LAoJLy8gICAgICAgbG9jYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEubG9jYWwubnVtVWludCwKCS8vICAgICB9KQoJaXR4bl9iZWdpbgoJaW50IGFwcGwKCWl0eG5fZmllbGQgVHlwZUVudW0KCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMTMKCS8vIGFwcHJvdmFsUHJvZ3JhbTogRXh0ZXJuYWxBcHAuYXBwcm92YWxQcm9ncmFtKCkKCWJ5dGUgYjY0IENpQUJBU1lDQTJadmJ3TmhjMkV4R0JTQkJnc3hHUWlOREFDSEFMVUFBQUFBQUFBQUFBQjVBQUFBQUFBQUFBQUFBQUNJQUFJaVE0b0FBREVBZ0Foc2IyTmhiRXRsZVNobWlZZ0FBaUpEaWdBQWlZZ0FBaUpEaWdBQUFERVdJZ2xKT0JBaUVrU0lBQUlpUTRvQkFJQUdZbTk0UzJWNVNieElLTCtKaUFBQ0lrT0tBQUFwc1lFRHNoQWlzaUtCQUxJQnM3UThaNG1JQUFJaVE0b0FBREVBS1dSd0FFeElGRVNKSWtPQUJMaEVlelkyR2dDT0FmL3hBSUFFb3d6bi80QUVSTkRhRFlBRTFoNUNWWUFFcGxxci9vQUVaVnhlQWpZYUFJNEYvMlQvYmY5Mi81Yi9zQUNBQkFHam8vODJHZ0NPQWY4L0FBPT0KCWl0eG5fZmllbGQgQXBwcm92YWxQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNAoJLy8gY2xlYXJTdGF0ZVByb2dyYW06IEV4dGVybmFsQXBwLmNsZWFyUHJvZ3JhbSgpCglieXRlIGI2NCBDZz09CglpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNQoJLy8gbG9jYWxOdW1CeXRlU2xpY2U6IEV4dGVybmFsQXBwLnNjaGVtYS5sb2NhbC5udW1CeXRlU2xpY2UKCWludCAxCglpdHhuX2ZpZWxkIExvY2FsTnVtQnl0ZVNsaWNlCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNgoJLy8gZ2xvYmFsTnVtQnl0ZVNsaWNlOiBFeHRlcm5hbEFwcC5zY2hlbWEuZ2xvYmFsLm51bUJ5dGVTbGljZQoJaW50IDAKCWl0eG5fZmllbGQgR2xvYmFsTnVtQnl0ZVNsaWNlCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExNwoJLy8gZ2xvYmFsTnVtVWludDogRXh0ZXJuYWxBcHAuc2NoZW1hLmdsb2JhbC5udW1VaW50CglpbnQgMQoJaXR4bl9maWVsZCBHbG9iYWxOdW1VaW50CgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjExOAoJLy8gbG9jYWxOdW1VaW50OiBFeHRlcm5hbEFwcC5zY2hlbWEubG9jYWwubnVtVWludAoJaW50IDAKCWl0eG5fZmllbGQgTG9jYWxOdW1VaW50CgoJLy8gRmVlIGZpZWxkIG5vdCBzZXQsIGRlZmF1bHRpbmcgdG8gMAoJaW50IDAKCWl0eG5fZmllbGQgRmVlCgoJLy8gU3VibWl0IGlubmVyIHRyYW5zYWN0aW9uCglpdHhuX3N1Ym1pdAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMjEKCS8vIHRoaXMuZXh0ZXJuYWxBcHBJRC52YWx1ZSA9IHRoaXMuaXR4bi5jcmVhdGVkQXBwbGljYXRpb25JRAoJYnl0ZSAweDY1Nzg3NDY1NzI2ZTYxNmM0MTcwNzA0OTQ0IC8vICJleHRlcm5hbEFwcElEIgoJaXR4biBDcmVhdGVkQXBwbGljYXRpb25JRAoJYXBwX2dsb2JhbF9wdXQKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTIzCgkvLyB0aGlzLmFzYS52YWx1ZSA9IHNlbmRBc3NldENyZWF0aW9uKHsKCS8vICAgICAgIGNvbmZpZ0Fzc2V0VG90YWw6IDEsCgkvLyAgICAgfSkKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWl0eG5fYmVnaW4KCWludCBhY2ZnCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjEyNAoJLy8gY29uZmlnQXNzZXRUb3RhbDogMQoJaW50IDEKCWl0eG5fZmllbGQgQ29uZmlnQXNzZXRUb3RhbAoKCS8vIEZlZSBmaWVsZCBub3Qgc2V0LCBkZWZhdWx0aW5nIHRvIDAKCWludCAwCglpdHhuX2ZpZWxkIEZlZQoKCS8vIFN1Ym1pdCBpbm5lciB0cmFuc2FjdGlvbgoJaXR4bl9zdWJtaXQKCWl0eG4gQ3JlYXRlZEFzc2V0SUQKCWFwcF9nbG9iYWxfcHV0CglyZXRzdWIKCi8vIGFkZHJlc3NCYWxhbmNlKGFkZHJlc3Mpdm9pZAoqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlOgoJLy8gYWRkcjogYWRkcmVzcwoJdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQoJZHVwCglsZW4KCWludCAzMgoJPT0KCWFzc2VydAoKCS8vIGV4ZWN1dGUgYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkCgljYWxsc3ViIGFkZHJlc3NCYWxhbmNlCglpbnQgMQoJcmV0dXJuCgovLyBhZGRyZXNzQmFsYW5jZShhZGRyOiBBZGRyZXNzKTogdm9pZAphZGRyZXNzQmFsYW5jZToKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMjkKCS8vIGxvZyhyYXdCeXRlcyhhZGRyLmlzSW5MZWRnZXIpKQoJZnJhbWVfZGlnIC0xIC8vIGFkZHI6IEFkZHJlc3MKCWFjY3RfcGFyYW1zX2dldCBBY2N0QmFsYW5jZQoJc3dhcAoJcG9wCglieXRlIDB4MDAKCWludCAwCgl1bmNvdmVyIDIKCXNldGJpdAoJbG9nCglyZXRzdWIKCi8vIHNtYWxsQm94KCl2b2lkCiphYmlfcm91dGVfc21hbGxCb3g6CgkvLyBleGVjdXRlIHNtYWxsQm94KCl2b2lkCgljYWxsc3ViIHNtYWxsQm94CglpbnQgMQoJcmV0dXJuCgovLyBzbWFsbEJveCgpOiB2b2lkCnNtYWxsQm94OgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjEzMwoJLy8gdGhpcy5zbWFsbEJveEtleS52YWx1ZSA9ICcnCglieXRlIDB4NzMgLy8gInMiCglkdXAKCWJveF9kZWwKCXBvcAoJYnl0ZSAweCAvLyAiIgoJYm94X3B1dAoJcmV0c3ViCgovLyBtZWRpdW1Cb3goKXZvaWQKKmFiaV9yb3V0ZV9tZWRpdW1Cb3g6CgkvLyBleGVjdXRlIG1lZGl1bUJveCgpdm9pZAoJY2FsbHN1YiBtZWRpdW1Cb3gKCWludCAxCglyZXR1cm4KCi8vIG1lZGl1bUJveCgpOiB2b2lkCm1lZGl1bUJveDoKCXByb3RvIDAgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxMzcKCS8vIHRoaXMubWVkaXVtQm94S2V5LmNyZWF0ZSg1XzAwMCkKCWJ5dGUgMHg2ZCAvLyAibSIKCWludCA1XzAwMAoJYm94X2NyZWF0ZQoJcG9wCglyZXRzdWIKCi8vIGV4dGVybmFsQXBwQ2FsbCgpdm9pZAoqYWJpX3JvdXRlX2V4dGVybmFsQXBwQ2FsbDoKCS8vIGV4ZWN1dGUgZXh0ZXJuYWxBcHBDYWxsKCl2b2lkCgljYWxsc3ViIGV4dGVybmFsQXBwQ2FsbAoJaW50IDEKCXJldHVybgoKLy8gZXh0ZXJuYWxBcHBDYWxsKCk6IHZvaWQKZXh0ZXJuYWxBcHBDYWxsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE0MQoJLy8gc2VuZE1ldGhvZENhbGw8W10sIHZvaWQ+KHsKCS8vICAgICAgIGFwcGxpY2F0aW9uSUQ6IHRoaXMuZXh0ZXJuYWxBcHBJRC52YWx1ZSwKCS8vICAgICAgIG5hbWU6ICdkdW1teScsCgkvLyAgICAgfSkKCWl0eG5fYmVnaW4KCWludCBhcHBsCglpdHhuX2ZpZWxkIFR5cGVFbnVtCgltZXRob2QgImR1bW15KCl2b2lkIgoJaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKCgkvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9yZXNvdXJjZS1wYWNrZXIvcmVzb3VyY2UtcGFja2VyLmFsZ28udHM6MTQyCgkvLyBhcHBsaWNhdGlvbklEOiB0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKCgkvLyBGZWUgZmllbGQgbm90IHNldCwgZGVmYXVsdGluZyB0byAwCglpbnQgMAoJaXR4bl9maWVsZCBGZWUKCgkvLyBTdWJtaXQgaW5uZXIgdHJhbnNhY3Rpb24KCWl0eG5fc3VibWl0CglyZXRzdWIKCi8vIGFzc2V0VG90YWwoKXZvaWQKKmFiaV9yb3V0ZV9hc3NldFRvdGFsOgoJLy8gZXhlY3V0ZSBhc3NldFRvdGFsKCl2b2lkCgljYWxsc3ViIGFzc2V0VG90YWwKCWludCAxCglyZXR1cm4KCi8vIGFzc2V0VG90YWwoKTogdm9pZAphc3NldFRvdGFsOgoJcHJvdG8gMCAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE0OAoJLy8gYXNzZXJ0KHRoaXMuYXNhLnZhbHVlLnRvdGFsKQoJYnl0ZSAweDYxNzM2MSAvLyAiYXNhIgoJYXBwX2dsb2JhbF9nZXQKCWFzc2V0X3BhcmFtc19nZXQgQXNzZXRUb3RhbAoJcG9wCglhc3NlcnQKCXJldHN1YgoKLy8gaGFzQXNzZXQoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfaGFzQXNzZXQ6CgkvLyBhZGRyOiBhZGRyZXNzCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAxCglkdXAKCWxlbgoJaW50IDMyCgk9PQoJYXNzZXJ0CgoJLy8gZXhlY3V0ZSBoYXNBc3NldChhZGRyZXNzKXZvaWQKCWNhbGxzdWIgaGFzQXNzZXQKCWludCAxCglyZXR1cm4KCi8vIGhhc0Fzc2V0KGFkZHI6IEFkZHJlc3MpOiB2b2lkCmhhc0Fzc2V0OgoJcHJvdG8gMSAwCgoJLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvcmVzb3VyY2UtcGFja2VyL3Jlc291cmNlLXBhY2tlci5hbGdvLnRzOjE1MgoJLy8gYXNzZXJ0KCFhZGRyLmlzT3B0ZWRJblRvQXNzZXQodGhpcy5hc2EudmFsdWUpKQoJZnJhbWVfZGlnIC0xIC8vIGFkZHI6IEFkZHJlc3MKCWJ5dGUgMHg2MTczNjEgLy8gImFzYSIKCWFwcF9nbG9iYWxfZ2V0Cglhc3NldF9ob2xkaW5nX2dldCBBc3NldEJhbGFuY2UKCXN3YXAKCXBvcAoJIQoJYXNzZXJ0CglyZXRzdWIKCi8vIGV4dGVybmFsTG9jYWwoYWRkcmVzcyl2b2lkCiphYmlfcm91dGVfZXh0ZXJuYWxMb2NhbDoKCS8vIGFkZHI6IGFkZHJlc3MKCXR4bmEgQXBwbGljYXRpb25BcmdzIDEKCWR1cAoJbGVuCglpbnQgMzIKCT09Cglhc3NlcnQKCgkvLyBleGVjdXRlIGV4dGVybmFsTG9jYWwoYWRkcmVzcyl2b2lkCgljYWxsc3ViIGV4dGVybmFsTG9jYWwKCWludCAxCglyZXR1cm4KCi8vIGV4dGVybmFsTG9jYWwoYWRkcjogQWRkcmVzcyk6IHZvaWQKZXh0ZXJuYWxMb2NhbDoKCXByb3RvIDEgMAoKCS8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL3Jlc291cmNlLXBhY2tlci9yZXNvdXJjZS1wYWNrZXIuYWxnby50czoxNTYKCS8vIGxvZyh0aGlzLmV4dGVybmFsQXBwSUQudmFsdWUubG9jYWxTdGF0ZShhZGRyLCAnbG9jYWxLZXknKSBhcyBieXRlcykKCWJ5dGUgMHg2NTc4NzQ2NTcyNmU2MTZjNDE3MDcwNDk0NCAvLyAiZXh0ZXJuYWxBcHBJRCIKCWFwcF9nbG9iYWxfZ2V0CglieXRlIDB4NmM2ZjYzNjE2YzRiNjU3OSAvLyAibG9jYWxLZXkiCglmcmFtZV9kaWcgLTEgLy8gYWRkcjogQWRkcmVzcwoJY292ZXIgMgoJYXBwX2xvY2FsX2dldF9leAoJYXNzZXJ0Cglsb2cKCXJldHN1YgoKKmFiaV9yb3V0ZV9jcmVhdGVBcHBsaWNhdGlvbjoKCWludCAxCglyZXR1cm4KCipjcmVhdGVfTm9PcDoKCW1ldGhvZCAiY3JlYXRlQXBwbGljYXRpb24oKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2NyZWF0ZUFwcGxpY2F0aW9uCgllcnIKCipjYWxsX05vT3A6CgltZXRob2QgImJvb3RzdHJhcCgpdm9pZCIKCW1ldGhvZCAiYWRkcmVzc0JhbGFuY2UoYWRkcmVzcyl2b2lkIgoJbWV0aG9kICJzbWFsbEJveCgpdm9pZCIKCW1ldGhvZCAibWVkaXVtQm94KCl2b2lkIgoJbWV0aG9kICJleHRlcm5hbEFwcENhbGwoKXZvaWQiCgltZXRob2QgImFzc2V0VG90YWwoKXZvaWQiCgltZXRob2QgImhhc0Fzc2V0KGFkZHJlc3Mpdm9pZCIKCW1ldGhvZCAiZXh0ZXJuYWxMb2NhbChhZGRyZXNzKXZvaWQiCgl0eG5hIEFwcGxpY2F0aW9uQXJncyAwCgltYXRjaCAqYWJpX3JvdXRlX2Jvb3RzdHJhcCAqYWJpX3JvdXRlX2FkZHJlc3NCYWxhbmNlICphYmlfcm91dGVfc21hbGxCb3ggKmFiaV9yb3V0ZV9tZWRpdW1Cb3ggKmFiaV9yb3V0ZV9leHRlcm5hbEFwcENhbGwgKmFiaV9yb3V0ZV9hc3NldFRvdGFsICphYmlfcm91dGVfaGFzQXNzZXQgKmFiaV9yb3V0ZV9leHRlcm5hbExvY2FsCgllcnI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDk=" + }, + "contract": { + "name": "ResourcePackerv9", + "desc": "", + "methods": [ + { + "name": "bootstrap", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "addressBalance", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "smallBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "mediumBox", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "externalAppCall", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "assetTotal", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "hasAsset", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "externalLocal", + "args": [ + { + "name": "addr", + "type": "address" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "createApplication", + "args": [], + "returns": { + "type": "void" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/resource-packer/resource-packer.algo.ts b/tests/artifacts/resource-packer/resource-packer.algo.ts new file mode 100644 index 00000000..d1e98660 --- /dev/null +++ b/tests/artifacts/resource-packer/resource-packer.algo.ts @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { Contract } from '@algorandfoundation/tealscript' + +class ExternalApp extends Contract { + localKey = LocalStateKey() + + boxKey = BoxKey() + + asa = GlobalStateKey() + + optInToApplication(): void { + this.localKey(this.txn.sender).value = 'foo' + } + + dummy(): void {} + + error(): void { + throw Error() + } + + boxWithPayment(_payment: PayTxn): void { + this.boxKey.value = 'foo' + } + + createAsset(): void { + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + senderAssetBalance(): void { + assert(!this.txn.sender.isOptedInToAsset(this.asa.value)) + } +} + +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class ResourcePackerv8 extends Contract { + programVersion = 8 + + externalAppID = GlobalStateKey() + + asa = GlobalStateKey() + + smallBoxKey = BoxKey({ key: 's' }) + + mediumBoxKey = BoxKey({ key: 'm' }) + + bootstrap(): void { + sendMethodCall<[], void>({ + name: 'createApplication', + approvalProgram: ExternalApp.approvalProgram(), + clearStateProgram: ExternalApp.clearProgram(), + localNumByteSlice: ExternalApp.schema.local.numByteSlice, + globalNumByteSlice: ExternalApp.schema.global.numByteSlice, + globalNumUint: ExternalApp.schema.global.numUint, + localNumUint: ExternalApp.schema.local.numUint, + }) + + this.externalAppID.value = this.itxn.createdApplicationID + + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + addressBalance(addr: Address): void { + log(rawBytes(addr.isInLedger)) + } + + smallBox(): void { + this.smallBoxKey.value = '' + } + + mediumBox(): void { + this.mediumBoxKey.create(5_000) + } + + externalAppCall(): void { + sendMethodCall<[], void>({ + applicationID: this.externalAppID.value, + name: 'dummy', + }) + } + + assetTotal(): void { + assert(this.asa.value.total) + } + + hasAsset(addr: Address): void { + assert(!addr.isOptedInToAsset(this.asa.value)) + } + + externalLocal(addr: Address): void { + log(this.externalAppID.value.localState(addr, 'localKey') as bytes) + } +} + +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class ResourcePackerv9 extends Contract { + programVersion = 9 + + externalAppID = GlobalStateKey() + + asa = GlobalStateKey() + + smallBoxKey = BoxKey({ key: 's' }) + + mediumBoxKey = BoxKey({ key: 'm' }) + + bootstrap(): void { + sendMethodCall<[], void>({ + name: 'createApplication', + approvalProgram: ExternalApp.approvalProgram(), + clearStateProgram: ExternalApp.clearProgram(), + localNumByteSlice: ExternalApp.schema.local.numByteSlice, + globalNumByteSlice: ExternalApp.schema.global.numByteSlice, + globalNumUint: ExternalApp.schema.global.numUint, + localNumUint: ExternalApp.schema.local.numUint, + }) + + this.externalAppID.value = this.itxn.createdApplicationID + + this.asa.value = sendAssetCreation({ + configAssetTotal: 1, + }) + } + + addressBalance(addr: Address): void { + log(rawBytes(addr.isInLedger)) + } + + smallBox(): void { + this.smallBoxKey.value = '' + } + + mediumBox(): void { + this.mediumBoxKey.create(5_000) + } + + externalAppCall(): void { + sendMethodCall<[], void>({ + applicationID: this.externalAppID.value, + name: 'dummy', + }) + } + + assetTotal(): void { + assert(this.asa.value.total) + } + + hasAsset(addr: Address): void { + assert(!addr.isOptedInToAsset(this.asa.value)) + } + + externalLocal(addr: Address): void { + log(this.externalAppID.value.localState(addr, 'localKey') as bytes) + } +} diff --git a/tests/artifacts/testing_app/app_spec.arc32.json b/tests/artifacts/testing_app/app_spec.arc32.json new file mode 100644 index 00000000..c308fc12 --- /dev/null +++ b/tests/artifacts/testing_app/app_spec.arc32.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/app_spec.arc56.json b/tests/artifacts/testing_app_arc56/app_spec.arc56.json new file mode 100644 index 00000000..da275d16 --- /dev/null +++ b/tests/artifacts/testing_app_arc56/app_spec.arc56.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/app_spec.arc32.json b/tests/artifacts/testing_app_puya/app_spec.arc32.json new file mode 100644 index 00000000..d8518906 --- /dev/null +++ b/tests/artifacts/testing_app_puya/app_spec.arc32.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/__init__.py b/tests/assets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py new file mode 100644 index 00000000..2c1987cb --- /dev/null +++ b/tests/assets/test_asset_manager.py @@ -0,0 +1,216 @@ +import pytest +from algosdk.atomic_transaction_composer import AccountTransactionSigner + +from algokit_utils import SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.assets.asset_manager import ( + AccountAssetInformation, + AssetInformation, + BulkAssetOptInOutResult, +) +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AssetCreateParams, + PaymentParams, +) + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def sender(algorand: AlgorandClient) -> SigningAccount: + 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 receiver(algorand: AlgorandClient) -> SigningAccount: + 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: SigningAccount) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get its info + asset_info = algorand.asset.get_by_id(asset_id) + + assert isinstance(asset_info, AssetInformation) + assert asset_info.asset_id == asset_id + assert asset_info.total == total + assert asset_info.decimals == 0 + assert asset_info.default_frozen is False + assert asset_info.unit_name == "TEST" + assert asset_info.asset_name == "Test Asset" + assert asset_info.url == "https://example.com" + assert asset_info.creator == sender.address + + +def test_get_account_information_with_address(algorand: AlgorandClient, sender: SigningAccount) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info + account_info = algorand.asset.get_account_information(sender.address, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_get_account_information_with_account(algorand: AlgorandClient, sender: SigningAccount) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info + account_info = algorand.asset.get_account_information(sender, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_get_account_information_with_transaction_signer(algorand: AlgorandClient, sender: SigningAccount) -> None: + # First create an asset + total = 1000 + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then get account info using transaction signer + signer = AccountTransactionSigner(sender.private_key) + account_info = algorand.asset.get_account_information(signer, asset_id) + + assert isinstance(account_info, AccountAssetInformation) + assert account_info.asset_id == asset_id + assert account_info.balance == total + assert account_info.frozen is False + + +def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: SigningAccount, receiver: SigningAccount) -> None: + # First create some assets + asset_ids = [] + for i in range(3): + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name=f"TST{i}", + asset_name=f"Test Asset {i}", + url="https://example.com", + signer=sender.signer, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + asset_ids.append(asset_id) + + # Fund receiver + algorand.send.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + # Then bulk opt-in + results = algorand.asset.bulk_opt_in(receiver.address, asset_ids, signer=receiver.signer) + + assert len(results) == len(asset_ids) + for result in results: + assert isinstance(result, BulkAssetOptInOutResult) + assert result.asset_id in asset_ids + assert result.transaction_id + + +def test_bulk_opt_out_not_opted_in_fails( + algorand: AlgorandClient, sender: SigningAccount, receiver: SigningAccount +) -> None: + # First create an asset + create_result = algorand.send.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Fund receiver but don't opt-in + algorand.send.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + # Then attempt to opt-out + with pytest.raises(ValueError, match="is not opted-in"): + algorand.asset.bulk_opt_out(account=receiver.address, asset_ids=[asset_id]) diff --git a/tests/clients/__init__.py b/tests/clients/__init__.py new file mode 100644 index 00000000..e69de29b 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..40f11e09 --- /dev/null +++ b/tests/clients/algorand_client/test_transfer.py @@ -0,0 +1,428 @@ +import httpx +import pytest +from pytest_httpx._httpx_mock import HTTPXMock + +from algokit_utils.algorand import AlgorandClient +from algokit_utils.clients.dispenser_api_client import DispenserApiConfig, TestNetDispenserApiClient +from algokit_utils.models.account import SigningAccount +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_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), 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: SigningAccount) -> 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: SigningAccount) -> 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: SigningAccount) -> 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: SigningAccount) -> 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: SigningAccount, +) -> 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: SigningAccount) -> 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: SigningAccount) -> 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: SigningAccount) -> 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="account asset info not found"): + algorand.asset.get_account_information(second_account, test_asset_id) + + algorand.send.asset_opt_in( + AssetOptInParams( + sender=second_account.address, + 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: SigningAccount) -> 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: SigningAccount) -> 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 to_account_info.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: SigningAccount +) -> 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 to_account_info.amount == AlgoAmount.from_algos(1) + + +def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: + algorand = AlgorandClient.testnet() + 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.testnet() + 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: SigningAccount) -> None: + second_account = algorand.account.random() + + algorand.account.rekey_account(funded_account.address, 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/conftest.py b/tests/conftest.py index be23305b..fab07acf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,30 +6,21 @@ 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, - Account, ApplicationClient, ApplicationSpecification, - EnsureBalanceParameters, - ensure_funded, - get_account, - get_algod_client, - get_indexer_client, - get_kmd_client_from_algod_client, + SigningAccount, replace_template_variables, ) -from dotenv import load_dotenv - -from tests import app_client_test +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME +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 + pass @pytest.fixture(autouse=True, scope="session") @@ -45,7 +36,7 @@ def check_output_stability(logs: str, *, test_name: str | None = None) -> None: caller_dir = caller_path.parent test_name = test_name or caller_frame.function caller_stem = Path(caller_frame.filename).stem - output_dir = caller_dir / f"{caller_stem}.approvals" + output_dir = caller_dir / "_snapshots" / f"{caller_stem}.approvals" output_dir.mkdir(exist_ok=True) output_file = output_dir / f"{test_name}.approved.txt" output_file_str = str(output_file) @@ -127,85 +118,27 @@ 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: SigningAccount, total: int | None) -> int: if total is None: total = math.floor(random.random() * 100) + 20 decimals = 0 asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}" - params = algod_client.suggested_params() - - txn = algosdk.transaction.AssetConfigTxn( - sender=sender.address, - sp=params, - total=total * 10**decimals, - decimals=decimals, - default_frozen=False, - unit_name="", - asset_name=asset_name, - manager=sender.address, - reserve=sender.address, - freeze=sender.address, - clawback=sender.address, - url="https://path/to/my/asset/details", - metadata_hash=None, - note=None, - lease=None, - rekey_to=None, - ) # type: ignore[no-untyped-call] - - signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call] - algod_client.send_transaction(signed_transaction) - ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call] - - if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int): - return ptx["asset-index"] - else: - raise ValueError("Unexpected response from pending_transaction_info") - - -def assure_funds(algod_client: "AlgodClient", account: Account) -> None: - ensure_funded( - algod_client, - EnsureBalanceParameters( - account_to_fund=account, - min_spending_balance_micro_algos=300000, - min_funding_increment_micro_algos=1, - ), + 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, + ) ) + + return int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] diff --git a/tests/models/test_algo_amount.py b/tests/models/test_algo_amount.py new file mode 100644 index 00000000..8b7d2c6a --- /dev/null +++ b/tests/models/test_algo_amount.py @@ -0,0 +1,102 @@ +from decimal import Decimal + +import pytest + +from algokit_utils.models.amount import AlgoAmount + + +def test_initialization() -> None: + # Test valid initialization formats + assert AlgoAmount({"microAlgos": 1_000_000}).micro_algos == 1_000_000 + assert AlgoAmount({"microAlgo": 500_000}).micro_algos == 500_000 + assert AlgoAmount({"algos": 1}).micro_algos == 1_000_000 + assert AlgoAmount({"algo": Decimal("0.5")}).micro_algos == 500_000 + + # Test decimal precision + assert AlgoAmount({"algos": Decimal("0.000001")}).micro_algos == 1 + assert AlgoAmount({"algo": Decimal("123.456789")}).micro_algos == 123_456_789 + + # Test invalid initialization + with pytest.raises(ValueError, match="Invalid amount provided"): + AlgoAmount({"invalid": 100}) + + +def test_from_methods() -> None: + assert AlgoAmount.from_micro_algos(500_000).micro_algos == 500_000 + assert AlgoAmount.from_micro_algo(250_000).micro_algos == 250_000 + assert AlgoAmount.from_algos(2).micro_algos == 2_000_000 + assert AlgoAmount.from_algo(Decimal("0.75")).micro_algos == 750_000 + + +def test_properties() -> None: + amount = AlgoAmount.from_micro_algos(1_234_567) + assert amount.micro_algos == 1_234_567 + assert amount.micro_algo == 1_234_567 + assert amount.algos == Decimal("1.234567") + assert amount.algo == Decimal("1.234567") + + +def test_arithmetic_operations() -> None: + a = AlgoAmount.from_algos(5) + b = AlgoAmount.from_algos(3) + + # Addition + assert (a + b).micro_algos == 8_000_000 + a += b + assert a.micro_algos == 8_000_000 + + # Subtraction + assert (a - b).micro_algos == 5_000_000 + a -= b + assert a.micro_algos == 5_000_000 + + # Right operations + assert (AlgoAmount.from_micro_algo(1000) + a).micro_algos == 5_001_000 + assert (AlgoAmount.from_algos(10) - a).micro_algos == 5_000_000 + + +def test_comparison_operators() -> None: + base = AlgoAmount.from_algos(5) + same = AlgoAmount.from_algos(5) + larger = AlgoAmount.from_algos(10) + + assert base == same + assert base != larger + assert base < larger + assert larger > base + assert base <= same + assert larger >= base + + # Test int comparison + assert base == 5_000_000 + assert base < 6_000_000 + assert base > 4_000_000 + + +def test_edge_cases() -> None: + # Zero value + zero = AlgoAmount.from_micro_algos(0) + assert zero.micro_algos == 0 + assert zero.algos == 0 + + # Very large values + large = AlgoAmount.from_algos(Decimal("1e9")) + assert large.micro_algos == 1e9 * 1e6 + + # Decimal precision limits + precise = AlgoAmount({"algos": Decimal("0.123456789")}) + assert precise.micro_algos == 123_456 + + +def test_string_representation() -> None: + assert str(AlgoAmount.from_micro_algos(1_000_000)) == "1,000,000 µALGO" + assert str(AlgoAmount.from_algos(Decimal("2.5"))) == "2,500,000 µALGO" + + +def test_type_safety() -> None: + with pytest.raises(TypeError, match="Unsupported operand type"): + # int is not AlgoAmount + AlgoAmount.from_algos(5) + 1000 # type: ignore # noqa: PGH003 + + with pytest.raises(TypeError, match="Unsupported operand type"): + AlgoAmount.from_algos(5) - "invalid" # type: ignore # noqa: PGH003 diff --git a/tests/test_algorand_client.py b/tests/test_algorand_client.py deleted file mode 100644 index 5f258640..00000000 --- a/tests/test_algorand_client.py +++ /dev/null @@ -1,222 +0,0 @@ -import json -from pathlib import Path - -import pytest -from algokit_utils import Account, ApplicationClient -from algokit_utils.beta.account_manager import AddressAndSigner -from algokit_utils.beta.algorand_client import ( - AlgorandClient, - AssetCreateParams, - AssetOptInParams, - MethodCallParams, - PayParams, -) -from algosdk.abi import Contract -from algosdk.atomic_transaction_composer import AtomicTransactionComposer - - -@pytest.fixture() -def algorand(funded_account: Account) -> AlgorandClient: - client = AlgorandClient.default_local_net() - client.set_signer(sender=funded_account.address, signer=funded_account.signer) - return client - - -@pytest.fixture() -def alice(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: - acct = algorand.account.random() - algorand.send.payment(PayParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) - return acct - - -@pytest.fixture() -def bob(algorand: AlgorandClient, funded_account: Account) -> AddressAndSigner: - acct = algorand.account.random() - algorand.send.payment(PayParams(sender=funded_account.address, receiver=acct.address, amount=1_000_000)) - return acct - - -@pytest.fixture() -def app_client(algorand: AlgorandClient, alice: AddressAndSigner) -> ApplicationClient: - client = ApplicationClient( - algorand.client.algod, - Path(__file__).parent / "app_algorand_client.json", - sender=alice.address, - signer=alice.signer, - ) - client.create(call_abi_method="createApplication") - return client - - -@pytest.fixture() -def contract() -> Contract: - with Path.open(Path(__file__).parent / "app_algorand_client.json") as f: - return Contract.from_json(json.dumps(json.load(f)["contract"])) - - -def test_send_payment(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: - amount = 100_000 - - alice_pre_balance = algorand.account.get_information(alice.address)["amount"] - bob_pre_balance = algorand.account.get_information(bob.address)["amount"] - result = algorand.send.payment(PayParams(sender=alice.address, receiver=bob.address, amount=amount)) - alice_post_balance = algorand.account.get_information(alice.address)["amount"] - bob_post_balance = algorand.account.get_information(bob.address)["amount"] - - assert result["confirmation"] is not None - assert alice_post_balance == alice_pre_balance - 1000 - amount - assert bob_post_balance == bob_pre_balance + amount - - -def test_send_asset_create(algorand: AlgorandClient, alice: AddressAndSigner) -> None: - total = 100 - - result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) - asset_index = result["confirmation"]["asset-index"] - - assert asset_index > 0 - - -def test_asset_opt_in(algorand: AlgorandClient, alice: AddressAndSigner, bob: AddressAndSigner) -> None: - total = 100 - - result = algorand.send.asset_create(AssetCreateParams(sender=alice.address, total=total)) - asset_index = result["confirmation"]["asset-index"] - - algorand.send.asset_opt_in(AssetOptInParams(sender=bob.address, asset_id=asset_index)) - - assert algorand.account.get_asset_information(bob.address, asset_index) is not None - - -DO_MATH_VALUE = 3 - - -def test_add_atc(algorand: AlgorandClient, app_client: ApplicationClient, alice: AddressAndSigner) -> None: - atc = AtomicTransactionComposer() - app_client.compose_call(atc, call_abi_method="doMath", a=1, b=2, operation="sum") - - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_atc(atc) - .execute() - ) - assert result.abi_results[0].return_value == DO_MATH_VALUE - - -def test_add_method_call( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("doMath"), - sender=alice.address, - app_id=app_client.app_id, - args=[1, 2, "sum"], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == DO_MATH_VALUE - - -def test_add_method_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg = PayParams(sender=alice.address, receiver=alice.address, amount=1) - result = ( - algorand.new_group() - .add_payment(PayParams(sender=alice.address, amount=0, receiver=alice.address)) - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("txnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[pay_arg], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - - -def test_add_method_call_with_method_call_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - hello_world_call = MethodCallParams( - method=contract.get_method_by_name("helloWorld"), sender=alice.address, app_id=app_client.app_id - ) - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("methodArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[hello_world_call], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == "Hello, World!" - assert result.abi_results[1].return_value == app_client.app_id - - -def test_add_method_call_with_method_call_arg_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg = PayParams(sender=alice.address, receiver=alice.address, amount=1) - txn_arg_call = MethodCallParams( - method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg] - ) - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("nestedTxnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[txn_arg_call], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - assert result.abi_results[1].return_value == app_client.app_id - - -def test_add_method_call_with_two_method_call_args_with_txn_arg( - algorand: AlgorandClient, contract: Contract, alice: AddressAndSigner, app_client: ApplicationClient -) -> None: - pay_arg_1 = PayParams(sender=alice.address, receiver=alice.address, amount=1) - txn_arg_call_1 = MethodCallParams( - method=contract.get_method_by_name("txnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[pay_arg_1], - note=b"1", - ) - - pay_arg_2 = PayParams(sender=alice.address, receiver=alice.address, amount=2) - txn_arg_call_2 = MethodCallParams( - method=contract.get_method_by_name("txnArg"), sender=alice.address, app_id=app_client.app_id, args=[pay_arg_2] - ) - - result = ( - algorand.new_group() - .add_method_call( - MethodCallParams( - method=contract.get_method_by_name("doubleNestedTxnArg"), - sender=alice.address, - app_id=app_client.app_id, - args=[txn_arg_call_1, txn_arg_call_2], - ) - ) - .execute() - ) - assert result.abi_results[0].return_value == alice.address - assert result.abi_results[1].return_value == alice.address - assert result.abi_results[2].return_value == app_client.app_id diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt b/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt deleted file mode 100644 index 598d4c2f..00000000 --- a/tests/test_app_client_call.approvals/test_readonly_call_with_error_with_new_client_missing_source_map.approved.txt +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index 976a00ce..bdb0d5b3 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -1,47 +1,85 @@ import json import os +from collections.abc import Generator from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest +from algosdk.abi.method import Method +from algosdk.atomic_transaction_composer import ( + AccountTransactionSigner, + AtomicTransactionComposer, + TransactionWithSigner, +) +from algosdk.transaction import PaymentTxn + from algokit_utils._debugging import ( PersistSourceMapInput, cleanup_old_trace_files, persist_sourcemaps, simulate_and_persist_response, ) -from algokit_utils.account import get_account -from algokit_utils.application_client import ApplicationClient -from algokit_utils.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications import AppFactoryCreateMethodCallParams +from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams from algokit_utils.common import Program -from algokit_utils.models import Account -from algosdk.atomic_transaction_composer import ( - AccountTransactionSigner, - AtomicTransactionComposer, - TransactionWithSigner, +from algokit_utils.models import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AssetCreateParams, + AssetTransferParams, + PaymentParams, ) -from algosdk.transaction import AssetTransferTxn, PaymentTxn - -from tests.conftest import get_unique_name - -if TYPE_CHECKING: - from algosdk.v2client.algod import AlgodClient - - -@pytest.fixture() -def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecification) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, signer=creator) - create_response = client.create("create") - assert create_response.tx_id - return client -def test_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory) -> None: +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, + dispenser, + min_spending_balance=AlgoAmount.from_algo(100), + min_funding_increment=AlgoAmount.from_algo(100), + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def client_fixture(algorand: AlgorandClient, funded_account: SigningAccount) -> AppClient: + app_spec = (Path(__file__).parent / "artifacts" / "legacy_app_client_test" / "app_client_test.json").read_text() + app_factory = algorand.client.get_app_factory( + app_spec=app_spec, default_sender=funded_account.address, default_signer=funded_account.signer + ) + app_client, _ = app_factory.send.create( + AppFactoryCreateMethodCallParams(method="create"), + compilation_params={ + "deletable": True, + "updatable": True, + "deploy_time_params": {"VERSION": 1}, + }, + ) + return app_client + + +@pytest.fixture +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.transactions.transaction_composer.config", new_callable=Mock) as mock_config: + mock_config.debug = True + mock_config.project_root = None + yield mock_config + + +def test_build_teal_sourcemaps(algorand: AlgorandClient, tmp_path_factory: pytest.TempPathFactory) -> None: cwd = tmp_path_factory.mktemp("cwd") approval = """ @@ -57,7 +95,7 @@ def test_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: py PersistSourceMapInput(raw_teal=clear, app_name="cool_app", file_name="clear"), ] - persist_sourcemaps(sources=sources, project_root=cwd, client=algod_client) + persist_sourcemaps(sources=sources, project_root=cwd, client=algorand.client.algod) root_path = cwd / ".algokit" / "sources" sourcemap_file_path = root_path / "sources.avm.json" @@ -71,7 +109,7 @@ def test_build_teal_sourcemaps(algod_client: "AlgodClient", tmp_path_factory: py def test_build_teal_sourcemaps_without_sources( - algod_client: "AlgodClient", tmp_path_factory: pytest.TempPathFactory + algorand: AlgorandClient, tmp_path_factory: pytest.TempPathFactory ) -> None: cwd = tmp_path_factory.mktemp("cwd") @@ -83,14 +121,14 @@ def test_build_teal_sourcemaps_without_sources( #pragma version 9 int 1 """ - compiled_approval = Program(approval, algod_client) - compiled_clear = Program(clear, algod_client) + compiled_approval = Program(approval, algorand.client.algod) + compiled_clear = Program(clear, algorand.client.algod) sources = [ PersistSourceMapInput(compiled_teal=compiled_approval, app_name="cool_app", file_name="approval.teal"), PersistSourceMapInput(compiled_teal=compiled_clear, app_name="cool_app", file_name="clear"), ] - persist_sourcemaps(sources=sources, project_root=cwd, client=algod_client, with_sources=False) + persist_sourcemaps(sources=sources, project_root=cwd, client=algorand.client.algod, with_sources=False) root_path = cwd / ".algokit" / "sources" sourcemap_file_path = root_path / "sources.avm.json" @@ -107,17 +145,16 @@ def test_build_teal_sourcemaps_without_sources( def test_simulate_and_persist_response_via_app_call( tmp_path_factory: pytest.TempPathFactory, - client_fixture: ApplicationClient, - mocker: Mock, + client_fixture: AppClient, + mock_config: Mock, ) -> None: - mock_config = mocker.patch("algokit_utils.application_client.config") mock_config.debug = True mock_config.trace_all = True mock_config.trace_buffer_size_mb = 256 cwd = tmp_path_factory.mktemp("cwd") mock_config.project_root = cwd - client_fixture.call("hello", name="test") + client_fixture.send.call(AppClientMethodCallParams(method="hello", args=["test"])) output_path = cwd / "debug_traces" @@ -130,26 +167,29 @@ def test_simulate_and_persist_response_via_app_call( def test_simulate_and_persist_response( - tmp_path_factory: pytest.TempPathFactory, client_fixture: ApplicationClient, mocker: Mock, funded_account: Account + tmp_path_factory: pytest.TempPathFactory, + algorand: AlgorandClient, + mock_config: Mock, + funded_account: SigningAccount, ) -> None: - mock_config = mocker.patch("algokit_utils.application_client.config") mock_config.debug = True mock_config.trace_all = True cwd = tmp_path_factory.mktemp("cwd") mock_config.project_root = cwd + algod = algorand.client.algod payment = PaymentTxn( sender=funded_account.address, - receiver=client_fixture.app_address, + receiver=funded_account.address, amt=1_000_000, note=b"Payment", - sp=client_fixture.algod_client.suggested_params(), - ) # type: ignore[no-untyped-call] + sp=algod.suggested_params(), + ) txn_with_signer = TransactionWithSigner(payment, AccountTransactionSigner(funded_account.private_key)) atc = AtomicTransactionComposer() atc.add_transaction(txn_with_signer) - simulate_and_persist_response(atc, cwd, client_fixture.algod_client) + simulate_and_persist_response(atc, cwd, algod) output_path = cwd / "debug_traces" content = list(output_path.iterdir()) @@ -161,7 +201,7 @@ def test_simulate_and_persist_response( trace_file_path = content[0] while trace_file_path.exists(): tmp_atc = atc.clone() - simulate_and_persist_response(tmp_atc, cwd, client_fixture.algod_client, buffer_size_mb=0.01) + simulate_and_persist_response(tmp_atc, cwd, algod, buffer_size_mb=0.003) @pytest.mark.parametrize( @@ -180,44 +220,66 @@ def test_simulate_and_persist_response( ({"pay": 1, "axfer": 1, "appl": 1}, "1pay_1axfer_1appl"), ], ) -def test_simulate_response_filename_generation( # noqa: PLR0913 +def test_simulate_response_filename_generation( transactions: dict[str, int], expected_filename_part: str, tmp_path_factory: pytest.TempPathFactory, - client_fixture: ApplicationClient, - funded_account: Account, + client_fixture: AppClient, + funded_account: SigningAccount, monkeypatch: pytest.MonkeyPatch, + mock_config: Mock, ) -> None: + asset_id = 1 + if "axfer" in transactions: + asset_id = client_fixture.algorand.send.asset_create( + AssetCreateParams( + sender=funded_account.address, + total=100_000_000, + decimals=0, + unit_name="TEST", + asset_name="Test Asset", + ) + ).asset_id + cwd = tmp_path_factory.mktemp("cwd") - sp = client_fixture.algod_client.suggested_params() - atc = AtomicTransactionComposer() - signer = AccountTransactionSigner(funded_account.private_key) + mock_config.debug = True + mock_config.trace_all = True + mock_config.trace_buffer_size_mb = 256 + mock_config.project_root = cwd + atc = client_fixture.algorand.new_group() # Add payment transactions for i in range(transactions.get("pay", 0)): - payment = PaymentTxn( - sender=funded_account.address, - receiver=client_fixture.app_address, - amt=1_000_000 * (i + 1), - note=f"Payment{i+1}".encode(), - sp=sp, - ) # type: ignore[no-untyped-call] - atc.add_transaction(TransactionWithSigner(payment, signer)) + atc.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=client_fixture.app_address, + amount=AlgoAmount.from_micro_algos(1_000_000 * (i + 1)), + note=f"Payment{i+1}".encode(), + ) + ) # Add asset transfer transactions for i in range(transactions.get("axfer", 0)): - asset_transfer = AssetTransferTxn( - sender=funded_account.address, - receiver=client_fixture.app_address, - amt=1_000 * (i + 1), - index=1, # Using asset ID 1 for test - sp=sp, - ) # type: ignore[no-untyped-call] - atc.add_transaction(TransactionWithSigner(asset_transfer, signer)) + atc.add_asset_transfer( + AssetTransferParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=1_000 * (i + 1), + asset_id=asset_id, + ) + ) # Add app calls for i in range(transactions.get("appl", 0)): - client_fixture.compose_call(atc, "hello", name=f"test{i+1}") + atc.add_app_call_method_call( + AppCallMethodCallParams( + method=Method.from_signature("hello(string)string"), + args=[f"test{i+1}"], + sender=funded_account.address, + app_id=client_fixture.app_id, + ) + ) # Mock datetime mock_datetime = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) @@ -229,7 +291,8 @@ def now(cls, tz: timezone | None = None) -> datetime: # noqa: ARG003 monkeypatch.setattr("algokit_utils._debugging.datetime", MockDateTime) - response = simulate_and_persist_response(atc, cwd, client_fixture.algod_client) + response = atc.simulate() + assert response.simulate_response last_round = response.simulate_response["last-round"] expected_filename = f"20230101_120000_lr{last_round}_{expected_filename_part}.trace.avm.json" @@ -253,10 +316,16 @@ class TestFile: mtime: datetime -def test_removes_oldest_files_when_buffer_size_exceeded(tmp_path: Path) -> None: - # Create test directory - trace_dir = tmp_path / "debug_traces" - trace_dir.mkdir() +def test_removes_oldest_files_when_buffer_size_exceeded( + tmp_path_factory: pytest.TempPathFactory, mock_config: Mock +) -> None: + cwd = tmp_path_factory.mktemp("cwd") + trace_dir = cwd / "debug_traces" + trace_dir.mkdir(exist_ok=True) + mock_config.debug = True + mock_config.trace_all = True + mock_config.trace_buffer_size_mb = 256 + mock_config.project_root = cwd # Create test files with different timestamps and sizes test_files: list[TestFile] = [ @@ -278,15 +347,23 @@ def test_removes_oldest_files_when_buffer_size_exceeded(tmp_path: Path) -> None: remaining_files = list(trace_dir.iterdir()) remaining_names = [f.name for f in remaining_files] - assert len(remaining_files) == 2 # noqa: PLR2004 + assert len(remaining_files) == 2 assert "newer.json" in remaining_names assert "newest.json" in remaining_names assert "old.json" not in remaining_names -def test_does_nothing_when_total_size_within_buffer_limit(tmp_path: Path) -> None: +def test_does_nothing_when_total_size_within_buffer_limit( + tmp_path_factory: pytest.TempPathFactory, mock_config: Mock +) -> None: + cwd = tmp_path_factory.mktemp("cwd") + mock_config.debug = True + mock_config.trace_all = True + mock_config.trace_buffer_size_mb = 256 + mock_config.project_root = cwd + # Create test directory - trace_dir = tmp_path / "debug_traces" + trace_dir = cwd / "debug_traces" trace_dir.mkdir() # Create two 512KB files (total 1MB) @@ -298,4 +375,4 @@ def test_does_nothing_when_total_size_within_buffer_limit(tmp_path: Path) -> Non cleanup_old_trace_files(trace_dir, buffer_size_mb=2.0) remaining_files = list(trace_dir.iterdir()) - assert len(remaining_files) == 2 # noqa: PLR2004 + assert len(remaining_files) == 2 diff --git a/tests/transactions/__init__.py b/tests/transactions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/transactions/test_abi_return.py b/tests/transactions/test_abi_return.py new file mode 100644 index 00000000..dc3d50f5 --- /dev/null +++ b/tests/transactions/test_abi_return.py @@ -0,0 +1,105 @@ +from algosdk.abi import ABIType, Method +from algosdk.abi.method import Returns +from algosdk.atomic_transaction_composer import ABIResult + +from algokit_utils.applications.abi import ABIReturn, ABIValue + + +def get_abi_result(type_str: str, value: ABIValue) -> ABIReturn: + """Helper function to simulate ABI method return value""" + abi_type = ABIType.from_string(type_str) + encoded = abi_type.encode(value) + decoded = abi_type.decode(encoded) + result = ABIResult( + method=Method(name="", args=[], returns=Returns(arg_type=type_str)), + raw_value=encoded, + return_value=decoded, + tx_id="", + tx_info={}, + decode_error=None, + ) + + return ABIReturn(result) + + +class TestABIReturn: + def test_uint32(self) -> None: + assert get_abi_result("uint32", 0).value == 0 + assert get_abi_result("uint32", 0).value == 0 + assert get_abi_result("uint32", 1).value == 1 + assert get_abi_result("uint32", 1).value == 1 + assert get_abi_result("uint32", 2**32 - 1).value == 2**32 - 1 + assert get_abi_result("uint32", 2**32 - 1).value == 2**32 - 1 + + def test_uint64(self) -> None: + assert get_abi_result("uint64", 0).value == 0 + assert get_abi_result("uint64", 1).value == 1 + assert get_abi_result("uint64", 2**32 - 1).value == 2**32 - 1 + assert get_abi_result("uint64", 2**64 - 1).value == 2**64 - 1 + + def test_uint32_array(self) -> None: + assert get_abi_result("uint32[]", [0]).value == [0] + assert get_abi_result("uint32[]", [0]).value == [0] + assert get_abi_result("uint32[]", [1]).value == [1] + assert get_abi_result("uint32[]", [1]).value == [1] + assert get_abi_result("uint32[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint32[]", [2**32 - 1, 1]).value == [2**32 - 1, 1] + + def test_uint32_fixed_array(self) -> None: + assert get_abi_result("uint32[1]", [0]).value == [0] + assert get_abi_result("uint32[1]", [0]).value == [0] + assert get_abi_result("uint32[1]", [1]).value == [1] + assert get_abi_result("uint32[1]", [1]).value == [1] + assert get_abi_result("uint32[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint32[1]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint32[2]", [2**32 - 1, 1]).value == [2**32 - 1, 1] + + def test_uint64_array(self) -> None: + assert get_abi_result("uint64[]", [0]).value == [0] + assert get_abi_result("uint64[]", [0]).value == [0] + assert get_abi_result("uint64[]", [1]).value == [1] + assert get_abi_result("uint64[]", [1]).value == [1] + assert get_abi_result("uint64[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint64[]", [2**64 - 1, 1]).value == [2**64 - 1, 1] + + def test_uint64_fixed_array(self) -> None: + assert get_abi_result("uint64[1]", [0]).value == [0] + assert get_abi_result("uint64[1]", [0]).value == [0] + assert get_abi_result("uint64[1]", [1]).value == [1] + assert get_abi_result("uint64[1]", [1]).value == [1] + assert get_abi_result("uint64[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[3]", [1, 2, 3]).value == [1, 2, 3] + assert get_abi_result("uint64[1]", [2**32 - 1]).value == [2**32 - 1] + assert get_abi_result("uint64[2]", [2**64 - 1, 1]).value == [2**64 - 1, 1] + + def test_tuple(self) -> None: + type_str = "(uint32,uint64,(uint32,uint64),uint32[],uint64[])" + assert get_abi_result(type_str, [0, 0, [0, 0], [0], [0]]).value == [ + 0, + 0, + [0, 0], + [0], + [0], + ] + assert get_abi_result(type_str, [1, 1, [1, 1], [1], [1]]).value == [ + 1, + 1, + [1, 1], + [1], + [1], + ] + assert get_abi_result( + type_str, + [2**32 - 1, 2**64 - 1, [2**32 - 1, 2**64 - 1], [1, 2, 3], [1, 2, 3]], + ).value == [ + 2**32 - 1, + 2**64 - 1, + [2**32 - 1, 2**64 - 1], + [1, 2, 3], + [1, 2, 3], + ] diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py new file mode 100644 index 00000000..cb277d26 --- /dev/null +++ b/tests/transactions/test_resource_packing.py @@ -0,0 +1,1083 @@ +import dataclasses +import json +from collections.abc import Generator +from pathlib import Path + +import algosdk +import pytest +from algosdk.atomic_transaction_composer import TransactionWithSigner +from algosdk.transaction import OnComplete, PaymentTxn + +from algokit_utils import SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams, FundAppAccountParams +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateParams +from algokit_utils.config import config +from algokit_utils.errors.logic_error import LogicError +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algos(100)) + return new_account + + +def load_arc32_spec(version: int) -> str: + # Load the appropriate spec file from the resource-packer directory + spec_path = Path(__file__).parent.parent / "artifacts" / "resource-packer" / f"ResourcePackerv{version}.arc32.json" + return spec_path.read_text() + + +class BaseResourcePackerTest: + """Base class for resource packing tests""" + + version: int + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Create app based on version + spec = load_arc32_spec(self.version) + factory = algorand.client.get_app_factory( + app_spec=spec, + default_sender=funded_account.address, + ) + self.app_client, _ = factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + self.app_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(2334300))) + self.app_client.send.call( + AppClientMethodCallParams(method="bootstrap", static_fee=AlgoAmount.from_micro_algo(3_000)) + ) + + yield + + config.configure(populate_app_call_resources=False) + + @pytest.fixture + def external_client(self, algorand: AlgorandClient, funded_account: SigningAccount) -> AppClient: + external_spec = ( + Path(__file__).parent.parent / "artifacts" / "resource-packer" / "ExternalApp.arc32.json" + ).read_text() + return algorand.client.get_app_client_by_id( + app_spec=external_spec, + app_id=int(self.app_client.get_global_state()["externalAppID"].value), + app_name="external", + default_sender=funded_account.address, + ) + + def test_accounts_address_balance_invalid_ref(self, algorand: AlgorandClient) -> None: + random_account = algorand.account.random() + with pytest.raises(LogicError, match=f"invalid Account reference {random_account.address}"): + self.app_client.send.call( + AppClientMethodCallParams( + method="addressBalance", + args=[random_account.address], + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_accounts_address_balance_valid_ref(self, algorand: AlgorandClient) -> None: + random_account = algorand.account.random() + self.app_client.send.call( + AppClientMethodCallParams( + method="addressBalance", + args=[random_account.address], + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_boxes_invalid_ref(self) -> None: + with pytest.raises(LogicError, match="invalid Box reference"): + self.app_client.send.call( + AppClientMethodCallParams( + method="smallBox", + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_boxes_valid_ref(self) -> None: + self.app_client.send.call( + AppClientMethodCallParams( + method="smallBox", + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + self.app_client.send.call( + AppClientMethodCallParams( + method="mediumBox", + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_apps_external_unavailable_app(self) -> None: + with pytest.raises(LogicError, match="unavailable App"): + self.app_client.send.call( + AppClientMethodCallParams( + method="externalAppCall", + static_fee=AlgoAmount.from_micro_algo(2_000), + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_apps_external_app(self) -> None: + self.app_client.send.call( + AppClientMethodCallParams( + method="externalAppCall", + static_fee=AlgoAmount.from_micro_algo(2_000), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_assets_unavailable_asset(self) -> None: + with pytest.raises(LogicError, match="unavailable Asset"): + self.app_client.send.call( + AppClientMethodCallParams( + method="assetTotal", + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_assets_valid_asset(self) -> None: + self.app_client.send.call( + AppClientMethodCallParams( + method="assetTotal", + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_cross_product_reference_has_asset(self, funded_account: SigningAccount) -> None: + self.app_client.send.call( + AppClientMethodCallParams( + method="hasAsset", + args=[funded_account.address], + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_cross_product_reference_invalid_external_local(self, funded_account: SigningAccount) -> None: + with pytest.raises(LogicError, match="unavailable App"): + self.app_client.send.call( + AppClientMethodCallParams( + method="externalLocal", + args=[funded_account.address], + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_cross_product_reference_external_local( + self, external_client: AppClient, funded_account: SigningAccount, algorand: AlgorandClient + ) -> None: + algorand.send.app_call_method_call( + external_client.params.opt_in( + AppClientMethodCallParams( + method="optInToApplication", + sender=funded_account.address, + ), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + algorand.send.app_call_method_call( + self.app_client.params.call( + AppClientMethodCallParams( + method="externalLocal", + args=[funded_account.address], + sender=funded_account.address, + ), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_address_balance_invalid_account_reference( + self, + ) -> None: + with pytest.raises(LogicError, match="invalid Account reference"): + self.app_client.send.call( + AppClientMethodCallParams( + method="addressBalance", + args=[algosdk.account.generate_account()[1]], + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + def test_address_balance( + self, + ) -> None: + self.app_client.send.call( + AppClientMethodCallParams( + method="addressBalance", + args=[algosdk.account.generate_account()[1]], + on_complete=OnComplete.NoOpOC, + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_cross_product_reference_invalid_has_asset(self, funded_account: SigningAccount) -> None: + with pytest.raises(LogicError, match="unavailable Asset"): + self.app_client.send.call( + AppClientMethodCallParams( + method="hasAsset", + args=[funded_account.address], + ), + send_params={ + "populate_app_call_resources": False, + }, + ) + + +class TestResourcePackerAVM8(BaseResourcePackerTest): + """Test resource packing with AVM 8""" + + version = 8 + + +class TestResourcePackerAVM9(BaseResourcePackerTest): + """Test resource packing with AVM 9""" + + version = 9 + + +class TestResourcePackerMixed: + """Test resource packing with mixed AVM versions""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Create v8 app + v8_spec = load_arc32_spec(8) + v8_factory = algorand.client.get_app_factory( + app_spec=v8_spec, + default_sender=funded_account.address, + ) + self.v8_client, _ = v8_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + + # Create v9 app + v9_spec = load_arc32_spec(9) + v9_factory = algorand.client.get_app_factory( + app_spec=v9_spec, + default_sender=funded_account.address, + ) + self.v9_client, _ = v9_factory.send.create(params=AppFactoryCreateMethodCallParams(method="createApplication")) + + yield + + config.configure(populate_app_call_resources=False) + + def test_same_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: + rekeyed_to = algorand.account.random() + algorand.account.rekey_account(funded_account.address, rekeyed_to) + + random_account = algorand.account.random() + + txn_group = algorand.send.new_group() + txn_group.add_app_call_method_call( + self.v8_client.params.call( + AppClientMethodCallParams( + method="addressBalance", + args=[random_account.address], + sender=funded_account.address, + signer=rekeyed_to.signer, + ), + ), + ) + txn_group.add_app_call_method_call( + self.v9_client.params.call( + AppClientMethodCallParams( + method="addressBalance", + args=[random_account.address], + sender=funded_account.address, + signer=rekeyed_to.signer, + ) + ) + ) + + result = txn_group.send( + { + "populate_app_call_resources": True, + } + ) + + v8_accounts = getattr(result.transactions[0].application_call, "accounts", None) or [] + v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] + assert len(v8_accounts) + len(v9_accounts) == 1 + + def test_app_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: + self.v8_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(328500))) + self.v8_client.send.call( + AppClientMethodCallParams( + method="bootstrap", + static_fee=AlgoAmount.from_micro_algo(3_000), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + external_app_id = int(self.v8_client.get_global_state()["externalAppID"].value) + external_app_addr = algosdk.logic.get_application_address(external_app_id) + + txn_group = algorand.send.new_group() + txn_group.add_app_call_method_call( + self.v8_client.params.call( + AppClientMethodCallParams( + method="externalAppCall", + static_fee=AlgoAmount.from_micro_algo(2_000), + sender=funded_account.address, + ), + ), + ) + txn_group.add_app_call_method_call( + self.v9_client.params.call( + AppClientMethodCallParams( + method="addressBalance", + args=[external_app_addr], + sender=funded_account.address, + ) + ) + ) + + result = txn_group.send( + { + "populate_app_call_resources": True, + } + ) + + v8_apps = getattr(result.transactions[0].application_call, "foreign_apps", None) or [] + v9_accounts = getattr(result.transactions[1].application_call, "accounts", None) or [] + assert len(v8_apps) + len(v9_accounts) == 1 + + +class TestResourcePackerMeta: + """Test meta aspects of resource packing""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + external_spec = ( + Path(__file__).parent.parent / "artifacts" / "resource-packer" / "ExternalApp.arc32.json" + ).read_text() + factory = algorand.client.get_app_factory( + app_spec=external_spec, + default_sender=funded_account.address, + ) + self.external_client, _ = factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + yield + + config.configure(populate_app_call_resources=False) + + def test_error_during_simulate(self) -> None: + with pytest.raises(LogicError) as exc_info: + self.external_client.send.call( + AppClientMethodCallParams( + method="error", + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + assert "Error during resource population simulation in transaction 0" in exc_info.value.logic_error_str + + def test_box_with_txn_arg(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: + payment = PaymentTxn( + sender=funded_account.address, + receiver=funded_account.address, + amt=0, + sp=algorand.client.algod.suggested_params(), + ) + payment_with_signer = TransactionWithSigner(payment, funded_account.signer) + + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(106100))) + + self.external_client.send.call( + AppClientMethodCallParams( + method="boxWithPayment", + args=[payment_with_signer], + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + + def test_sender_asset_holding(self) -> None: + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_000))) + + self.external_client.send.call( + AppClientMethodCallParams( + method="createAsset", + static_fee=AlgoAmount.from_micro_algo(2_000), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + result = self.external_client.send.call(AppClientMethodCallParams(method="senderAssetBalance")) + + assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 + + def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: SigningAccount) -> None: + auth_addr = algorand.account.random() + algorand.account.rekey_account(funded_account.address, auth_addr) + + self.external_client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_micro_algo(200_001))) + + self.external_client.send.call( + AppClientMethodCallParams( + method="createAsset", + static_fee=AlgoAmount.from_micro_algo(2_001), + ), + send_params={ + "populate_app_call_resources": True, + }, + ) + result = self.external_client.send.call(AppClientMethodCallParams(method="senderAssetBalance")) + + assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 + + +class TestCoverAppCallInnerFees: + """Test covering app call inner transaction fees""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Load inner fee contract spec + spec_path = Path(__file__).parent.parent / "artifacts" / "inner-fee" / "application.json" + inner_fee_spec = json.loads(spec_path.read_text()) + + # Create app factory + factory = algorand.client.get_app_factory(app_spec=inner_fee_spec, default_sender=funded_account.address) + + # Create 3 app instances + self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app1")) + self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app2")) + self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app3")) + + # Fund app accounts + for client in [self.app_client1, self.app_client2, self.app_client3]: + client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algos(2))) + + yield + + config.configure(populate_app_call_resources=False) + + def test_throws_when_no_max_fee(self) -> None: + """Test that error is thrown when no max fee is supplied""" + with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): + self.app_client1.send.call( + AppClientMethodCallParams( + method="no_op", + ), + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_inner_fees_not_covered(self) -> None: + """Test that error is thrown when inner transaction fees are not covered""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + + with pytest.raises(Exception, match="fee too small"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": False, + }, + ) + + def test_does_not_alter_fee_without_inners(self) -> None: + """Test that fee is not altered when app call has no inner transactions""" + + expected_fee = 1000 + params = AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(2000), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_throws_when_max_fee_too_small(self) -> None: + """Test that error is thrown when max fee is too small to cover inner fees""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: + """Test that error is thrown when static fee is too small for inner transaction fees""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_alters_fee_handling_when_no_itxns_covered(self) -> None: + """Test that fee handling is altered when no inner transaction fees are covered""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_all_inners_covered(self) -> None: + """Test that fee handling is altered when all inner transaction fees are covered""" + + expected_fee = 1000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_some_inners_covered(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 5300 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_when_some_inners_have_surplus(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 2000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fees(self) -> None: + """Test that fee handling is altered when multiple app calls are in a group with inners with varying fees""" + txn_1_expected_fee = 5800 + txn_2_expected_fee = 6000 + + txn_1_params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], + static_fee=AlgoAmount.from_micro_algos(txn_1_expected_fee), + note=b"txn_1", + ) + + txn_2_params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(txn_2_expected_fee), + note=b"txn_2", + ) + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) + .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == txn_1_expected_fee + self._assert_min_fee(self.app_client1, txn_1_params, txn_1_expected_fee) + assert result.transactions[1].raw.fee == txn_2_expected_fee + self._assert_min_fee(self.app_client1, txn_2_params, txn_2_expected_fee) + + def test_does_not_alter_static_fee_with_surplus(self) -> None: + """Test that a static fee with surplus is not altered""" + + expected_fee = 6000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + static_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + + def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: + """Test fee handling with large inner fee surplus pooling to lower siblings""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: + """Test fee handling with inner fee surplus pooling to some lower siblings""" + + expected_fee = 6300 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: + """Test fee handling with large inner fee surplus but no pooling""" + + expected_fee = 10_000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) -> None: + """Test fee handling with multiple inner fee surplus poolings to lower siblings""" + + expected_fee = 7100 + params = AppClientMethodCallParams( + method="send_inners_with_fees_2", + args=[ + self.app_client2.app_id, + self.app_client3.app_id, + [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], + ], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: SigningAccount) -> None: + """Test that fee is not altered when another transaction in group covers inner fees""" + + expected_fee = 8000 + + result = ( + self.app_client1.algorand.new_group() + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == expected_fee + # We could technically reduce the below to 0, however it adds more complexity + # and is probably unlikely to be a common use case + assert result.transactions[1].raw.fee == 1000 + + def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: SigningAccount) -> None: + """Test that surplus fees are allocated to the most fee constrained transaction first""" + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(2000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(7500), + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(0), + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 7500 + assert result.transactions[2].raw.fee == 0 + + def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) -> None: + """Test fee handling with nested ABI method calls""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + # Setup transaction parameters + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(4000), + ) + ) + + payment_params = PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(1500), + ) + + expected_fee = 2000 + params = AppClientMethodCallParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment(payment_params), + txn_arg_call, + ], + static_fee=AlgoAmount.from_micro_algos(expected_fee), + ) + result = nested_client.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert len(result.transactions) == 3 + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 3500 + assert result.transactions[2].raw.fee == expected_fee + + self._assert_min_fee( + nested_client, + dataclasses.replace( + params, + args=[self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call], + ), + expected_fee, + ) + + def test_throws_when_max_fee_below_calculated(self) -> None: + """Test that error is thrown when max fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 1200 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(1200), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(10_000), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_throws_when_nested_max_fee_below_calculated(self, funded_account: SigningAccount) -> None: + """Test that error is thrown when nested max fee is below calculated fee""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algos(2000), + ) + ) + + with pytest.raises( + ValueError, match="Calculated transaction fee 5000 µALGO is greater than max of 2000 for transaction 1" + ): + nested_client.send.call( + AppClientMethodCallParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + ) + ), + txn_arg_call, + ], + max_fee=AlgoAmount.from_micro_algos(10_000), + ), + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_static_fee_below_calculated(self) -> None: + """Test that error is thrown when static fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 5000 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(5000), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algos(10_000), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: SigningAccount) -> None: + """Test that error is thrown when static fee for non-app-call transaction is too low""" + + with pytest.raises( + ValueError, match="An additional fee of 500 µALGO is required for non app call transaction 2" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(13_000), + max_fee=AlgoAmount.from_micro_algos(14_000), + ) + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algos(1000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algos(0), + static_fee=AlgoAmount.from_micro_algos(500), + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: + """Test fee handling with expensive ABI method calls that use ensure_budget to op-up""" + + expected_fee = 10_000 + params = AppClientMethodCallParams( + method="burn_ops", + args=[6200], + max_fee=AlgoAmount.from_micro_algos(12_000), + ) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert result.transaction.raw.fee == expected_fee + assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] + self._assert_min_fee(self.app_client1, params, expected_fee) + + def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallParams, fee: int) -> None: + """Helper to assert minimum required fee""" + if fee == 1000: + return + params_copy = dataclasses.replace( + params, + static_fee=None, + extra_fee=None, + ) + + with pytest.raises(Exception, match="fee too small"): + app_client.send.call(params_copy) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py new file mode 100644 index 00000000..473b6b73 --- /dev/null +++ b/tests/transactions/test_transaction_composer.py @@ -0,0 +1,432 @@ +import base64 +from collections.abc import Generator +from pathlib import Path +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock, patch + +import algosdk +import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + AssetConfigTxn, + AssetCreateTxn, + PaymentTxn, +) + +from algokit_utils._legacy_v2.account import get_account +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.account import MultisigMetadata, SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + PaymentParams, + SendAtomicTransactionComposerResults, + TransactionComposer, +) +from legacy_v2_tests.conftest import get_unique_name + +if TYPE_CHECKING: + from algokit_utils.models.transaction import Arc2TransactionNote + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture(autouse=True) +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.transactions.transaction_composer.config", new_callable=Mock) as mock_config: + mock_config.debug = True + mock_config.project_root = None + yield mock_config + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def funded_secondary_account(algorand: AlgorandClient) -> SigningAccount: + secondary_name = get_unique_name() + return get_account(algorand.client.algod, secondary_name) + + +def test_add_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + txn = PaymentTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + receiver=funded_account.address, + amt=AlgoAmount.from_algos(1).micro_algos, + ) + composer.add_transaction(txn) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], PaymentTxn) + assert built.transactions[0].sender == funded_account.address + assert built.transactions[0].receiver == funded_account.address + assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos + + +def test_add_asset_create(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + expected_total = 1000 + params = AssetCreateParams( + sender=funded_account.address, + total=expected_total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + + composer.add_asset_create(params) + built = composer.build_transactions() + response = composer.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"] + + assert len(response.tx_ids) == 1 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + assert isinstance(built.transactions[0], AssetCreateTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert created_asset["creator"] == funded_account.address + assert txn.total == created_asset["total"] == expected_total + assert txn.decimals == created_asset["decimals"] == 0 + assert txn.default_frozen == created_asset["default-frozen"] is False + assert txn.unit_name == created_asset["unit-name"] == "TEST" + assert txn.asset_name == created_asset["name"] == "Test Asset" + + +def test_add_asset_config( + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount +) -> None: + # First create an asset + asset_txn = AssetCreateTxn( + sender=funded_account.address, + sp=algorand.client.algod.suggested_params(), + total=1000, + decimals=0, + default_frozen=False, + unit_name="CFG", + asset_name="Configurable Asset", + manager=funded_account.address, + ) + signed_asset_txn = asset_txn.sign(funded_account.signer.private_key) + tx_id = algorand.client.algod.send_transaction(signed_asset_txn) + asset_before_config = algorand.client.algod.asset_info( + algorand.client.algod.pending_transaction_info(tx_id)["asset-index"] # type: ignore[call-overload] + ) + asset_before_config_index = asset_before_config["index"] # type: ignore[call-overload] + + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + params = AssetConfigParams( + sender=funded_account.address, + asset_id=asset_before_config_index, + manager=funded_secondary_account.address, + ) + composer.add_asset_config(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], AssetConfigTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.index == asset_before_config_index + assert txn.manager == funded_secondary_account.address + + composer.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 + + +def test_add_app_create(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + params = AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, + ) + composer.add_app_create(params) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], ApplicationCallTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + composer.send({"max_rounds_to_wait": 20}) + + +def test_add_app_call_method_call(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + 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, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, + ) + ) + response = composer.send() + app_id = algorand.client.algod.pending_transaction_info(response.tx_ids[0])["application-index"] # type: ignore[call-overload] + + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_app_call_method_call( + AppCallMethodCallParams( + sender=funded_account.address, + app_id=app_id, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["world"], + ) + ) + built = composer.build_transactions() + + assert len(built.transactions) == 1 + assert isinstance(built.transactions[0], ApplicationCallTxn) + txn = built.transactions[0] + assert txn.sender == funded_account.address + response = composer.send({"max_rounds_to_wait": 20}) + assert response.returns[-1].value == "Hello, world" + + +def test_simulate(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + composer.build() + simulate_response = composer.simulate() + assert simulate_response + + +def test_send(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + response = composer.send() + assert isinstance(response, SendAtomicTransactionComposerResults) + assert len(response.tx_ids) == 1 + assert response.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + + +def test_arc2_note() -> None: + note_data: Arc2TransactionNote = { + "dapp_name": "TestDApp", + "format": "j", + "data": '{"key":"value"}', + } + encoded_note = TransactionComposer.arc2_note(note_data) + expected_note = b'TestDApp:j{"key":"value"}' + assert encoded_note == expected_note + + +def test_arc2_note_dapp_name_validation() -> None: + invalid_names = [ + "_TestDApp", # starts with underscore + "Test", # too short + "a" * 33, # too long + "Test@App!", # invalid character ! + "Test App", # contains space + ] + + for invalid_name in invalid_names: + note_data: Arc2TransactionNote = {"dapp_name": invalid_name, "format": "j", "data": {"key": "value"}} + with pytest.raises(ValueError, match="dapp_name must be"): + TransactionComposer.arc2_note(note_data) + + +def test_arc2_note_valid_dapp_names() -> None: + valid_names = [ + "TestDApp", # simple case + "test-dapp", # with hyphen + "test_dapp", # with underscore + "test.dapp", # with dot + "test@dapp", # with @ + "test/dapp", # with / + "a" * 32, # maximum length + "12345", # minimum length, numeric + ] + + for valid_name in valid_names: + note_data: Arc2TransactionNote = {"dapp_name": valid_name, "format": "j", "data": {"key": "value"}} + encoded_note = TransactionComposer.arc2_note(note_data) + assert encoded_note.startswith(valid_name.encode()) + + +def _get_test_transaction( + default_account: SigningAccount, amount: AlgoAmount | None = None, sender: SigningAccount | None = None +) -> dict[str, Any]: + return { + "sender": sender.address if sender else default_account.address, + "receiver": default_account.address, + "amount": amount or AlgoAmount.from_algos(1), + } + + +def test_transaction_is_capped_by_low_min_txn_fee(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + with pytest.raises(ValueError, match="Transaction fee 1000 is greater than max_fee 1 µALGO"): + algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1)) + ) + + +def test_transaction_cap_is_ignored_if_higher_than_fee( + algorand: AlgorandClient, funded_account: SigningAccount +) -> None: + response = algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), max_fee=AlgoAmount.from_micro_algo(1_000_000)) + ) + assert isinstance(response.confirmation, dict) + assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_micro_algo(1000) + + +def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + response = algorand.send.payment( + PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algos(1)) + ) + assert isinstance(response.confirmation, dict) + assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algos(1) + + +def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(1)))) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(2)))) + response = composer.send() + + assert isinstance(response.confirmations[0], dict) + assert isinstance(response.confirmations[1], dict) + assert response.confirmations[0].get("txn", {}).get("txn", {}).get("grp") is not None + assert response.confirmations[1].get("txn", {}).get("txn", {}).get("grp") is not None + assert response.transactions[0].payment.group is not None + assert response.transactions[1].payment.group is not None + assert len(response.confirmations) == 2 + assert response.confirmations[0]["confirmed-round"] >= response.transactions[0].payment.first_valid_round + assert response.confirmations[1]["confirmed-round"] >= response.transactions[1].payment.first_valid_round + assert ( + response.confirmations[0]["txn"]["txn"]["grp"] + == base64.b64encode(response.transactions[0].payment.group).decode() + ) + assert ( + response.confirmations[1]["txn"]["txn"]["grp"] + == base64.b64encode(response.transactions[1].payment.group).decode() + ) + + +def test_multisig_single_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + multisig = algorand.account.multisig( + metadata=MultisigMetadata( + version=1, + threshold=1, + addresses=[funded_account.address], + ), + signing_accounts=[funded_account], + ) + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + ) + algorand.send.payment( + PaymentParams(sender=multisig.address, receiver=funded_account.address, amount=AlgoAmount.from_micro_algo(500)) + ) + + +def test_multisig_double_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + account2 = algorand.account.random() + algorand.account.ensure_funded(account2, funded_account, AlgoAmount.from_algos(10)) + + # Setup multisig + multisig = algorand.account.multisig( + metadata=MultisigMetadata( + version=1, + threshold=2, + addresses=[funded_account.address, account2.address], + ), + signing_accounts=[funded_account, account2], + ) + + # Fund multisig + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + ) + + # Use multisig + algorand.send.payment( + PaymentParams(sender=multisig.address, receiver=funded_account.address, amount=AlgoAmount.from_micro_algo(500)) + ) + + +@pytest.mark.usefixtures("mock_config") +def test_transactions_fails_in_debug_mode(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + txn1 = algorand.create_transaction.payment(PaymentParams(**_get_test_transaction(funded_account))) + txn2 = algorand.create_transaction.payment( + PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_micro_algo(9999999999999))) + ) + composer = TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: funded_account.signer, + ) + composer.add_transaction(txn1) + composer.add_transaction(txn2) + + with pytest.raises(Exception) as e: # noqa: PT011 + composer.send() + + assert f"transaction {txn2.get_txid()}: overspend" in e.value.traces[0]["failure_message"] # type: ignore[attr-defined] diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py new file mode 100644 index 00000000..a9916f96 --- /dev/null +++ b/tests/transactions/test_transaction_creator.py @@ -0,0 +1,272 @@ +from pathlib import Path + +import algosdk +import pytest +from algosdk.transaction import ( + ApplicationCallTxn, + AssetConfigTxn, + AssetCreateTxn, + AssetDestroyTxn, + AssetFreezeTxn, + AssetTransferTxn, + KeyregTxn, + PaymentTxn, +) + +from algokit_utils.algorand import AlgorandClient +from algokit_utils.models.account import SigningAccount +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OnlineKeyRegistrationParams, + PaymentParams, +) + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def funded_secondary_account(algorand: AlgorandClient, funded_account: SigningAccount) -> SigningAccount: + account = algorand.account.random() + algorand.send.payment( + PaymentParams(sender=funded_account.address, receiver=account.address, amount=AlgoAmount.from_algos(1)) + ) + return account + + +def test_create_payment_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + txn = algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_algos(1), + ) + ) + + assert isinstance(txn, PaymentTxn) + assert txn.sender == funded_account.address + assert txn.receiver == funded_account.address + assert txn.amt == AlgoAmount.from_algos(1).micro_algos + + +def test_create_asset_create_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + expected_total = 1000 + txn = algorand.create_transaction.asset_create( + AssetCreateParams( + sender=funded_account.address, + total=expected_total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + ) + + assert isinstance(txn, AssetCreateTxn) + assert txn.sender == funded_account.address + assert txn.total == expected_total + assert txn.decimals == 0 + assert txn.default_frozen is False + assert txn.unit_name == "TEST" + assert txn.asset_name == "Test Asset" + assert txn.url == "https://example.com" + + +def test_create_asset_config_transaction( + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount +) -> None: + txn = algorand.create_transaction.asset_config( + AssetConfigParams( + sender=funded_account.address, + asset_id=1, + manager=funded_secondary_account.address, + ) + ) + + assert isinstance(txn, AssetConfigTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.manager == funded_secondary_account.address + + +def test_create_asset_freeze_transaction( + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount +) -> None: + txn = algorand.create_transaction.asset_freeze( + AssetFreezeParams( + sender=funded_account.address, + asset_id=1, + account=funded_secondary_account.address, + frozen=True, + ) + ) + + assert isinstance(txn, AssetFreezeTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.target == funded_secondary_account.address + assert txn.new_freeze_state is True + + +def test_create_asset_destroy_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + txn = algorand.create_transaction.asset_destroy( + AssetDestroyParams( + sender=funded_account.address, + asset_id=1, + ) + ) + + assert isinstance(txn, AssetDestroyTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + + +def test_create_asset_transfer_transaction( + algorand: AlgorandClient, funded_account: SigningAccount, funded_secondary_account: SigningAccount +) -> None: + expected_amount = 100 + txn = algorand.create_transaction.asset_transfer( + AssetTransferParams( + sender=funded_account.address, + asset_id=1, + amount=expected_amount, + receiver=funded_secondary_account.address, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == expected_amount + assert txn.receiver == funded_secondary_account.address + + +def test_create_asset_opt_in_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + txn = algorand.create_transaction.asset_opt_in( + AssetOptInParams( + sender=funded_account.address, + asset_id=1, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == 0 + assert txn.receiver == funded_account.address + + +def test_create_asset_opt_out_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + txn = algorand.create_transaction.asset_opt_out( + AssetOptOutParams( + sender=funded_account.address, + asset_id=1, + creator=funded_account.address, + ) + ) + + assert isinstance(txn, AssetTransferTxn) + assert txn.sender == funded_account.address + assert txn.index == 1 + assert txn.amount == 0 + assert txn.receiver == funded_account.address + assert txn.close_assets_to == funded_account.address + + +def test_create_app_create_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + txn = algorand.create_transaction.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, + ) + ) + + assert isinstance(txn, ApplicationCallTxn) + assert txn.sender == funded_account.address + assert txn.approval_program == b"\x06\x81\x01" + assert txn.clear_program == b"\x06\x81\x01" + + +def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + approval_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "approval.teal").read_text() + clear_state_program = Path(Path(__file__).parent.parent / "artifacts" / "hello_world" / "clear.teal").read_text() + + # First create the app + create_result = algorand.send.app_create( + AppCreateParams( + sender=funded_account.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, + ) + ) + app_id = algorand.client.algod.pending_transaction_info(create_result.tx_ids[0])["application-index"] # type: ignore[call-overload] + + # Then test creating a method call transaction + result = algorand.create_transaction.app_call_method_call( + AppCallMethodCallParams( + sender=funded_account.address, + app_id=app_id, + method=algosdk.abi.Method.from_signature("hello(string)string"), + args=["world"], + ) + ) + + assert len(result.transactions) == 1 + assert isinstance(result.transactions[0], ApplicationCallTxn) + assert result.transactions[0].sender == funded_account.address + assert result.transactions[0].index == app_id + + +def test_create_online_key_registration_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: + sp = algorand.get_suggested_params() + expected_dilution = 100 + expected_first = sp.first + expected_last = sp.first + int(10e6) + + txn = algorand.create_transaction.online_key_registration( + OnlineKeyRegistrationParams( + sender=funded_account.address, + vote_key="G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=", + selection_key="LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=", + state_proof_key=b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", + vote_first=expected_first, + vote_last=expected_last, + vote_key_dilution=expected_dilution, + ) + ) + + assert isinstance(txn, KeyregTxn) + assert txn.sender == funded_account.address + assert txn.selkey == "LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=" + assert txn.sprfkey == b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==" + assert txn.votefst == expected_first + assert txn.votelst == expected_last + assert txn.votekd == expected_dilution diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py new file mode 100644 index 00000000..0820638d --- /dev/null +++ b/tests/transactions/test_transaction_sender.py @@ -0,0 +1,487 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch + +import algosdk +import pytest +from algosdk.transaction import OnComplete + +from algokit_utils import SigningAccount +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_manager import AppManager +from algokit_utils.assets.asset_manager import AssetManager +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import ( + AppCallMethodCallParams, + AppCallParams, + AppCreateParams, + AssetConfigParams, + AssetCreateParams, + AssetDestroyParams, + AssetFreezeParams, + AssetOptInParams, + AssetOptOutParams, + AssetTransferParams, + OfflineKeyRegistrationParams, + OnlineKeyRegistrationParams, + PaymentParams, + TransactionComposer, +) +from algokit_utils.transactions.transaction_sender import AlgorandClientTransactionSender + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + ) + algorand.set_signer(sender=new_account.address, signer=new_account.signer) + return new_account + + +@pytest.fixture +def sender(funded_account: SigningAccount) -> SigningAccount: + return funded_account + + +@pytest.fixture +def receiver(algorand: AlgorandClient) -> SigningAccount: + 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" / "app_spec.arc32.json" + return raw_json_spec.read_text() + + +@pytest.fixture +def test_hello_world_arc32_app_spec() -> ApplicationSpecification: + raw_json_spec = Path(__file__).parent.parent / "artifacts" / "hello_world" / "app_spec.arc32.json" + return ApplicationSpecification.from_json(raw_json_spec.read_text()) + + +@pytest.fixture +def test_hello_world_arc32_app_id( + algorand: AlgorandClient, funded_account: SigningAccount, test_hello_world_arc32_app_spec: ApplicationSpecification +) -> int: + global_schema = test_hello_world_arc32_app_spec.global_state_schema + local_schema = test_hello_world_arc32_app_spec.local_state_schema + 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_byte_slices": int(global_schema.num_byte_slices) if global_schema.num_byte_slices else 0, + "local_ints": int(local_schema.num_uints) if local_schema.num_uints else 0, + "local_byte_slices": int(local_schema.num_byte_slices) if local_schema.num_byte_slices else 0, + }, + ) + ) + return response.app_id + + +@pytest.fixture +def transaction_sender(algorand: AlgorandClient, sender: SigningAccount) -> AlgorandClientTransactionSender: + def new_group() -> TransactionComposer: + return TransactionComposer( + algod=algorand.client.algod, + get_signer=lambda _: sender.signer, + ) + + return AlgorandClientTransactionSender( + new_group=new_group, + asset_manager=AssetManager(algorand.client.algod, new_group), + app_manager=AppManager(algorand.client.algod), + algod_client=algorand.client.algod, + ) + + +def test_payment( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: + amount = AlgoAmount.from_algos(1) + result = transaction_sender.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=amount, + ) + ) + + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + txn = result.transaction.payment + assert txn + assert txn.sender == sender.address + assert txn.receiver == receiver.address + assert txn.amt == amount.micro_algos + + +def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: + total = 1000 + params = AssetCreateParams( + sender=sender.address, + total=total, + decimals=0, + default_frozen=False, + unit_name="TEST", + asset_name="Test Asset", + url="https://example.com", + ) + + result = transaction_sender.asset_create(params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + txn = result.transaction.asset_config + assert txn + assert txn.sender == sender.address + assert txn.total == total + assert txn.decimals == 0 + assert txn.default_frozen is False + assert txn.unit_name == "TEST" + assert txn.asset_name == "Test Asset" + assert txn.url == "https://example.com" + + +def test_asset_config( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="CFG", + asset_name="Config Asset", + url="https://example.com", + manager=sender.address, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then configure it + config_params = AssetConfigParams( + sender=sender.address, + asset_id=asset_id, + manager=receiver.address, + ) + result = transaction_sender.asset_config(config_params) + + assert len(result.tx_ids) == 1 + assert 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( + transaction_sender: AlgorandClientTransactionSender, + sender: SigningAccount, +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="FRZ", + url="https://example.com", + asset_name="Freeze Asset", + freeze=sender.address, + manager=sender.address, + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then freeze it + freeze_params = AssetFreezeParams( + sender=sender.address, + asset_id=asset_id, + account=sender.address, + frozen=True, + ) + result = transaction_sender.asset_freeze(freeze_params) + + assert len(result.tx_ids) == 1 + 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 + assert txn.new_freeze_state is True + + +def test_asset_destroy(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + unit_name="DEL", + asset_name="Delete Asset", + manager=sender.address, + url="https://example.com", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then destroy it + destroy_params = AssetDestroyParams( + sender=sender.address, + asset_id=asset_id, + ) + result = transaction_sender.asset_destroy(destroy_params) + + assert len(result.tx_ids) == 1 + txn = result.transaction.asset_config + assert txn + assert txn.sender == sender.address + assert txn.index == asset_id + + +def test_asset_transfer( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="XFR", + asset_name="Transfer Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in receiver + transaction_sender.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + ) + + # Then transfer it + amount = 100 + transfer_params = AssetTransferParams( + sender=sender.address, + asset_id=asset_id, + receiver=receiver.address, + amount=amount, + ) + result = transaction_sender.asset_transfer(transfer_params) + + assert len(result.tx_ids) == 1 + txn = result.transaction.asset_transfer + assert txn + assert txn.sender == sender.address + assert txn.index == asset_id + assert txn.receiver == receiver.address + assert txn.amount == amount + + +def test_asset_opt_in( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="OPT", + asset_name="Opt Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in + opt_in_params = AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + result = transaction_sender.asset_opt_in(opt_in_params) + + assert len(result.tx_ids) == 1 + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer + assert txn.sender == receiver.address + assert txn.index == asset_id + assert txn.amount == 0 + assert txn.receiver == receiver.address + + +def test_asset_opt_out( + transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount +) -> None: + # First create an asset + create_result = transaction_sender.asset_create( + AssetCreateParams( + sender=sender.address, + total=1000, + decimals=0, + default_frozen=False, + url="https://example.com", + unit_name="OUT", + asset_name="Opt Out Asset", + ) + ) + asset_id = int(create_result.confirmation["asset-index"]) # type: ignore[call-overload] + + # Then opt-in + transaction_sender.asset_opt_in( + AssetOptInParams( + sender=receiver.address, + asset_id=asset_id, + signer=receiver.signer, + ) + ) + + # Then opt-out + opt_out_params = AssetOptOutParams( + sender=receiver.address, + asset_id=asset_id, + creator=sender.address, + signer=receiver.signer, + ) + result = transaction_sender.asset_opt_out(params=opt_out_params) + + assert result.transaction.asset_transfer + txn = result.transaction.asset_transfer + assert txn.sender == receiver.address + assert txn.index == asset_id + assert txn.amount == 0 + assert txn.receiver == receiver.address + assert txn.close_assets_to == sender.address + + +def test_app_create(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: + approval_program = "#pragma version 6\nint 1" + clear_state_program = "#pragma version 6\nint 1" + params = AppCreateParams( + sender=sender.address, + approval_program=approval_program, + clear_state_program=clear_state_program, + schema={"global_ints": 0, "global_byte_slices": 0, "local_ints": 0, "local_byte_slices": 0}, + ) + + result = transaction_sender.app_create(params) + assert result.app_id > 0 + assert result.app_address + + 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" + + +def test_app_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount +) -> None: + params = AppCallParams( + app_id=test_hello_world_arc32_app_id, + sender=sender.address, + on_complete=OnComplete.NoOpOC, + args=[b"\x02\xbe\xce\x11", b"test"], + ) + + result = transaction_sender.app_call(params) + assert not result.abi_return # TODO: improve checks + + +def test_app_call_method_call( + test_hello_world_arc32_app_id: int, transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount +) -> None: + params = AppCallMethodCallParams( + 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.abi_return + assert result.abi_return.value == "Hello2, test" + + +@patch("logging.Logger.debug") +def test_payment_logging( + mock_debug: MagicMock, + transaction_sender: AlgorandClientTransactionSender, + sender: SigningAccount, + receiver: SigningAccount, +) -> None: + amount = AlgoAmount.from_algos(1) + transaction_sender.payment( + PaymentParams( + sender=sender.address, + receiver=receiver.address, + amount=amount, + ) + ) + + assert mock_debug.call_count == 1 + log_message = mock_debug.call_args[0][0] + assert "Sending 1,000,000 µALGO" in log_message + assert sender.address in log_message + assert receiver.address in log_message + + +def test_key_registration(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: + sp = transaction_sender._algod.suggested_params() # noqa: SLF001 + + params = OnlineKeyRegistrationParams( + sender=sender.address, + vote_key="G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=", + selection_key="LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=", + state_proof_key=b"RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", + vote_first=sp.first, + vote_last=sp.first + int(10e6), + vote_key_dilution=100, + ) + + result = transaction_sender.online_key_registration(params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] + + sp = transaction_sender._algod.suggested_params() # noqa: SLF001 + + off_key_reg_params = OfflineKeyRegistrationParams( + sender=sender.address, + prevent_account_from_ever_participating_again=True, + ) + + result = transaction_sender.offline_key_registration(off_key_reg_params) + assert len(result.tx_ids) == 1 + assert result.confirmations[-1]["confirmed-round"] > 0 # type: ignore[call-overload] diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..fc71eb75 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,54 @@ +from pathlib import Path +from typing import Literal, overload + +from algokit_utils.applications.app_manager import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, AppManager +from algokit_utils.applications.app_spec import Arc32Contract, Arc56Contract + + +@overload +def load_app_spec( + path: Path, + arc: Literal[32], + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> Arc32Contract: ... + + +@overload +def load_app_spec( + path: Path, + arc: Literal[56], + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> Arc56Contract: ... + + +def load_app_spec( + path: Path, + arc: Literal[32, 56], + *, + updatable: bool | None = None, + deletable: bool | None = None, + template_values: dict | None = None, +) -> Arc32Contract | Arc56Contract: + arc_class = Arc32Contract if arc == 32 else Arc56Contract + spec = arc_class.from_json(path.read_text(encoding="utf-8")) + + template_variables = template_values or {} + if updatable is not None: + template_variables["UPDATABLE"] = int(updatable) + + if deletable is not None: + template_variables["DELETABLE"] = int(deletable) + + if isinstance(spec, Arc32Contract): + spec.approval_program = ( + AppManager.replace_template_variables(spec.approval_program, template_variables) + .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") + .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") + ) + return spec