├── .github └── workflows │ └── pyteal-utils-build-test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __init__.py ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── api.rst │ ├── api │ ├── pytealutils.inline.rst │ ├── pytealutils.iter.rst │ ├── pytealutils.math.rst │ ├── pytealutils.rst │ ├── pytealutils.storage.rst │ ├── pytealutils.strings.rst │ └── pytealutils.transaction.rst │ ├── conf.py │ └── index.rst ├── examples ├── blob │ └── main.py └── iter │ └── main.py ├── pyproject.toml ├── pytealutils ├── __init__.py ├── debug │ ├── __init__.py │ └── debug.py ├── inline │ ├── __init__.py │ ├── inline_asm.py │ └── test_inline_asm.py ├── iter │ ├── __init__.py │ ├── iter.py │ └── test_iter.py ├── math │ ├── README.md │ ├── __init__.py │ ├── math.py │ ├── signed_int.py │ ├── test_math.py │ └── test_signed_int.py ├── storage │ ├── __init__.py │ ├── global_blob.py │ ├── local_blob.py │ ├── storage.py │ ├── test_global_blob.py │ ├── test_local_blob.py │ └── test_storage.py ├── strings │ ├── __init__.py │ ├── string.py │ └── test_string.py └── transaction │ ├── __init__.py │ ├── inner_transactions.py │ └── transaction.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── helpers.py └── teal_diff.py /.github/workflows/pyteal-utils-build-test.yaml: -------------------------------------------------------------------------------- 1 | name: PyTeal-Utils Build and Test validation 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build-backend: 7 | name: Build & Test PyTeal-Utils 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.10.4 17 | 18 | - name: Set up Poetry 19 | uses: abatilo/actions-poetry@v2.1.0 20 | with: 21 | poetry-version: 1.1.6 22 | 23 | - name: Install Python dependencies 24 | run: poetry install 25 | 26 | - uses: pre-commit/action@v3.0.0 27 | name: "Linters and formatters check" 28 | with: 29 | extra_args: --all-files 30 | 31 | - name: Clone Algorand Sanbox 32 | run: cd .. && git clone https://github.com/algorand/sandbox.git 33 | 34 | - name: Run Algorand Sandbox 35 | shell: 'script -q -e -c "bash {0}"' # hacky hack to make TTY work 36 | run: cd ../sandbox && ./sandbox up dev -v 37 | 38 | - name: Delay before testing 39 | run: sleep 60 40 | 41 | - name: Run pytest 42 | run: poetry run pytest 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pyteal_utils.egg-info/ 2 | __pycache__/ 3 | .venv/ 4 | .vscode/ 5 | 6 | # Sphinx documentation 7 | docs/build/ 8 | poetry.lock 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/pycqa/isort 13 | rev: 5.9.3 14 | hooks: 15 | - id: isort 16 | - repo: https://github.com/myint/autoflake 17 | rev: v1.4 18 | hooks: 19 | - id: autoflake 20 | args: 21 | - --in-place 22 | - --remove-unused-variables 23 | - --remove-all-unused-imports 24 | - --expand-star-imports 25 | - --ignore-init-module-imports 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | If you are interested in contributing to the project, we welcome and thank you. 4 | We want to make PyTEAL the best and easiest way to build on Algorand Virtual Machine. 5 | 6 | We appreciate your willingness to help us. 7 | 8 | ## Pull Requests guidelines for new PyTEAL utils 9 | 10 | Pull Requests (PR) are the best way to propose your PyTEAL utils. 11 | 12 | We actively welcome and support community proposals to grow PyTEAL utils, 13 | let's do it in a clean, organized, tested and well documented way: 14 | 15 | 1. Fork this repo and create your branch from `main`, please name it `util/...` 16 | 2. Each PyTEAL utils `subpackage` must have its own folder inside `pytealutils`. 17 | 3. New PyTEAL utils must be tested. Please follow `pytest` best practices. 18 | 4. Unit Tests: add specific unit-tests as `*_test.py` in the same `subpackage` folder! 19 | 5. E2E Tests: add end-to-end tests as `*_test.py` in the [tests](https://github.com/algorand/pyteal-utils/tree/main/tests) folder. 20 | 6. Verify coding style consistency with `pre-commit run --all-files` before submitting your PR! 21 | 7. Ensure all your specifc unit-tests or end-to-end pass! 22 | 8. Make sure your code lints. 23 | 9. New PyTEAL utils must be documented. Examples are useful too. [Get inspired](https://github.com/algorand/pyteal/blob/master/pyteal/ast/cond.py#L18)! 24 | 10. Issue your PR! 25 | 26 | ### Running tests 27 | 28 | The Sandbox repository has to either be available at `../sandbox` or set via `ALGORAND_SANBOX_DIR`. 29 | 30 | ```shell 31 | (.venv) pytest 32 | ``` 33 | 34 | ## Testing utils 35 | 36 | How many times you already found your self copying&pasting the same helper 37 | functions and classes from different projects? We want to avoid this! 38 | 39 | To ensure homogeneous and repeatable tests please use common helpers from 40 | [utils.py](https://github.com/algorand/pyteal-utils/blob/main/tests/utils.py). 41 | Growing and maintaining common helpers avoid code repetition and fragmentation 42 | for those general utilities like: instantiate an Algod client, create and fund 43 | an Accout in Sandbox, etc. 44 | 45 | Unless your test *really* needs ad-hoc helpers, the best practice is using 46 | common helpers. 47 | 48 | Do you think a really useful helper for everybody is missing? Please consider [filing an issue](https://help.github.com/en/articles/creating-an-issue) providing: 49 | 50 | 1. Helper function/class name 51 | 2. Helper function/class short description 52 | 3. Optional: implementation draft/proposal 53 | 54 | ## Request new PyTEAL utils 55 | 56 | Want to suggest a new PyTEAL utils to the community? Please consider [filing an issue](https://help.github.com/en/articles/creating-an-issue) providing: 57 | 58 | 1. PyTEAL utils name 59 | 2. PyTEAL utils short description 60 | 3. Optional: implementation draft/proposal 61 | 62 | ## Use a Consistent Coding Style 63 | 64 | We recommand the contributing code following [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/). 65 | 66 | We rely on pre-commit and hooks running recommended validators. Refer to `pre-commit-config.yaml`, make sure to have the hook activated when commiting to this repository. 67 | 68 | 69 | ## License 70 | 71 | By contributing, you agree that your contributions will be licensed under its MIT License. 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Algorand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyteal-utils 2 | 3 | *EXPERIMENTAL WIP* 4 | 5 | There is no guarantee to the API of this repository. It is subject to change without a tagged release. 6 | 7 | This repository is meant to contain PyTEAL utility methods common in many Smart Contract programs. 8 | 9 | ## Contents 10 | 11 | - [pyteal-utils](#pyteal-utils) 12 | - [Contents](#contents) 13 | - [Utils](#utils) 14 | - [Inline Assembly](#inline-assembly) 15 | - [Iter](#iter) 16 | - [Math](#math) 17 | - [Storage](#storage) 18 | - [Strings](#strings) 19 | - [Transactions](#transactions) 20 | - [Contributing](#contributing) 21 | - [Prerequisites](#prerequisites) 22 | - [Set up your PyTEAL environment](#set-up-your-pyteal-environment) 23 | 24 | ## Utils 25 | 26 | ### Inline Assembly 27 | 28 | - `InlineAssembly` - Can be used to inject TEAL source directly into a PyTEAL program 29 | 30 | ### Iter 31 | 32 | - `accumulate` 33 | - `iterate` - Provides a convenience method for calling a method n times 34 | 35 | ### Math 36 | 37 | - `odd` - Returns 1 if `x` is odd 38 | - `even` - Returns 1 if `x` is even 39 | - `factorial` - Returns `x! = x * x-1 * x-2 * ...` 40 | - `wide_factorial` - Returns `x! = x * x-1 * x-2 * ...` 41 | - `wide_power` 42 | - `exponential` - Approximates `e ** x` for `n` iterations 43 | - `log2` 44 | - `log10` - Returns log base `10` of the integer passed 45 | - `ln` - Returns natural log of `x` of the integer passed 46 | - `pow10` - Returns `10 ** x` 47 | - `max` - Returns the maximum of 2 integers 48 | - `min` - Returns the minimum of 2 integers 49 | - `div_ceil` - Returns the result of division rounded up to the next integer 50 | - `saturation` - Returns an output that is the value of _n_ bounded to the _upper_ and _lower_ saturation values 51 | 52 | ### Storage 53 | 54 | - `GlobalBlob` - Class holding static methods to work with the global storage of an application as a binary large object 55 | - `LocalBlob` - Class holding static methods to work with the local storage of an application as a binary large object 56 | - `global_must_get` - Returns the result of a global storage MaybeValue if it exists, else Assert and fail the program 57 | - `global_get_else` - Returns the result of a global storage MaybeValue if it exists, else return a default value 58 | - `local_must_get` - Returns the result of a loccal storage MaybeValue if it exists, else Assert and fail the program 59 | - `local_get_else` - Returns the result of a local storage MaybeValue if it exists, else return a default value 60 | 61 | ### Strings 62 | 63 | - `atoi` - Converts a byte string representing a number to the integer value it represents 64 | - `itoa` - Converts an integer to the ascii byte string it represents 65 | - `witoa` - Converts an byte string interpreted as an integer to the ascii byte string it represents 66 | - `head` - Gets the first byte from a bytestring, returns as bytes 67 | - `tail` - Returns the string with the first character removed 68 | - `suffix` - Returns the last n bytes of a given byte string 69 | - `prefix` - Returns the first n bytes of a given byte string 70 | - `rest` 71 | - `encode_uvarint` - Returns the uvarint encoding of an integer 72 | 73 | ### Transactions 74 | 75 | - `assert_common_checks` - Calls all txn checker assert methods 76 | - `assert_min_fee` - Checks that the fee for a transaction is exactly equal to the current min fee 77 | - `assert_no_rekey` - Checks that the rekey_to field is empty, Assert if it is set 78 | - `assert_no_close_to` - Checks that the close_remainder_to field is empty, Assert if it is set 79 | - `assert_no_asset_close_to` - Checks that the asset_close_to field is empty, Assert if it is set 80 | 81 | Common inner transaction operations 82 | 83 | - `pay` 84 | - `axfer` 85 | 86 | ## Contributing 87 | 88 | As [PyTEAL](https://github.com/algorand/pyteal) user, your contribution is extremely valuable to grow PyTEAL utilities! 89 | 90 | Please follow the [contribution guide](https://github.com/algorand/pyteal-utils/blob/main/CONTRIBUTING.md)! 91 | 92 | ## Prerequisites 93 | 94 | - [poetry](https://python-poetry.org/) 95 | - [pre-commit](https://pre-commit.com/) 96 | - [py-algorand-sdk](https://github.com/algorand/py-algorand-sdk) 97 | - [pyteal](https://github.com/algorand/pyteal) 98 | - [pytest](https://docs.pytest.org/) 99 | - [Docker Compose](https://docs.docker.com/compose/install/) 100 | - [Algorand Sandbox](https://github.com/algorand/sandbox) 101 | 102 | ### Set up your PyTEAL environment 103 | 104 | 1. Set up the [sandbox](https://github.com/algorand/sandbox) and start it (`dev` mode recommended): `./sandbox up dev` 105 | 2. Clone this repo: `git clone https://github.com/algorand/pyteal-utils.git` and `cd` into the `pyteal-utils` directory 106 | 3. Install Python dependecies: `poetry install` 107 | 4. Activate a virual env: `poetry shell` 108 | 5. Configure pre-commit hooks: `pre-commit install` 109 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorand/pyteal-utils/c4976887ddd959d285894cf7d3e0feddef3821ba/__init__.py -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: buildapi Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | buildapi: 23 | sphinx-apidoc -fMET ../pytealutils -o source/api 24 | @echo "Auto-generation of API documentation finished. " \ 25 | "The generated files are in 'api/'" 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | Information on specific functions, classes, and methods. 5 | 6 | .. toctree:: 7 | :glob: 8 | 9 | api/* 10 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.inline.rst: -------------------------------------------------------------------------------- 1 | pytealutils.inline package 2 | ========================== 3 | 4 | .. automodule:: pytealutils.inline 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.inline.inline_asm 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.inline.test_inline_asm 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.iter.rst: -------------------------------------------------------------------------------- 1 | pytealutils.iter package 2 | ======================== 3 | 4 | .. automodule:: pytealutils.iter 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.iter.iter 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.iter.test_iter 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.math.rst: -------------------------------------------------------------------------------- 1 | pytealutils.math package 2 | ======================== 3 | 4 | .. automodule:: pytealutils.math 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.math.math 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.math.signed_int 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | 25 | .. automodule:: pytealutils.math.test_math 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | .. automodule:: pytealutils.math.test_signed_int 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.rst: -------------------------------------------------------------------------------- 1 | pytealutils package 2 | =================== 3 | 4 | .. automodule:: pytealutils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | pytealutils.inline 16 | pytealutils.iter 17 | pytealutils.math 18 | pytealutils.storage 19 | pytealutils.strings 20 | pytealutils.transaction 21 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.storage.rst: -------------------------------------------------------------------------------- 1 | pytealutils.storage package 2 | =========================== 3 | 4 | .. automodule:: pytealutils.storage 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.storage.global_blob 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.storage.local_blob 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | 25 | .. automodule:: pytealutils.storage.storage 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | .. automodule:: pytealutils.storage.test_global_blob 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | 37 | .. automodule:: pytealutils.storage.test_local_blob 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | 43 | .. automodule:: pytealutils.storage.test_storage 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.strings.rst: -------------------------------------------------------------------------------- 1 | pytealutils.strings package 2 | =========================== 3 | 4 | .. automodule:: pytealutils.strings 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.strings.string 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.strings.test_string 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/api/pytealutils.transaction.rst: -------------------------------------------------------------------------------- 1 | pytealutils.transaction package 2 | =============================== 3 | 4 | .. automodule:: pytealutils.transaction 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | 13 | .. automodule:: pytealutils.transaction.inner_transactions 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | 19 | .. automodule:: pytealutils.transaction.transaction 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath(".")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "pyteal-utils" 22 | copyright = "2022, Algorand" 23 | author = "Algorand" 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | 32 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"] 33 | source_suffix = [".rst", ".md"] 34 | master_doc = "index" 35 | 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = [] 41 | 42 | napoleon_include_init_with_doc = True 43 | 44 | templates_path = ["_templates"] 45 | 46 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "api/pytealutils.rst"] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = "sphinx_rtd_theme" 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ["_static"] 60 | 61 | 62 | # This is the expected signature of the handler for this event, cf doc 63 | def autodoc_skip_member_handler(app, what, name, obj, skip, options): 64 | return name.startswith("test_") 65 | 66 | 67 | # Automatically called by sphinx at startup 68 | def setup(app): 69 | app.connect("autodoc-skip-member", autodoc_skip_member_handler) 70 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyteal-utils documentation master file, created by 2 | sphinx-quickstart on Mon Jan 10 11:11:20 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyteal-utils's documentation! 7 | ======================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | .. toctree:: 14 | api 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /examples/blob/main.py: -------------------------------------------------------------------------------- 1 | from pyteal import ( 2 | Assert, 3 | Bytes, 4 | Cond, 5 | Int, 6 | Len, 7 | Log, 8 | Mode, 9 | OnComplete, 10 | Pop, 11 | Seq, 12 | Txn, 13 | compileTeal, 14 | ) 15 | 16 | from pytealutils.storage import LocalBlob 17 | 18 | 19 | def demo_application(): 20 | b = LocalBlob() 21 | 22 | data = Bytes("base16", "deadbeef" * 16) 23 | test = Seq( 24 | b.zero(Int(0)), # Required on initialization 25 | Pop( 26 | b.write(Int(0), Int(0), data) 27 | ), # write returns the number of bits written, just pop it 28 | Log( 29 | b.read(Int(0), Int(0), Int(32)) 30 | ), # Should return the first 32 bytes of `data` 31 | Assert(b.read(Int(0), Int(0), Len(data)) == data), # Should pass assert 32 | Int(1), 33 | ) 34 | return Cond( 35 | [Txn.application_id() == Int(0), Int(1)], 36 | [Txn.on_completion() == OnComplete.OptIn, Int(1)], 37 | [Txn.on_completion() == OnComplete.UpdateApplication, Int(1)], 38 | [Int(1), test], 39 | ) 40 | 41 | 42 | print(compileTeal(demo_application(), mode=Mode.Application, version=5)) 43 | -------------------------------------------------------------------------------- /examples/iter/main.py: -------------------------------------------------------------------------------- 1 | from pyteal import * 2 | 3 | from pytealutils.iter import * 4 | 5 | 6 | def demo_application(): 7 | 8 | i = ScratchVar() 9 | test = Seq(iterate(Log(Itob(i.load())), Int(10), i), Int(1)) 10 | return Cond( 11 | [Txn.application_id() == Int(0), Int(1)], 12 | [Txn.on_completion() == OnComplete.OptIn, Int(1)], 13 | [Txn.on_completion() == OnComplete.UpdateApplication, Int(1)], 14 | [Int(1), test], 15 | ) 16 | 17 | 18 | print(compileTeal(demo_application(), mode=Mode.Application, version=5)) 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pyteal-utils" 3 | version = "0.0.1" 4 | description = "" 5 | authors = ["Algorand Community "] 6 | packages = [ 7 | { include = "pytealutils" } 8 | ] 9 | 10 | [tool.poetry.dependencies] 11 | python = ">=3.10" 12 | pyteal = ">=0.13.0" 13 | py-algorand-sdk = ">=1.13.1" 14 | pytest = ">=6.2.5" 15 | pytest-xdist = "^2.4.0" 16 | PyYAML = "^6.0" 17 | pydantic = "^1.8.2" 18 | pytest-depends = "^1.0.1" 19 | 20 | [tool.poetry.dev-dependencies] 21 | black = {version = "^21.9b0", allow-prereleases = true} 22 | 23 | [build-system] 24 | requires = ["setuptools", "poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | 27 | [tool.isort] 28 | profile = "black" 29 | -------------------------------------------------------------------------------- /pytealutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorand/pyteal-utils/c4976887ddd959d285894cf7d3e0feddef3821ba/pytealutils/__init__.py -------------------------------------------------------------------------------- /pytealutils/debug/__init__.py: -------------------------------------------------------------------------------- 1 | from .debug import log_stats 2 | -------------------------------------------------------------------------------- /pytealutils/debug/debug.py: -------------------------------------------------------------------------------- 1 | from pyteal import Bytes, Concat, Global, Log 2 | 3 | from ..strings import itoa 4 | 5 | 6 | def log_stats(): 7 | return Log( 8 | Concat( 9 | Bytes("Current App Id: "), 10 | itoa(Global.current_application_id()), 11 | Bytes("Caller App Id: "), 12 | itoa(Global.caller_app_id()), 13 | Bytes("Budget: "), 14 | itoa(Global.opcode_budget()), 15 | Bytes("Group size: "), 16 | itoa(Global.group_size()), 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /pytealutils/inline/__init__.py: -------------------------------------------------------------------------------- 1 | from .inline_asm import InlineAssembly 2 | -------------------------------------------------------------------------------- /pytealutils/inline/inline_asm.py: -------------------------------------------------------------------------------- 1 | from pyteal import CompileOptions, Expr, LeafExpr, Mode, TealBlock, TealOp, TealType 2 | 3 | # Credit Julian (RandLabs) 4 | 5 | 6 | class CustomOp: 7 | def __init__(self, opcode): 8 | self.opcode = opcode 9 | self.mode = Mode.Signature | Mode.Application 10 | self.min_version = 2 11 | 12 | def __str__(self) -> str: 13 | return self.opcode 14 | 15 | 16 | class InlineAssembly(LeafExpr): 17 | """InlineAssembly can be used to inject TEAL source directly into a PyTEAL program 18 | 19 | Often the generated PyTEAL is not as efficient as it can be. This class offers a way to 20 | write the most efficient TEAL for code paths that would otherwise be impossible because 21 | of opcode budget constraints. 22 | 23 | It can also be used to implement methods that are not yet available in the PyTEAL repository 24 | 25 | Args: 26 | opcode: string containing the teal to inject 27 | args: any number of PyTEAL expressions to place before this opcode 28 | type: The type this Expression returns, to help during PyTEAL compilation 29 | 30 | """ 31 | 32 | def __init__( 33 | self, opcode: str, *args: "Expr", type: TealType = TealType.none 34 | ) -> None: 35 | super().__init__() 36 | opcode_with_args = opcode.split(" ") 37 | self.op = CustomOp(opcode_with_args[0]) 38 | self.type = type 39 | self.opcode_args = opcode_with_args[1:] 40 | self.args = args 41 | 42 | def __teal__(self, options: "CompileOptions"): 43 | op = TealOp(self, self.op, *self.opcode_args) 44 | return TealBlock.FromOp(options, op, *self.args[::1]) 45 | 46 | def __str__(self): 47 | return "(InlineAssembly: {})".format(self.op.opcode) 48 | 49 | def type_of(self): 50 | return self.type 51 | -------------------------------------------------------------------------------- /pytealutils/inline/test_inline_asm.py: -------------------------------------------------------------------------------- 1 | from pyteal import * 2 | 3 | from tests.helpers import * 4 | 5 | from .inline_asm import InlineAssembly 6 | 7 | 8 | def test_inline_assembly(): 9 | get_uint8 = """ 10 | extract 7 1 11 | """ 12 | s = ScratchSlot() 13 | expr = Seq(InlineAssembly(get_uint8, Itob(Int(255))), s.store(), Log(s.load())) 14 | 15 | expected = [logged_int(255)[-2:]] 16 | assert_output(expr, expected) 17 | 18 | 19 | # def test_inline_assembly_invalid(): 20 | # get_uint8 = """ 21 | # extract a b 22 | # """ 23 | # s = ScratchSlot() 24 | # expr = Seq(InlineAssembly(get_uint8, Itob(Int(255))), s.store(), Log(s.load())) 25 | # 26 | # expected = [ 27 | # INVALID_SYNTAX 28 | # ] # TODO: in the next version, this will be "unable to parse" 29 | # assert_fail(expr, expected) 30 | -------------------------------------------------------------------------------- /pytealutils/iter/__init__.py: -------------------------------------------------------------------------------- 1 | from .iter import accumulate, iterate 2 | -------------------------------------------------------------------------------- /pytealutils/iter/iter.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyteal import BinaryExpr, Expr, For, Int, Op, ScratchVar, Seq, Subroutine, TealType 4 | 5 | 6 | def accumulate(vals: List[Expr], op: Op) -> Expr: 7 | """accumulate provides a convenient way to efficiently combine elements in a list. 8 | 9 | Pairs elements of the vals list recursively to save on ops and reduce the need for intermediate scratch var storing/loading 10 | 11 | Typical Usage Example: 12 | accumulate([Int(1), Int(2), Int(3), Int(4)], Op.Add) # comes out to (1+2)+(3+4) == 10 13 | 14 | Args: 15 | vals: A List of Expr objects to evaluate, these should evaluate to a Bytes or Int 16 | op: A teal operation to perform on the list 17 | 18 | Returns: 19 | A single expression representing the accumulated value after applying the op for all elements 20 | 21 | """ 22 | ops: List[Expr] = [] 23 | for n in range(0, len(vals) - 1, 2): 24 | ops.append( 25 | BinaryExpr(op, TealType.uint64, TealType.uint64, vals[n], vals[n + 1]) 26 | ) 27 | 28 | # If its an odd number, we cant match it, just add the last one 29 | if len(vals) % 2 == 1: 30 | ops.append(vals[-1]) 31 | 32 | if len(ops) > 1: 33 | return accumulate(ops, op) 34 | 35 | return Seq(ops) 36 | 37 | 38 | def iterate(sub: Expr, n: Int, i: ScratchVar = None) -> Expr: 39 | """Iterate provides a convenience method for calling a method n times 40 | 41 | Args: 42 | sub: A PyTEAL Expr to call, should not return anything 43 | n: The number of times to call the expression 44 | i: (Optional) A ScratchVar to use for iteration, passed if the caller wants to access the iterator 45 | 46 | Returns: 47 | A Subroutine expression to be passed directly into an Expr tree 48 | """ 49 | if i is None: 50 | i = ScratchVar() 51 | 52 | @Subroutine(TealType.none) 53 | def _impl() -> Expr: 54 | init = i.store(Int(0)) 55 | cond = i.load() < n 56 | iter = i.store(i.load() + Int(1)) 57 | return For(init, cond, iter).Do(sub) 58 | 59 | return _impl() 60 | -------------------------------------------------------------------------------- /pytealutils/iter/test_iter.py: -------------------------------------------------------------------------------- 1 | from pyteal import ( 2 | Bytes, 3 | Int, 4 | Itob, 5 | Log, 6 | Op, 7 | ScratchVar, 8 | Subroutine, 9 | SubroutineCall, 10 | TealType, 11 | ) 12 | 13 | from tests.helpers import assert_output, logged_bytes, logged_int 14 | 15 | from .iter import accumulate, iterate 16 | 17 | 18 | def test_iterate(): 19 | expr = iterate(Log(Bytes("a")), Int(10)) 20 | assert type(expr) is SubroutineCall 21 | 22 | output = [logged_bytes("a")] * 10 23 | assert_output(expr, output) 24 | 25 | 26 | def test_iterate_with_closure(): 27 | i = ScratchVar() 28 | 29 | @Subroutine(TealType.none) 30 | def logthing(): 31 | return Log(Itob(i.load())) 32 | 33 | expr = iterate(logthing(), Int(10), i) 34 | assert type(expr) is SubroutineCall 35 | 36 | output = [logged_int(x) for x in range(10)] 37 | assert_output(expr, output) 38 | 39 | 40 | def test_accumulate(): 41 | expr = Log(Itob(accumulate([Int(1) for _ in range(10)], Op.add))) 42 | output = [logged_int(10)] 43 | assert_output(expr, output) 44 | -------------------------------------------------------------------------------- /pytealutils/math/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Fixed Point 5 | https://en.wikipedia.org/wiki/Fixed-point_arithmetic 6 | 7 | https://en.wikipedia.org/wiki/Binary_logarithm#Iterative_approximation 8 | 9 | https://dsp.stackexchange.com/questions/15790/inverse-fixed-point-number-number-1 10 | 11 | https://inst.eecs.berkeley.edu/~cs61c/sp06/handout/fixedpt.html 12 | -------------------------------------------------------------------------------- /pytealutils/math/__init__.py: -------------------------------------------------------------------------------- 1 | from .math import div_ceil, max, min, pow10 2 | -------------------------------------------------------------------------------- /pytealutils/math/math.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from pyteal import ( 4 | BitLen, 5 | BytesAdd, 6 | BytesDiv, 7 | BytesMinus, 8 | BytesMul, 9 | BytesZero, 10 | Concat, 11 | Exp, 12 | Expr, 13 | ExtractUint64, 14 | If, 15 | Int, 16 | Itob, 17 | Len, 18 | Not, 19 | Return, 20 | ScratchSlot, 21 | Seq, 22 | Subroutine, 23 | TealType, 24 | ) 25 | 26 | from ..inline.inline_asm import InlineAssembly 27 | 28 | _scale = 1000000 29 | _log2_10 = math.log2(10) 30 | _log2_e = math.log2(math.e) 31 | 32 | _max_uint = (2**64) - 1 33 | _half_uint = (2**32) - 1 34 | 35 | log2_10 = Int(int(_log2_10 * _scale)) 36 | log2_e = Int(int(_log2_e * _scale)) 37 | scale = Int(_scale) 38 | 39 | max_uint = Int(_max_uint) 40 | half_uint = Int(_half_uint) 41 | 42 | 43 | @Subroutine(TealType.uint64) 44 | def odd(x): 45 | """odd returns 1 if x is odd 46 | 47 | Args: 48 | x: uint64 to evaluate 49 | 50 | Returns: 51 | uint64 1 or 0 where 1 is true and 0 is false 52 | 53 | """ 54 | return x & Int(1) 55 | 56 | 57 | @Subroutine(TealType.uint64) 58 | def even(x): 59 | """even returns 1 if x is even 60 | 61 | Args: 62 | x: uint64 to evaluate 63 | 64 | Returns: 65 | uint64 1 or 0 where 1 is true and 0 is false 66 | 67 | 68 | """ 69 | return Not(odd(x)) 70 | 71 | 72 | @Subroutine(TealType.uint64) 73 | def factorial(x): 74 | """factorial returns x! = x * x-1 * x-2 * ..., 75 | for a 64bit integer, the max possible value is maxes out at 20 76 | 77 | Args: 78 | x: uint64 to call evaluate 79 | 80 | Returns: 81 | uint64 representing the factorial of the argument passed 82 | 83 | """ 84 | return If(x == Int(1), x, x * factorial(x - Int(1))) 85 | 86 | 87 | @Subroutine(TealType.bytes) 88 | def wide_factorial(x): 89 | """factorial returns x! = x * x-1 * x-2 * ..., 90 | 91 | Args: 92 | x: bytes to evaluate as an integer 93 | 94 | Returns: 95 | bytes representing the integer that is the result of the factorial applied on the argument passed 96 | 97 | """ 98 | return If( 99 | BitLen(x) == Int(1), x, BytesMul(x, wide_factorial(BytesMinus(x, Itob(Int(1))))) 100 | ) 101 | 102 | 103 | @Subroutine(TealType.bytes) 104 | def wide_power(x, n): 105 | """wide_power returns the result of x^n evaluated using expw and combining the hi/low uint64s into a byte string 106 | 107 | Args: 108 | x: uint64 base for evaluation 109 | n: uint64 power to apply as exponent 110 | 111 | Returns: 112 | bytes representing the high and low bytes of a wide power evaluation 113 | 114 | """ 115 | return Seq(InlineAssembly("expw", x, n), stack_to_wide()) 116 | 117 | 118 | def exponential(x, n): 119 | """exponential approximates e**x for n iterations 120 | 121 | TODO: currently this is scaled to 1000 first then scaled back. A better implementation should include the use of ufixed in abi types 122 | 123 | Args: 124 | x: The exponent to apply 125 | n: The number of iterations, more is better appx but costs ops 126 | 127 | Returns: 128 | uint64 that is the result of raising e**x 129 | 130 | """ 131 | _scale = Itob(Int(1000)) 132 | 133 | @Subroutine(TealType.bytes) 134 | def _impl(x: TealType.bytes, f: TealType.bytes, n: TealType.uint64): 135 | return If( 136 | n == Int(1), 137 | BytesAdd(_scale, BytesMul(x, _scale)), 138 | BytesAdd( 139 | _impl(x, BytesDiv(f, Itob(n)), n - Int(1)), 140 | BytesDiv(BytesMul(_scale, wide_power(bytes_to_int(x), n)), f), 141 | ), 142 | ) 143 | 144 | return bytes_to_int(BytesDiv(_impl(Itob(x), wide_factorial(Itob(n)), n), _scale)) 145 | 146 | 147 | @Subroutine(TealType.uint64) 148 | def log2(x): 149 | """log2 is currently an alias for BitLen 150 | 151 | TODO: implement with ufixed 152 | 153 | """ 154 | return BitLen(x) # Only returns integral component 155 | 156 | 157 | @Subroutine(TealType.uint64) 158 | def ln(x): 159 | """Returns natural log of x for integer passed 160 | 161 | This is heavily truncated since log2 does not return the fractional component yet 162 | 163 | TODO: implement with ufixed 164 | 165 | Args: 166 | x: uint64 on which to take the natural log 167 | 168 | Returns: 169 | uint64 as the natural log of the value passed. 170 | """ 171 | return (log2(x) * scale) / log2_e 172 | 173 | 174 | @Subroutine(TealType.uint64) 175 | def log10(x): 176 | """Returns log base 10 of the integer passed 177 | 178 | uses log10(x) = log2(x)/log2(10) identity 179 | 180 | TODO: implement with ufixed 181 | 182 | Args: 183 | x: uint64 on which to take the log base 10 184 | 185 | Returns: 186 | uint64 as the log10 of the value passed 187 | 188 | """ 189 | return (log2(x) * scale) / log2_10 190 | 191 | 192 | @Subroutine(TealType.uint64) 193 | def pow10(x) -> Expr: 194 | """ 195 | Returns 10^x, useful for things like total supply of an asset 196 | 197 | """ 198 | return Exp(Int(10), x) 199 | 200 | 201 | @Subroutine(TealType.uint64) 202 | def max(a, b) -> Expr: 203 | """max returns the max of 2 integers""" 204 | return If(a > b, a, b) 205 | 206 | 207 | @Subroutine(TealType.uint64) 208 | def min(a, b) -> Expr: 209 | """min returns the min of 2 integers""" 210 | return If(a < b, a, b) 211 | 212 | 213 | @Subroutine(TealType.uint64) 214 | def div_ceil(a, b) -> Expr: 215 | """Returns the result of division rounded up to the next integer 216 | 217 | Args: 218 | a: uint64 numerator for the operation 219 | b: uint64 denominator for the operation 220 | 221 | Returns: 222 | uint64 result of a truncated division + 1 223 | 224 | """ 225 | q = a / b 226 | return If(a % b > Int(0), q + Int(1), q) 227 | 228 | 229 | @Subroutine(TealType.uint64) 230 | def bytes_to_int(x): 231 | return If( 232 | Len(x) < Int(8), 233 | ExtractUint64(Concat(BytesZero(Int(8) - Len(x)), x), Int(0)), 234 | ExtractUint64(x, Len(x) - Int(8)), 235 | ) 236 | 237 | 238 | @Subroutine(TealType.bytes) 239 | def stack_to_wide(): 240 | """stack_to_wide returns the combination of the high and low integers returned from a wide math operation as bytes""" 241 | h = ScratchSlot() 242 | l = ScratchSlot() 243 | return Seq( 244 | l.store(), 245 | h.store(), # Take the low and high ints off the stack and combine them 246 | Concat(Itob(h.load()), Itob(l.load())), 247 | ) 248 | 249 | 250 | @Subroutine(TealType.uint64) 251 | def saturation(n, upper_limit, lower_limit) -> Expr: 252 | """Produces an output that is the value of n bounded to the upper and lower 253 | saturation values. The upper and lower limits are specified by the 254 | parameters upper_limit and lower_limit.""" 255 | return ( 256 | If(n >= upper_limit) 257 | .Then(Return(upper_limit)) 258 | .ElseIf(n <= lower_limit) 259 | .Then(Return(lower_limit)) 260 | .Else(Return(n)) 261 | ) 262 | -------------------------------------------------------------------------------- /pytealutils/math/signed_int.py: -------------------------------------------------------------------------------- 1 | from pyteal import BinaryExpr, Expr, Int, Op, TealType, UnaryExpr 2 | 3 | # Credit CiottiGiorgio 4 | 5 | 6 | class SignedInt(Int): 7 | def __init__(self, value: int): 8 | assert ( 9 | -(2**63) <= value <= 2**63 - 1 10 | ), "Value must be between -2^63 and 2^63-1" 11 | 12 | if value < 0: 13 | value = abs(value) 14 | value = ((value ^ 0xFFFFFFFFFFFFFFFF) + 1) % 2**64 15 | 16 | super().__init__(value) 17 | 18 | def __sub__(self, other) -> Expr: 19 | return SignedInt.__add_modulo__(self, SignedInt.two_complement(other)) 20 | 21 | def __add__(self, other) -> Expr: 22 | return SignedInt.__add_modulo__(self, other) 23 | 24 | @staticmethod 25 | def __add_modulo__(left, right) -> Expr: 26 | # We use addition wide because there are instances where the result is greater than 2^64. 27 | # Of course when adding any two 64bit uint(s) the result can at most be one bit longer. 28 | # The overflow is not on top of the stack so we have to swap and pop. 29 | addition_with_overflow = BinaryExpr( 30 | Op.addw, TealType.uint64, TealType.uint64, left, right 31 | ) 32 | addition_swapped = UnaryExpr( 33 | Op.swap, TealType.anytype, TealType.anytype, addition_with_overflow 34 | ) 35 | addition_without_overflow = UnaryExpr( 36 | Op.pop, TealType.uint64, TealType.uint64, addition_swapped 37 | ) 38 | 39 | return addition_without_overflow 40 | 41 | @staticmethod 42 | def add(l, r) -> Expr: 43 | return SignedInt.__add_modulo__(l, r) 44 | 45 | @staticmethod 46 | def subtract(l, r) -> Expr: 47 | return SignedInt.__add_modulo__(l, SignedInt.two_complement(r)) 48 | 49 | @staticmethod 50 | def two_complement(n) -> Expr: 51 | return SignedInt.__add_modulo__(~n, Int(1)) 52 | -------------------------------------------------------------------------------- /pytealutils/math/test_math.py: -------------------------------------------------------------------------------- 1 | from pyteal import Int, Itob, Log, Seq 2 | 3 | from tests.helpers import assert_output, logged_int 4 | 5 | from .math import div_ceil, even, max, odd, pow10, saturation 6 | 7 | 8 | def test_even(): 9 | num = 5 10 | expr = Seq(Log(Itob(even(Int(num)))), Log(Itob(even(Int(num - 1))))) 11 | output = [logged_int(0), logged_int(1)] 12 | assert_output(expr, output, pad_budget=15) 13 | 14 | 15 | def test_odd(): 16 | num = 6 17 | expr = Seq(Log(Itob(odd(Int(num)))), Log(Itob(odd(Int(num - 1))))) 18 | output = [logged_int(0), logged_int(1)] 19 | assert_output(expr, output, pad_budget=15) 20 | 21 | 22 | # def test_factorial(): 23 | # num = 5 24 | # expr = Log(Itob(bytes_to_int(wide_factorial(Itob(Int(num)))))) 25 | # output = [logged_int(int(pymath.factorial(num)))] 26 | # assert_output(expr, output, pad_budget=15) 27 | # 28 | # 29 | # def test_exponential(): 30 | # num = 10 31 | # expr = Log(Itob(bytes_to_int(exponential(Int(num), Int(30))))) 32 | # output = [logged_int(int(pymath.exp(num)))] 33 | # assert_output(expr, output, pad_budget=15) 34 | # 35 | # def test_ln(): 36 | # num = 10 37 | # expr = Log(Itob(ln(Int(num), Int(2)))) 38 | # output = [logged_int(int(pymath.log(num)))] 39 | # assert_output(expr, output, pad_budget=15) 40 | 41 | # def test_log2(): 42 | # num = 17 43 | # # expr = Log(Itob(log2(Int(num)))) 44 | # expr = Pop(log2(Int(num))) 45 | # output = [logged_int(int(pymath.log2(num)))] 46 | # print(pymath.log2(num)) 47 | # assert_output(expr, output, pad_budget=15) 48 | 49 | 50 | # def test_log10(): 51 | # num = 123123123 52 | # expr = Log(Itob(scaled_log10(Int(num)))) 53 | # output = [logged_int(int(pymath.log10(num)))] 54 | # print(pymath.log10(num)) 55 | # assert_output(expr, output) 56 | 57 | 58 | def test_pow10(): 59 | expr = Log(Itob(pow10(Int(3)))) 60 | output = [logged_int(int(1e3))] 61 | assert_output(expr, output) 62 | 63 | 64 | def test_min(): 65 | expr = Log(Itob(min(Int(100), Int(10)))) 66 | output = [logged_int(int(10))] 67 | assert_output(expr, output) 68 | 69 | 70 | def test_max(): 71 | expr = Log(Itob(max(Int(100), Int(10)))) 72 | output = [logged_int(int(100))] 73 | assert_output(expr, output) 74 | 75 | 76 | def test_div_ceil(): 77 | expr = Log(Itob(div_ceil(Int(100), Int(3)))) 78 | output = [logged_int(int(34))] 79 | assert_output(expr, output) 80 | 81 | 82 | # def test_negative_power(): 83 | # expr = Log(negative_power(Int(100), Int(3))) 84 | # output = [logged_int(int(math.pow(100, -3)))] 85 | # assert_output(expr, output) 86 | 87 | 88 | def test_saturation(): 89 | expr = Log(Itob(saturation(Int(50), Int(100), Int(20)))) 90 | output = [logged_int(int(50))] 91 | assert_output(expr, output) 92 | 93 | expr = Log(Itob(saturation(Int(15), Int(100), Int(20)))) 94 | output = [logged_int(int(20))] 95 | assert_output(expr, output) 96 | 97 | expr = Log(Itob(saturation(Int(150), Int(100), Int(20)))) 98 | output = [logged_int(int(100))] 99 | assert_output(expr, output) 100 | -------------------------------------------------------------------------------- /pytealutils/math/test_signed_int.py: -------------------------------------------------------------------------------- 1 | from pyteal import Int, Itob, Log, Seq 2 | 3 | from tests.helpers import assert_output, logged_int 4 | 5 | from .signed_int import SignedInt 6 | 7 | 8 | def test_signed_sub(): 9 | num = 100 10 | expr = Seq( 11 | Log(Itob(SignedInt(num) - SignedInt(num + 1))), 12 | Log(Itob(SignedInt(num) + SignedInt(num + 1))), 13 | ) 14 | 15 | output = ["ff" * 8, logged_int(num + num + 1)] 16 | assert_output(expr, output) 17 | 18 | 19 | def test_signed_sub_add(): 20 | num = 100 21 | expr = Seq( 22 | Log(Itob(SignedInt.add(SignedInt(num) - SignedInt(num + 5), Int(num + 5)))) 23 | ) 24 | output = [logged_int(num)] 25 | assert_output(expr, output) 26 | 27 | 28 | def test_signed_static(): 29 | expr = Log(Itob(SignedInt.subtract(Int(10), Int(11)))) 30 | output = ["ff" * 8] 31 | assert_output(expr, output) 32 | -------------------------------------------------------------------------------- /pytealutils/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from .local_blob import LocalBlob, max_bytes, max_keys, page_size 2 | from .storage import global_get_else, global_must_get, local_get_else, local_must_get 3 | -------------------------------------------------------------------------------- /pytealutils/storage/global_blob.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from pyteal import ( 4 | App, 5 | Bytes, 6 | BytesZero, 7 | Concat, 8 | Expr, 9 | Extract, 10 | For, 11 | GetByte, 12 | If, 13 | Int, 14 | Itob, 15 | Len, 16 | Or, 17 | ScratchVar, 18 | Seq, 19 | SetByte, 20 | Subroutine, 21 | Substring, 22 | TealType, 23 | ) 24 | 25 | from ..inline import InlineAssembly 26 | 27 | _max_keys = 64 28 | _page_size = 128 - 1 # need 1 byte for key 29 | _max_bytes = _max_keys * _page_size 30 | _max_bits = _max_bytes * 8 31 | 32 | max_keys = Int(_max_keys) 33 | page_size = Int(_page_size) 34 | max_bytes = Int(_max_bytes) 35 | 36 | 37 | def _key_and_offset(idx: Int) -> Tuple[Int, Int]: 38 | return idx / page_size, idx % page_size 39 | 40 | 41 | @Subroutine(TealType.bytes) 42 | def intkey(i: Expr) -> Expr: 43 | return Extract(Itob(i), Int(7), Int(1)) 44 | 45 | 46 | # TODO: Add Keyspace range? 47 | class GlobalBlob: 48 | """ 49 | Blob is a class holding static methods to work with the global storage of an application as a binary large object 50 | 51 | The `zero` method must be called on an application on opt in and the schema of the global storage should be 16 bytes 52 | """ 53 | 54 | @staticmethod 55 | @Subroutine(TealType.none) 56 | def zero() -> Expr: 57 | """ 58 | initializes global state of an application to all zero bytes 59 | 60 | This allows us to be lazy later and _assume_ all the strings are the same size 61 | """ 62 | 63 | # Expects bzero'd, max_keys 64 | zloop = """ 65 | zero_loop: 66 | int 1 67 | - // ["00"*page_size, key-1] 68 | dup2 // ["00"*page_size, key, "00"*page_size, key] 69 | itob // ["00"*page_size, key, "00"*page_size, itob(key)] 70 | extract 7 1 // ["00"*page_size, key, "00"*page_size, itob(key)[-1]] get the last byte of the int 71 | swap // ["00"*page_size, key, itob(key)[-1], "00"*page_size] 72 | app_global_put // ["00"*page_size, key] (removes top 2 elements) 73 | dup // ["00"*page_size, key-1, key-1] 74 | bnz zero_loop // start loop over if key-1>0 75 | pop 76 | pop // take extra junk off the stack 77 | retsub 78 | callsub zero_loop 79 | """ 80 | return InlineAssembly(zloop, BytesZero(page_size), max_keys, type=TealType.none) 81 | 82 | @staticmethod 83 | @Subroutine(TealType.uint64) 84 | def get_byte(idx): 85 | """ 86 | Get a single byte from global storage of an application by index 87 | """ 88 | key, offset = _key_and_offset(idx) 89 | return GetByte(App.globalGet(intkey(key)), offset) 90 | 91 | @staticmethod 92 | @Subroutine(TealType.none) 93 | def set_byte(idx, byte): 94 | """ 95 | Set a single byte from global storage of an application by index 96 | """ 97 | key, offset = _key_and_offset(idx) 98 | return App.globalPut( 99 | intkey(key), SetByte(App.globalGet(intkey(key)), offset, byte) 100 | ) 101 | 102 | @staticmethod 103 | @Subroutine(TealType.bytes) 104 | def read(bstart, bstop) -> Expr: 105 | """ 106 | read bytes between bstart and bend from global storage of an application by index 107 | """ 108 | 109 | start_key, start_offset = _key_and_offset(bstart) 110 | stop_key, stop_offset = _key_and_offset(bstop) 111 | 112 | key = ScratchVar() 113 | buff = ScratchVar() 114 | 115 | start = ScratchVar() 116 | stop = ScratchVar() 117 | 118 | init = key.store(start_key) 119 | cond = key.load() <= stop_key 120 | incr = key.store(key.load() + Int(1)) 121 | 122 | return Seq( 123 | buff.store(Bytes("")), 124 | For(init, cond, incr).Do( 125 | Seq( 126 | start.store(If(key.load() == start_key, start_offset, Int(0))), 127 | stop.store(If(key.load() == stop_key, stop_offset, page_size)), 128 | buff.store( 129 | Concat( 130 | buff.load(), 131 | Substring( 132 | App.globalGet(intkey(key.load())), 133 | start.load(), 134 | stop.load(), 135 | ), 136 | ) 137 | ), 138 | ) 139 | ), 140 | buff.load(), 141 | ) 142 | 143 | @staticmethod 144 | @Subroutine(TealType.uint64) 145 | def write(bstart, buff) -> Expr: 146 | """ 147 | write bytes between bstart and len(buff) to global storage of an application 148 | """ 149 | 150 | start_key, start_offset = _key_and_offset(bstart) 151 | stop_key, stop_offset = _key_and_offset(bstart + Len(buff)) 152 | 153 | key = ScratchVar() 154 | start = ScratchVar() 155 | stop = ScratchVar() 156 | written = ScratchVar() 157 | 158 | init = key.store(start_key) 159 | cond = key.load() <= stop_key 160 | incr = key.store(key.load() + Int(1)) 161 | 162 | delta = ScratchVar() 163 | 164 | return Seq( 165 | written.store(Int(0)), 166 | For(init, cond, incr).Do( 167 | Seq( 168 | start.store(If(key.load() == start_key, start_offset, Int(0))), 169 | stop.store(If(key.load() == stop_key, stop_offset, page_size)), 170 | App.globalPut( 171 | intkey(key.load()), 172 | If( 173 | Or(stop.load() != page_size, start.load() != Int(0)) 174 | ) # Its a partial write 175 | .Then( 176 | Seq( 177 | delta.store(stop.load() - start.load()), 178 | Concat( 179 | Substring( 180 | App.globalGet(intkey(key.load())), 181 | Int(0), 182 | start.load(), 183 | ), 184 | Extract(buff, written.load(), delta.load()), 185 | Substring( 186 | App.globalGet(intkey(key.load())), 187 | stop.load(), 188 | page_size, 189 | ), 190 | ), 191 | ) 192 | ) 193 | .Else( 194 | Seq( 195 | delta.store(page_size), 196 | Extract(buff, written.load(), page_size), 197 | ) 198 | ), 199 | ), 200 | written.store(written.load() + delta.load()), 201 | ) 202 | ), 203 | written.load(), 204 | ) 205 | -------------------------------------------------------------------------------- /pytealutils/storage/local_blob.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from pyteal import ( 4 | App, 5 | Bytes, 6 | BytesZero, 7 | Concat, 8 | Expr, 9 | Extract, 10 | For, 11 | GetByte, 12 | If, 13 | Int, 14 | Itob, 15 | Len, 16 | Or, 17 | ScratchVar, 18 | Seq, 19 | SetByte, 20 | Subroutine, 21 | Substring, 22 | TealType, 23 | ) 24 | 25 | _max_keys = 16 26 | _page_size = 128 - 1 # need 1 byte for key 27 | _max_bytes = _max_keys * _page_size 28 | _max_bits = _max_bytes * 8 29 | 30 | max_keys = Int(_max_keys) 31 | page_size = Int(_page_size) 32 | max_bytes = Int(_max_bytes) 33 | 34 | 35 | def _key_and_offset(idx: Int) -> Tuple[Int, Int]: 36 | return idx / page_size, idx % page_size 37 | 38 | 39 | @Subroutine(TealType.bytes) 40 | def intkey(i) -> Expr: 41 | return Extract(Itob(i), Int(7), Int(1)) 42 | 43 | 44 | # TODO: Add Keyspace range? 45 | class LocalBlob: 46 | """ 47 | Blob is a class holding static methods to work with the local storage of an account as a binary large object 48 | 49 | The `zero` method must be called on an account on opt in and the schema of the local storage should be 16 bytes 50 | """ 51 | 52 | @staticmethod 53 | @Subroutine(TealType.none) 54 | def zero(acct) -> Expr: 55 | """ 56 | initializes local state of an account to all zero bytes 57 | 58 | This allows us to be lazy later and _assume_ all the strings are the same size 59 | 60 | """ 61 | i = ScratchVar() 62 | init = i.store(Int(0)) 63 | cond = i.load() < max_keys 64 | iter = i.store(i.load() + Int(1)) 65 | return For(init, cond, iter).Do( 66 | App.localPut(acct, intkey(i.load()), BytesZero(page_size)) 67 | ) 68 | 69 | @staticmethod 70 | @Subroutine(TealType.uint64) 71 | def get_byte(acct, idx): 72 | """ 73 | Get a single byte from local storage of an account by index 74 | """ 75 | key, offset = _key_and_offset(idx) 76 | return GetByte(App.localGet(acct, intkey(key)), offset) 77 | 78 | @staticmethod 79 | @Subroutine(TealType.none) 80 | def set_byte(acct, idx, byte): 81 | """ 82 | Set a single byte from local storage of an account by index 83 | """ 84 | key, offset = _key_and_offset(idx) 85 | return App.localPut( 86 | acct, intkey(key), SetByte(App.localGet(acct, intkey(key)), offset, byte) 87 | ) 88 | 89 | @staticmethod 90 | @Subroutine(TealType.bytes) 91 | def read(acct, bstart, bend) -> Expr: 92 | """ 93 | read bytes between bstart and bend from local storage of an account by index 94 | """ 95 | 96 | start_key, start_offset = _key_and_offset(bstart) 97 | stop_key, stop_offset = _key_and_offset(bend) 98 | 99 | key = ScratchVar() 100 | buff = ScratchVar() 101 | 102 | start = ScratchVar() 103 | stop = ScratchVar() 104 | 105 | init = key.store(start_key) 106 | cond = key.load() <= stop_key 107 | incr = key.store(key.load() + Int(1)) 108 | 109 | return Seq( 110 | buff.store(Bytes("")), 111 | For(init, cond, incr).Do( 112 | Seq( 113 | start.store(If(key.load() == start_key, start_offset, Int(0))), 114 | stop.store(If(key.load() == stop_key, stop_offset, page_size)), 115 | buff.store( 116 | Concat( 117 | buff.load(), 118 | Substring( 119 | App.localGet(acct, intkey(key.load())), 120 | start.load(), 121 | stop.load(), 122 | ), 123 | ) 124 | ), 125 | ) 126 | ), 127 | buff.load(), 128 | ) 129 | 130 | @staticmethod 131 | @Subroutine(TealType.uint64) 132 | def write(acct, bstart, buff) -> Expr: 133 | """ 134 | write bytes between bstart and len(buff) to local storage of an account 135 | """ 136 | 137 | start_key, start_offset = _key_and_offset(bstart) 138 | stop_key, stop_offset = _key_and_offset(bstart + Len(buff)) 139 | 140 | key = ScratchVar() 141 | start = ScratchVar() 142 | stop = ScratchVar() 143 | written = ScratchVar() 144 | 145 | init = key.store(start_key) 146 | cond = key.load() <= stop_key 147 | incr = key.store(key.load() + Int(1)) 148 | 149 | delta = ScratchVar() 150 | 151 | return Seq( 152 | written.store(Int(0)), 153 | For(init, cond, incr).Do( 154 | Seq( 155 | start.store(If(key.load() == start_key, start_offset, Int(0))), 156 | stop.store(If(key.load() == stop_key, stop_offset, page_size)), 157 | App.localPut( 158 | acct, 159 | intkey(key.load()), 160 | If( 161 | Or(stop.load() != page_size, start.load() != Int(0)) 162 | ) # Its a partial write 163 | .Then( 164 | Seq( 165 | delta.store(stop.load() - start.load()), 166 | Concat( 167 | Substring( 168 | App.localGet(acct, intkey(key.load())), 169 | Int(0), 170 | start.load(), 171 | ), 172 | Extract(buff, written.load(), delta.load()), 173 | Substring( 174 | App.localGet(acct, intkey(key.load())), 175 | stop.load(), 176 | page_size, 177 | ), 178 | ), 179 | ) 180 | ) 181 | .Else( 182 | Seq( 183 | delta.store(page_size), 184 | Extract(buff, written.load(), page_size), 185 | ) 186 | ), 187 | ), 188 | written.store(written.load() + delta.load()), 189 | ) 190 | ), 191 | written.load(), 192 | ) 193 | -------------------------------------------------------------------------------- /pytealutils/storage/storage.py: -------------------------------------------------------------------------------- 1 | from pyteal import App, Assert, Expr, If, Int, Seq, Subroutine, TealType 2 | 3 | 4 | @Subroutine(TealType.anytype) 5 | def global_must_get(key) -> Expr: 6 | """Returns the result of a global storage MaybeValue if it exists, else Assert and fail the program""" 7 | maybe = App.globalGetEx(Int(0), key) 8 | return Seq(maybe, Assert(maybe.hasValue()), maybe.value()) 9 | 10 | 11 | @Subroutine(TealType.anytype) 12 | def global_get_else(key, default: Expr) -> Expr: 13 | """Returns the result of a global storage MaybeValue if it exists, else return a default value""" 14 | maybe = App.globalGetEx(Int(0), key) 15 | return Seq(maybe, If(maybe.hasValue(), maybe.value(), default)) 16 | 17 | 18 | @Subroutine(TealType.anytype) 19 | def local_must_get(acct, key) -> Expr: 20 | """Returns the result of a loccal storage MaybeValue if it exists, else Assert and fail the program""" 21 | mv = App.localGetEx(acct, Int(0), key) 22 | return Seq(mv, Assert(mv.hasValue()), mv.value()) 23 | 24 | 25 | @Subroutine(TealType.anytype) 26 | def local_get_else(acct, key, default: Expr) -> Expr: 27 | """Returns the result of a local storage MaybeValue if it exists, else return a default value""" 28 | mv = App.localGetEx(acct, Int(0), key) 29 | return Seq(mv, If(mv.hasValue()).Then(mv.value()).Else(default)) 30 | -------------------------------------------------------------------------------- /pytealutils/storage/test_global_blob.py: -------------------------------------------------------------------------------- 1 | from algosdk.future.transaction import StateSchema 2 | from pyteal import Bytes, BytesZero, Int, Itob, Log, Pop, Seq 3 | 4 | from tests.helpers import ( 5 | LOGIC_EVAL_ERROR, 6 | assert_fail, 7 | assert_output, 8 | logged_bytes, 9 | logged_int, 10 | ) 11 | 12 | from .global_blob import GlobalBlob, max_bytes 13 | 14 | # Can re-use the same blob 15 | b = GlobalBlob() 16 | 17 | 18 | def test_global_blob_zero(): 19 | expr = Seq(b.zero(), Log(b.read(Int(0), Int(64)))) 20 | expected = [logged_int(0) * 8] 21 | assert_output(expr, expected, global_schema=StateSchema(0, 64)) 22 | 23 | 24 | def test_global_blob_zero_no_schema(): 25 | expr = Seq(b.zero(), Log(b.read(Int(0), Int(64)))) 26 | # Not providing the required schema 27 | expected = [LOGIC_EVAL_ERROR] 28 | assert_fail(expr, expected) 29 | 30 | 31 | def test_global_blob_write_read(): 32 | expr = Seq( 33 | b.zero(), 34 | Pop(b.write(Int(0), Bytes("deadbeef" * 2))), 35 | Log(b.read(Int(0), Int(8))), 36 | ) 37 | expected = [logged_bytes("deadbeef")] 38 | assert_output(expr, expected, global_schema=StateSchema(0, 64), pad_budget=3) 39 | 40 | 41 | def test_global_blob_write_read_boundary(): 42 | expr = Seq( 43 | b.zero(), 44 | Pop(b.write(Int(0), BytesZero(Int(381)))), 45 | Log(b.read(Int(32), Int(40))), 46 | ) 47 | expected = ["00" * 8] 48 | assert_output(expr, expected, global_schema=StateSchema(0, 64), pad_budget=3) 49 | 50 | 51 | def test_global_blob_write_read_no_zero(): 52 | expr = Seq(Pop(b.write(Int(0), Bytes("deadbeef" * 2))), Log(b.read(Int(0), Int(8)))) 53 | expected = [LOGIC_EVAL_ERROR] 54 | assert_fail(expr, expected, global_schema=StateSchema(0, 64), pad_budget=3) 55 | 56 | 57 | def test_global_blob_write_read_past_end(): 58 | expr = Seq( 59 | b.zero(), 60 | Pop(b.write(Int(0), Bytes("deadbeef" * 2))), 61 | Log(b.read(Int(0), max_bytes)), 62 | ) 63 | expected = [LOGIC_EVAL_ERROR] 64 | assert_fail(expr, expected, global_schema=StateSchema(0, 64), pad_budget=3) 65 | 66 | 67 | def test_global_blob_set_get(): 68 | expr = Seq(b.zero(), b.set_byte(Int(32), Int(123)), Log(Itob(b.get_byte(Int(32))))) 69 | expected = [logged_int(123)] 70 | assert_output(expr, expected, global_schema=StateSchema(0, 64)) 71 | 72 | 73 | def test_global_blob_set_past_end(): 74 | expr = Seq(b.zero(), b.set_byte(max_bytes, Int(123))) 75 | expected = [LOGIC_EVAL_ERROR] 76 | assert_fail(expr, expected, global_schema=StateSchema(0, 64)) 77 | -------------------------------------------------------------------------------- /pytealutils/storage/test_local_blob.py: -------------------------------------------------------------------------------- 1 | from pyteal import Bytes, BytesZero, Int, Itob, Log, Pop, Seq 2 | 3 | from tests.helpers import ( 4 | LOGIC_EVAL_ERROR, 5 | assert_fail, 6 | assert_stateful_fail, 7 | assert_stateful_output, 8 | logged_bytes, 9 | logged_int, 10 | ) 11 | 12 | from .local_blob import LocalBlob, max_bytes 13 | 14 | # Can re-use the same blob 15 | b = LocalBlob() 16 | 17 | 18 | def test_local_blob_zero(): 19 | expr = Seq(b.zero(Int(0)), Log(b.read(Int(0), Int(0), Int(64)))) 20 | expected = [logged_int(0) * 8] 21 | assert_stateful_output(expr, expected) 22 | 23 | 24 | def test_local_blob_no_schema(): 25 | expr = Seq(b.zero(Int(0)), Log(b.read(Int(0), Int(0), Int(64)))) 26 | expected = [LOGIC_EVAL_ERROR] 27 | assert_fail(expr, expected) 28 | 29 | 30 | def test_local_blob_write_read(): 31 | expr = Seq( 32 | b.zero(Int(0)), 33 | Pop(b.write(Int(0), Int(0), Bytes("deadbeef" * 8))), 34 | Log(b.read(Int(0), Int(32), Int(40))), 35 | ) 36 | expected = [logged_bytes("deadbeef")] 37 | assert_stateful_output(expr, expected) 38 | 39 | 40 | def test_local_blob_write_read_boundary(): 41 | expr = Seq( 42 | b.zero(Int(0)), 43 | Pop(b.write(Int(0), Int(0), BytesZero(Int(381)))), 44 | Log(b.read(Int(0), Int(32), Int(40))), 45 | ) 46 | expected = ["00" * 8] 47 | assert_stateful_output(expr, expected) 48 | 49 | 50 | def test_local_blob_write_read_no_zero(): 51 | expr = Seq( 52 | Pop(b.write(Int(0), Int(0), Bytes("deadbeef" * 8))), 53 | Log(b.read(Int(0), Int(32), Int(40))), 54 | ) 55 | expected = [LOGIC_EVAL_ERROR] 56 | assert_stateful_fail(expr, expected) 57 | 58 | 59 | def test_local_blob_write_read_past_end(): 60 | expr = Seq( 61 | b.zero(Int(0)), 62 | Pop(b.write(Int(0), Int(0), Bytes("deadbeef" * 8))), 63 | Log(b.read(Int(0), Int(0), max_bytes)), 64 | ) 65 | expected = [LOGIC_EVAL_ERROR] 66 | assert_stateful_fail(expr, expected) 67 | 68 | 69 | def test_local_blob_set_get(): 70 | expr = Seq( 71 | b.zero(Int(0)), 72 | b.set_byte(Int(0), Int(32), Int(123)), 73 | Log(Itob(b.get_byte(Int(0), Int(32)))), 74 | ) 75 | expected = [logged_int(123)] 76 | assert_stateful_output(expr, expected) 77 | 78 | 79 | def test_local_blob_set_past_end(): 80 | expr = Seq( 81 | b.zero(Int(0)), 82 | b.set_byte(Int(0), max_bytes, Int(123)), 83 | Log(Itob(b.get_byte(Int(0), Int(32)))), 84 | ) 85 | expected = [LOGIC_EVAL_ERROR] 86 | assert_stateful_fail(expr, expected) 87 | -------------------------------------------------------------------------------- /pytealutils/storage/test_storage.py: -------------------------------------------------------------------------------- 1 | from pyteal import App, Bytes, Int, Log, Seq 2 | 3 | from tests.helpers import assert_stateful_fail, assert_stateful_output, logged_bytes 4 | 5 | from .storage import global_get_else, global_must_get, local_get_else, local_must_get 6 | 7 | 8 | def test_global_get_else(): 9 | expr = Log(global_get_else(Bytes("doesn't exist"), Bytes("default"))) 10 | expected = [logged_bytes("default")] 11 | assert_stateful_output(expr, expected) 12 | 13 | 14 | def test_global_must_get(): 15 | expr = Seq( 16 | App.globalPut(Bytes("exists"), Bytes("success")), 17 | Log(global_must_get(Bytes("exists"))), 18 | ) 19 | expected = [logged_bytes("success")] 20 | assert_stateful_output(expr, expected) 21 | 22 | 23 | def test_global_must_get_missing(): 24 | expr = Log(global_must_get(Bytes("doesnt exist"))) 25 | assert_stateful_fail(expr, ["logic eval error"]) 26 | 27 | 28 | def test_local_must_get(): 29 | expr = Seq( 30 | App.localPut(Int(0), Bytes("exists"), Bytes("success")), 31 | Log(local_must_get(Int(0), Bytes("exists"))), 32 | ) 33 | expected = [logged_bytes("success")] 34 | assert_stateful_output(expr, expected) 35 | 36 | 37 | def test_local_must_get_missing(): 38 | expr = Log(local_must_get(Int(0), Bytes("doesnt exist"))) 39 | assert_stateful_fail(expr, ["logic eval error"]) 40 | 41 | 42 | def test_local_get_else(): 43 | expr = Log(local_get_else(Int(0), Bytes("doesn't exist"), Bytes("default"))) 44 | expected = [logged_bytes("default")] 45 | assert_stateful_output(expr, expected) 46 | -------------------------------------------------------------------------------- /pytealutils/strings/__init__.py: -------------------------------------------------------------------------------- 1 | from .string import atoi, encode_uvarint, head, itoa, prefix, rest, suffix, tail, witoa 2 | -------------------------------------------------------------------------------- /pytealutils/strings/string.py: -------------------------------------------------------------------------------- 1 | from pyteal import ( 2 | Assert, 3 | BitLen, 4 | Btoi, 5 | Bytes, 6 | BytesDiv, 7 | BytesGt, 8 | BytesMod, 9 | Concat, 10 | Extract, 11 | GetByte, 12 | If, 13 | Int, 14 | Itob, 15 | Len, 16 | ScratchVar, 17 | Seq, 18 | Subroutine, 19 | Substring, 20 | TealType, 21 | ) 22 | 23 | from pytealutils.math import pow10 24 | 25 | # Magic number to convert between ascii chars and integers 26 | _ascii_zero = 48 27 | _ascii_nine = _ascii_zero + 9 28 | ascii_zero = Int(_ascii_zero) 29 | ascii_nine = Int(_ascii_nine) 30 | 31 | 32 | @Subroutine(TealType.uint64) 33 | def ascii_to_int(arg): 34 | """ascii_to_int converts the integer representing a character in ascii to the actual integer it represents 35 | 36 | Args: 37 | arg: uint64 in the range 48-57 that is to be converted to an integer 38 | 39 | Returns: 40 | uint64 that is the value the ascii character passed in represents 41 | 42 | """ 43 | return Seq(Assert(arg >= ascii_zero), Assert(arg <= ascii_nine), arg - ascii_zero) 44 | 45 | 46 | @Subroutine(TealType.bytes) 47 | def int_to_ascii(arg): 48 | """int_to_ascii converts an integer to the ascii byte that represents it""" 49 | return Extract(Bytes("0123456789"), arg, Int(1)) 50 | 51 | 52 | @Subroutine(TealType.uint64) 53 | def atoi(a): 54 | """atoi converts a byte string representing a number to the integer value it represents""" 55 | return If( 56 | Len(a) > Int(0), 57 | (ascii_to_int(GetByte(a, Int(0))) * pow10(Len(a) - Int(1))) 58 | + atoi(Substring(a, Int(1), Len(a))), 59 | Int(0), 60 | ) 61 | 62 | 63 | @Subroutine(TealType.bytes) 64 | def itoa(i): 65 | """itoa converts an integer to the ascii byte string it represents""" 66 | return If( 67 | i == Int(0), 68 | Bytes("0"), 69 | Concat( 70 | If(i / Int(10) > Int(0), itoa(i / Int(10)), Bytes("")), 71 | int_to_ascii(i % Int(10)), 72 | ), 73 | ) 74 | 75 | 76 | @Subroutine(TealType.bytes) 77 | def witoa(i): 78 | """witoa converts an byte string interpreted as an integer to the ascii byte string it represents""" 79 | return If( 80 | BitLen(i) == Int(0), 81 | Bytes("0"), 82 | Concat( 83 | If( 84 | BytesGt(BytesDiv(i, Bytes("base16", "A0")), Bytes("base16", "A0")), 85 | witoa(BytesDiv(i, Bytes("base16", "A0"))), 86 | Bytes(""), 87 | ), 88 | int_to_ascii(Btoi(BytesMod(i, Bytes("base16", "A0")))), 89 | ), 90 | ) 91 | 92 | 93 | @Subroutine(TealType.bytes) 94 | def head(s): 95 | """head gets the first byte from a bytestring, returns as bytes""" 96 | return Extract(s, Int(0), Int(1)) 97 | 98 | 99 | @Subroutine(TealType.bytes) 100 | def tail(s): 101 | """tail returns the string with the first character removed""" 102 | return Substring(s, Int(1), Len(s)) 103 | 104 | 105 | @Subroutine(TealType.bytes) 106 | def suffix(s, n): 107 | """suffix returns the last n bytes of a given byte string""" 108 | return Substring(s, Len(s) - n, Len(s)) 109 | 110 | 111 | @Subroutine(TealType.bytes) 112 | def prefix(s, n): 113 | """prefix returns the first n bytes of a given byte string""" 114 | return Substring(s, Int(0), n) 115 | 116 | 117 | @Subroutine(TealType.bytes) 118 | def rest(s, n): 119 | """prefix returns the first n bytes of a given byte string""" 120 | return Substring(s, n, Len(s)) 121 | 122 | 123 | @Subroutine(TealType.bytes) 124 | def encode_uvarint(val, b): 125 | """ 126 | Returns the uvarint encoding of an integer 127 | 128 | Useful in the case that the bytecode for a contract is being populated, since 129 | integers in a contract are uvarint encoded 130 | 131 | This subroutine is recursive, the first call should include 132 | the integer to be encoded and an empty bytestring 133 | 134 | """ 135 | buff = ScratchVar() 136 | return Seq( 137 | buff.store(b), 138 | Concat( 139 | buff.load(), 140 | If( 141 | val >= Int(128), 142 | encode_uvarint( 143 | val >> Int(7), 144 | Extract(Itob((val & Int(255)) | Int(128)), Int(7), Int(1)), 145 | ), 146 | Extract(Itob(val & Int(255)), Int(7), Int(1)), 147 | ), 148 | ), 149 | ) 150 | -------------------------------------------------------------------------------- /pytealutils/strings/test_string.py: -------------------------------------------------------------------------------- 1 | from pyteal import Bytes, Int, Itob, Log 2 | 3 | from tests.helpers import ( 4 | LOGIC_EVAL_ERROR, 5 | assert_fail, 6 | assert_output, 7 | logged_bytes, 8 | logged_int, 9 | ) 10 | 11 | from .string import atoi, encode_uvarint, head, itoa, prefix, suffix, tail 12 | 13 | 14 | def test_atoi(): 15 | expr = Log(Itob(atoi(Bytes("123")))) 16 | output = [logged_int(int(123))] 17 | assert_output(expr, output) 18 | 19 | 20 | def test_atoi_invalid(): 21 | expr = Log(Itob(atoi(Bytes("abc")))) 22 | assert_fail(expr, [LOGIC_EVAL_ERROR]) 23 | 24 | 25 | def test_itoa(): 26 | expr = Log(itoa(Int(123))) 27 | output = [logged_bytes("123")] 28 | assert_output(expr, output) 29 | 30 | 31 | def test_head(): 32 | expr = Log(head(Bytes("deadbeef"))) 33 | output = [logged_bytes("d")] 34 | assert_output(expr, output) 35 | 36 | 37 | def test_head_empty(): 38 | expr = Log(tail(Bytes(""))) 39 | assert_fail(expr, [LOGIC_EVAL_ERROR]) 40 | 41 | 42 | def test_tail(): 43 | expr = Log(tail(Bytes("deadbeef"))) 44 | output = [logged_bytes("eadbeef")] 45 | assert_output(expr, output) 46 | 47 | 48 | def test_tail_empty(): 49 | expr = Log(tail(Bytes(""))) 50 | assert_fail(expr, [LOGIC_EVAL_ERROR]) 51 | 52 | 53 | def test_suffix(): 54 | expr = Log(suffix(Bytes("deadbeef"), Int(2))) 55 | output = [logged_bytes("ef")] 56 | assert_output(expr, output) 57 | 58 | 59 | def test_suffix_past_length(): 60 | expr = Log(suffix(Bytes("deadbeef"), Int(9))) 61 | assert_fail(expr, [LOGIC_EVAL_ERROR]) 62 | 63 | 64 | def test_prefix(): 65 | expr = Log(prefix(Bytes("deadbeef"), Int(2))) 66 | output = [logged_bytes("de")] 67 | assert_output(expr, output) 68 | 69 | 70 | def test_prefix_past_length(): 71 | expr = Log(prefix(Bytes("deadbeef"), Int(9))) 72 | assert_fail(expr, [LOGIC_EVAL_ERROR]) 73 | 74 | 75 | def test_encode_uvarint(): 76 | expr = Log(encode_uvarint(Int(500), Bytes(""))) 77 | output = ["f403"] 78 | assert_output(expr, output) 79 | -------------------------------------------------------------------------------- /pytealutils/transaction/__init__.py: -------------------------------------------------------------------------------- 1 | from .inner_transactions import axfer, pay 2 | from .transaction import assert_no_asset_close_to, assert_no_close_to, assert_no_rekey 3 | -------------------------------------------------------------------------------- /pytealutils/transaction/inner_transactions.py: -------------------------------------------------------------------------------- 1 | from pyteal import Expr, InnerTxnBuilder, Seq, Subroutine, TealType, TxnField, TxnType 2 | 3 | 4 | @Subroutine(TealType.none) 5 | def axfer(receiver, asset_id, amt) -> Expr: 6 | return Seq( 7 | InnerTxnBuilder.Begin(), 8 | InnerTxnBuilder.SetFields( 9 | { 10 | TxnField.type_enum: TxnType.AssetTransfer, 11 | TxnField.asset_receiver: receiver, 12 | TxnField.xfer_asset: asset_id, 13 | TxnField.asset_amount: amt, 14 | } 15 | ), 16 | InnerTxnBuilder.Submit(), 17 | ) 18 | 19 | 20 | @Subroutine(TealType.none) 21 | def pay(receiver, amt) -> Expr: 22 | return Seq( 23 | InnerTxnBuilder.Begin(), 24 | InnerTxnBuilder.SetFields( 25 | { 26 | TxnField.type_enum: TxnType.Payment, 27 | TxnField.receiver: receiver, 28 | TxnField.amount: amt, 29 | } 30 | ), 31 | InnerTxnBuilder.Submit(), 32 | ) 33 | -------------------------------------------------------------------------------- /pytealutils/transaction/transaction.py: -------------------------------------------------------------------------------- 1 | from pyteal import Assert, Expr, Global, Gtxn, Seq, Subroutine, TealType 2 | 3 | 4 | @Subroutine(TealType.none) 5 | def assert_common_checks(idx): 6 | """Calls all txn checker assert methods 7 | 8 | Note: This doesn't mean the transaction is "safe" but these are common things to check for any transaction 9 | see https://developer.algorand.org/docs/get-details/dapps/avm/teal/guidelines/ for more details 10 | """ 11 | return Seq( 12 | assert_min_fee(idx), 13 | assert_no_rekey(idx), 14 | assert_no_close_to(idx), 15 | assert_no_asset_close_to(idx), 16 | ) 17 | 18 | 19 | @Subroutine(TealType.none) 20 | def assert_min_fee(idx): 21 | """Checks that the fee for a transaction is exactly equal to the current min fee""" 22 | return Assert(Gtxn[idx] == Global.min_txn_fee) 23 | 24 | 25 | @Subroutine(TealType.none) 26 | def assert_no_rekey(idx) -> Expr: 27 | """Checks that the rekey_to field is empty, Assert if it is set""" 28 | return Assert(Gtxn[idx].rekey_to() == Global.zero_address) 29 | 30 | 31 | @Subroutine(TealType.none) 32 | def assert_no_close_to(idx) -> Expr: 33 | """Checks that the close_remainder_to field is empty, Assert if it is set""" 34 | return Assert(Gtxn[idx].close_remainder_to() == Global.zero_address) 35 | 36 | 37 | @Subroutine(TealType.none) 38 | def assert_no_asset_close_to(idx) -> Expr: 39 | """Checks that the asset_close_to field is empty, Assert if it is set""" 40 | return Assert(Gtxn[idx].asset_close_to() == Global.zero_address) 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==21.2.0 2 | black==21.12b0 3 | cffi==1.15.0 4 | click==8.0.3 5 | iniconfig==1.1.1 6 | msgpack==1.0.3 7 | mypy-extensions==0.4.3 8 | packaging==21.3 9 | pathspec==0.9.0 10 | platformdirs==2.4.1 11 | pluggy==1.0.0 12 | py==1.11.0 13 | py-algorand-sdk==1.9.0 14 | pycparser==2.21 15 | pycryptodomex==3.12.0 16 | PyNaCl==1.4.0 17 | pyparsing==3.0.6 18 | -e git+git@github.com:algorand/pyteal.git@master#egg=pyteal 19 | pytest==6.2.5 20 | six==1.16.0 21 | toml==0.10.2 22 | tomli==1.2.3 23 | typing-extensions==4.0.1 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="pyteal-utils", 10 | version="0.0.1", 11 | author="Algorand", 12 | author_email="pypiservice@algorand.com", 13 | description="PyTEAL utility library", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/algorand/pyteal-utils", 17 | packages=setuptools.find_packages(), 18 | install_requires=["py-algorand-sdk", "pyteal"], 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | python_requires=">=3.6", 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algorand/pyteal-utils/c4976887ddd959d285894cf7d3e0feddef3821ba/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """Module containing helper functions for testing PyTeal Utils.""" 2 | 3 | from base64 import b64decode 4 | from typing import List, Optional 5 | 6 | import algosdk.abi as sdkabi 7 | from algosdk import account, encoding, kmd, logic, mnemonic 8 | from algosdk.future import transaction 9 | from algosdk.v2client import algod, indexer 10 | from pyteal import Cond, Expr, Int, Mode, Seq, Txn, compileTeal 11 | 12 | TEAL_VERSION = 5 13 | CLEAR_PROG = bytes([TEAL_VERSION, 129, 1]) # pragma 5; int 1 14 | 15 | LOGIC_EVAL_ERROR = "logic eval error" 16 | INVALID_SYNTAX = "invalid syntax" 17 | 18 | ## Clients 19 | 20 | 21 | def _algod_client(algod_address="http://localhost:4001", algod_token="a" * 64): 22 | """Instantiate and return Algod client object.""" 23 | return algod.AlgodClient(algod_token, algod_address) 24 | 25 | 26 | def _indexer_client(indexer_address="http://localhost:8980", indexer_token="a" * 64): 27 | """Instantiate and return Indexer client object.""" 28 | return indexer.IndexerClient(indexer_token, indexer_address) 29 | 30 | 31 | def _kmd_client(kmd_address="http://localhost:4002", kmd_token="a" * 64): 32 | """Instantiate and return a KMD client object.""" 33 | return kmd.KMDClient(kmd_token, kmd_address) 34 | 35 | 36 | # Env helpers 37 | 38 | 39 | class Account: 40 | def __init__( 41 | self, 42 | address: str, 43 | private_key: Optional[str], 44 | lsig: Optional[transaction.LogicSig] = None, 45 | app_id: Optional[int] = None, 46 | ): 47 | self.address = address 48 | self.private_key = private_key 49 | self.lsig = lsig 50 | self.app_id = app_id 51 | 52 | assert self.private_key or self.lsig or self.app_id 53 | 54 | def mnemonic(self) -> str: 55 | return mnemonic.from_private_key(self.private_key) 56 | 57 | def is_lsig(self) -> bool: 58 | return bool(not self.private_key and not self.app_id and self.lsig) 59 | 60 | def application_address(self) -> str: 61 | return logic.get_application_address(self.app_id) 62 | 63 | @classmethod 64 | def create(cls) -> "Account": 65 | private_key, address = account.generate_account() 66 | return cls(private_key=private_key, address=str(address)) 67 | 68 | @property 69 | def decoded_address(self): 70 | return encoding.decode_address(self.address) 71 | 72 | 73 | def get_kmd_accounts( 74 | kmd_wallet_name="unencrypted-default-wallet", kmd_wallet_password="" 75 | ): 76 | kmd_client = _kmd_client() 77 | wallets = kmd_client.list_wallets() 78 | 79 | walletID = None 80 | for wallet in wallets: 81 | if wallet["name"] == kmd_wallet_name: 82 | walletID = wallet["id"] 83 | break 84 | 85 | if walletID is None: 86 | raise Exception("Wallet not found: {}".format(kmd_wallet_name)) 87 | 88 | walletHandle = kmd_client.init_wallet_handle(walletID, kmd_wallet_password) 89 | 90 | try: 91 | addresses = kmd_client.list_keys(walletHandle) 92 | 93 | privateKeys = [ 94 | kmd_client.export_key(walletHandle, kmd_wallet_password, addr) 95 | for addr in addresses 96 | ] 97 | 98 | kmdAccounts = [ 99 | Account(address=addresses[i], private_key=privateKeys[i]) 100 | for i in range(len(privateKeys)) 101 | ] 102 | finally: 103 | kmd_client.release_wallet_handle(walletHandle) 104 | 105 | return kmdAccounts 106 | 107 | 108 | def sign(signer: Account, txn: transaction.Transaction): 109 | """Sign a transaction with an Account.""" 110 | if signer.is_lsig(): 111 | return transaction.LogicSigTransaction(txn, signer.lsig) 112 | else: 113 | assert signer.private_key 114 | return txn.sign(signer.private_key) 115 | 116 | 117 | def sign_send_wait( 118 | algod_client: algod.AlgodClient, 119 | signer: Account, 120 | txn: transaction.Transaction, 121 | debug=False, 122 | ): 123 | """Sign a transaction, submit it, and wait for its confirmation.""" 124 | signed_txn = sign(signer, txn) 125 | tx_id = signed_txn.transaction.get_txid() 126 | 127 | if debug: 128 | transaction.write_to_file([signed_txn], "/tmp/txn.signed", overwrite=True) 129 | 130 | algod_client.send_transactions([signed_txn]) 131 | transaction.wait_for_confirmation(algod_client, tx_id) 132 | return algod_client.pending_transaction_info(tx_id) 133 | 134 | 135 | ## Teal Helpers 136 | 137 | # Create global client to be used in tests 138 | client = _algod_client() 139 | 140 | 141 | def logged_bytes(b: str): 142 | return bytes(b, "ascii").hex() 143 | 144 | 145 | def logged_int(i: int, bits: int = 64): 146 | return i.to_bytes(bits // 8, "big").hex() 147 | 148 | 149 | def assert_stateful_output(expr: Expr, output: List[str]): 150 | assert expr is not None 151 | 152 | src = compile_stateful_app(expr) 153 | assert len(src) > 0 154 | 155 | compiled = assemble_bytecode(client, src) 156 | assert len(compiled["hash"]) == 58 157 | 158 | app_id = create_app( 159 | client, 160 | compiled["result"], 161 | transaction.StateSchema(0, 16), 162 | transaction.StateSchema(0, 64), 163 | ) 164 | 165 | logs, cost, callstack = call_app(client, app_id) 166 | print("\nCost: {}, CallStack: {}".format(cost, callstack)) 167 | print(logs) 168 | 169 | destroy_app(client, app_id) 170 | 171 | assert logs == output 172 | 173 | 174 | def assert_stateful_fail(expr: Expr, output: List[str]): 175 | assert expr is not None 176 | 177 | emsg = None 178 | 179 | try: 180 | src = compile_stateful_app(expr) 181 | assert len(src) > 0 182 | 183 | compiled = assemble_bytecode(client, src) 184 | assert len(compiled["hash"]) == 58 185 | 186 | app_id = create_app( 187 | client, 188 | compiled["result"], 189 | transaction.StateSchema(0, 16), 190 | transaction.StateSchema(0, 64), 191 | ) 192 | 193 | call_app(client, app_id) 194 | except Exception as e: 195 | emsg = str(e) 196 | 197 | assert emsg is not None 198 | assert output.pop() in emsg 199 | 200 | destroy_app(client, app_id) 201 | 202 | 203 | def assert_output(expr: Expr, output: List[str], **kwargs): 204 | assert expr is not None 205 | 206 | src = compile_method(expr) 207 | assert len(src) > 0 208 | 209 | compiled = assemble_bytecode(client, src) 210 | assert len(compiled["hash"]) == 58 211 | 212 | logs, cost, callstack = execute_app(client, compiled["result"], **kwargs) 213 | print("\nCost: {}, CallStack: {}".format(cost, callstack)) 214 | print(logs) 215 | assert logs == output 216 | 217 | 218 | def assert_application_output(expr: Expr, output: List[str], **kwargs): 219 | assert expr is not None 220 | 221 | src = compile_app(expr) 222 | assert len(src) > 0 223 | 224 | compiled = assemble_bytecode(client, src) 225 | assert len(compiled["hash"]) == 58 226 | 227 | logs, cost, callstack = execute_app(client, compiled["result"], **kwargs) 228 | print("\nCost: {}, CallStack: {}".format(cost, callstack)) 229 | print(logs) 230 | assert logs == output 231 | 232 | 233 | def assert_close_enough( 234 | expr: Expr, output: List[float], precisions: List[sdkabi.UfixedType], **kwargs 235 | ): 236 | """assert_close_enough takes some list of floats and corresponding precision and 237 | asserts that the result from the logic output is close enough to the expected value 238 | """ 239 | assert expr is not None 240 | 241 | src = compile_method(expr) 242 | assert len(src) > 0 243 | 244 | compiled = assemble_bytecode(client, src) 245 | assert len(compiled["hash"]) == 58 246 | 247 | logs, _, _ = execute_app(client, compiled["result"], **kwargs) 248 | for idx in range(len(output)): 249 | scale = 10 ** precisions[idx].precision 250 | 251 | incoming = precisions[idx].decode(bytes.fromhex(logs[idx])) 252 | expected = output[idx] * scale 253 | max_delta = 2.0 # since we scale the others _up_, we can leave this scaled as 2 254 | 255 | assert ( 256 | abs(expected - incoming) <= max_delta 257 | ), "Difference greater than max_delta: {} vs {}".format( 258 | abs(expected - incoming), max_delta 259 | ) 260 | 261 | 262 | def assert_fail(expr: Expr, output: List[str], **kwargs): 263 | assert expr is not None 264 | 265 | emsg = None 266 | 267 | try: 268 | src = compile_method(expr) 269 | assert len(src) > 0 270 | 271 | compiled = assemble_bytecode(client, src) 272 | assert len(compiled["hash"]) == 58 273 | 274 | execute_app(client, compiled["result"]) 275 | except Exception as e: 276 | emsg = str(e) 277 | 278 | assert emsg is not None 279 | assert output.pop() in emsg 280 | 281 | 282 | def compile_method(method: Expr, version: int = TEAL_VERSION): 283 | return compileTeal(Seq(method, Int(1)), mode=Mode.Application, version=version) 284 | 285 | 286 | def compile_app(application: Expr, version: int = TEAL_VERSION): 287 | return compileTeal(application, mode=Mode.Application, version=version) 288 | 289 | 290 | def compile_stateful_app(method: Expr, version: int = TEAL_VERSION): 291 | expr = Cond( 292 | [Txn.application_id() == Int(0), Int(1)], 293 | [Txn.application_args.length() > Int(0), Int(1)], 294 | [Int(1), Seq(method, Int(1))], 295 | ) 296 | return compileTeal(expr, mode=Mode.Application, version=version) 297 | 298 | 299 | def compile_sig(method: Expr, version: int = TEAL_VERSION): 300 | return compileTeal( 301 | Seq(method, Int(1)), 302 | mode=Mode.Signature, 303 | version=version, 304 | assembleConstants=True, 305 | ) 306 | 307 | 308 | def assemble_bytecode(client: algod.AlgodClient, src: str): 309 | return client.compile(src) 310 | 311 | 312 | def execute_app(client: algod.AlgodClient, bytecode: str, **kwargs): 313 | sp = client.suggested_params() 314 | 315 | acct = get_kmd_accounts().pop() 316 | 317 | if "local_schema" not in kwargs: 318 | kwargs["local_schema"] = transaction.StateSchema(0, 0) 319 | 320 | if "global_schema" not in kwargs: 321 | kwargs["global_schema"] = transaction.StateSchema(0, 0) 322 | 323 | txns = [ 324 | transaction.ApplicationCallTxn( 325 | acct.address, 326 | sp, 327 | 0, 328 | transaction.OnComplete.DeleteApplicationOC, 329 | kwargs["local_schema"], 330 | kwargs["global_schema"], 331 | b64decode(bytecode), 332 | CLEAR_PROG, 333 | ) 334 | ] 335 | 336 | if "pad_budget" in kwargs: 337 | for i in range(kwargs["pad_budget"]): 338 | txns.append( 339 | transaction.ApplicationCallTxn( 340 | acct.address, 341 | sp, 342 | 0, 343 | transaction.OnComplete.DeleteApplicationOC, 344 | kwargs["local_schema"], 345 | kwargs["global_schema"], 346 | CLEAR_PROG, 347 | CLEAR_PROG, 348 | note=str(i).encode(), 349 | ) 350 | ) 351 | 352 | txns = [txn.sign(acct.private_key) for txn in transaction.assign_group_id(txns)] 353 | drr = transaction.create_dryrun(client, txns) 354 | 355 | result = client.dryrun(drr) 356 | 357 | return get_stats_from_dryrun(result) 358 | 359 | 360 | def get_stats_from_dryrun(dryrun_result): 361 | logs, cost, trace_len = [], [], [] 362 | txn = dryrun_result["txns"][0] 363 | raise_rejected(txn) 364 | if "logs" in txn: 365 | logs.extend([b64decode(l).hex() for l in txn["logs"]]) 366 | if "cost" in txn: 367 | cost.append(txn["cost"]) 368 | if "app-call-trace" in txn: 369 | trace_len.append(len(txn["app-call-trace"])) 370 | return logs, cost, trace_len 371 | 372 | 373 | def raise_rejected(txn): 374 | if "app-call-messages" in txn: 375 | if "REJECT" in txn["app-call-messages"]: 376 | raise Exception(txn["app-call-messages"][-1]) 377 | 378 | 379 | def create_app( 380 | client: algod.AlgodClient, 381 | bytecode: str, 382 | local_schema: transaction.StateSchema, 383 | global_schema: transaction.StateSchema, 384 | **kwargs 385 | ): 386 | sp = client.suggested_params() 387 | 388 | acct = get_kmd_accounts().pop() 389 | 390 | txn = transaction.ApplicationCallTxn( 391 | acct.address, 392 | sp, 393 | 0, 394 | transaction.OnComplete.NoOpOC, 395 | local_schema, 396 | global_schema, 397 | b64decode(bytecode), 398 | CLEAR_PROG, 399 | **kwargs 400 | ) 401 | 402 | txid = client.send_transaction(txn.sign(acct.private_key)) 403 | result = transaction.wait_for_confirmation(client, txid, 3) 404 | 405 | return result["application-index"] 406 | 407 | 408 | def call_app(client: algod.AlgodClient, app_id: int, **kwargs): 409 | sp = client.suggested_params() 410 | 411 | acct = get_kmd_accounts().pop() 412 | 413 | txns = transaction.assign_group_id( 414 | [ 415 | transaction.ApplicationOptInTxn(acct.address, sp, app_id), 416 | transaction.ApplicationCallTxn( 417 | acct.address, sp, app_id, transaction.OnComplete.NoOpOC, **kwargs 418 | ), 419 | transaction.ApplicationClearStateTxn(acct.address, sp, app_id), 420 | ] 421 | ) 422 | 423 | drr = transaction.create_dryrun( 424 | client, [txn.sign(acct.private_key) for txn in txns] 425 | ) 426 | result = client.dryrun(drr) 427 | 428 | return get_stats_from_dryrun(result) 429 | 430 | 431 | def destroy_app(client: algod.AlgodClient, app_id: int, **kwargs): 432 | sp = client.suggested_params() 433 | 434 | acct = get_kmd_accounts().pop() 435 | 436 | txns = transaction.assign_group_id( 437 | [ 438 | transaction.ApplicationCallTxn( 439 | acct.address, 440 | sp, 441 | app_id, 442 | transaction.OnComplete.DeleteApplicationOC, 443 | app_args=["cleanup"], 444 | **kwargs 445 | ) 446 | ] 447 | ) 448 | 449 | txid = client.send_transactions([txn.sign(acct.private_key) for txn in txns]) 450 | 451 | transaction.wait_for_confirmation(client, txid, 3) 452 | -------------------------------------------------------------------------------- /tests/teal_diff.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Union 3 | 4 | 5 | class LineDiff: 6 | def __init__(self, line_num: int, x: str, y: str): 7 | self.line_num = line_num 8 | self.x = x 9 | self.y = y 10 | self.same = x == y 11 | 12 | def __repr__(self) -> str: 13 | LN = f"" 14 | return LN + ("(same)" if self.same else f"[{self.x}] --> [{self.y}]") 15 | 16 | 17 | class TEALDiff: 18 | def __init__(self, program_x: str, program_y: str, strip_comments: bool = True): 19 | self.x = program_x 20 | self.y = program_y 21 | self.strip_comments = strip_comments 22 | 23 | self.diffs: List["LineDiff"] = self.diff() 24 | 25 | def all_the_same(self) -> bool: 26 | return all(map(lambda d: d.same, self.diffs)) 27 | 28 | def assert_equals(self, msg: str = None) -> None: 29 | assert self.all_the_same(), msg 30 | 31 | def first_diff(self) -> Union["LineDiff", None]: 32 | for diff in self.diffs: 33 | if not diff.same: 34 | return diff 35 | 36 | return None 37 | 38 | @classmethod 39 | def decomment(cls, line: str) -> str: 40 | m = re.search('(// [^"]+$)', line) 41 | if not m: 42 | return line 43 | 44 | return line[: -len(m.groups()[-1])] 45 | 46 | @classmethod 47 | def lines(cls, src: str, strip_comments: bool = True) -> List[str]: 48 | lines = map(lambda s: s.strip(), src.splitlines()) 49 | if strip_comments: 50 | lines = map(lambda s: cls.decomment(s).strip(), lines) 51 | 52 | return list(lines) 53 | 54 | def diff(self) -> List["LineDiff"]: 55 | xlines = self.lines(self.x, strip_comments=self.strip_comments) 56 | ylines = self.lines(self.y, strip_comments=self.strip_comments) 57 | len_diff = len(ylines) - len(xlines) 58 | if len_diff < 0: 59 | ylines += [""] * -len_diff 60 | elif len_diff > 0: 61 | xlines += [""] * len_diff 62 | 63 | n = len(xlines) 64 | 65 | line_diffs = list( 66 | map(lambda z: LineDiff(*z), zip(range(1, n + 1), xlines, ylines)) 67 | ) 68 | return line_diffs 69 | --------------------------------------------------------------------------------