diff --git a/.flake8 b/.flake8 index 99982dab1..262d4b172 100644 --- a/.flake8 +++ b/.flake8 @@ -10,6 +10,7 @@ ignore = per-file-ignores = pyteal/compiler/optimizer/__init__.py: F401 + examples/application/abi/algobank.py: F403, F405 examples/application/asset.py: F403, F405 examples/application/opup.py: F403, F405 examples/application/security_token.py: F403, F405 diff --git a/docs/abi.rst b/docs/abi.rst new file mode 100644 index 000000000..9186b0ee1 --- /dev/null +++ b/docs/abi.rst @@ -0,0 +1,917 @@ +.. _abi: + +ABI Support +=========== + +.. warning:: + ABI support is still taking shape and is subject to backwards incompatible changes. + * Based on feedback, the API and usage patterns are likely to change. + * For the following use cases, feel encouraged to rely on abstractions. Expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + + * :any:`ABIReturnSubroutine` usage for ABI Application entry point definition. + + * :any:`Router` usage for defining how to route program invocations. + * For general purpose :any:`Subroutine` definition usage, use at your own risk. Based on feedback, the API and usage patterns will change more freely and with less effort to provide migration paths. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +`ARC-4 `_ introduces a set of standards that increase the interoperability of smart contracts in the Algorand ecosystem. This set of standards is commonly referred to as Algorand's application binary interface, or ABI. + +This page will introduce and explain the relevant concepts necessary to build a PyTeal application that adheres to the ARC-4 ABI standards. + +Types +------ + +The ABI supports a variety of data types whose encodings are standardized. + +.. note:: + Be aware that the ABI type system in PyTeal has been designed specifically for the limited use case of describing a program's inputs and outputs. At the time of writing, we **do not recommend** using ABI types in a program's internal storage or computation logic, as the more basic :any:`TealType.uint64` and :any:`TealType.bytes` :any:`Expr` types are far more efficient for these purposes. + +Fundamentals +~~~~~~~~~~~~ + +Before diving into the specific ABI types, let us first explain the the fundamentals of PyTeal's ABI type system, which includes behavior common to all ABI types. + +:code:`abi.BaseType` +^^^^^^^^^^^^^^^^^^^^ + +:any:`abi.BaseType` is an abstract base class that all ABI type classes inherit from. This class defines a few methods common to all ABI types: + +* :any:`abi.BaseType.decode(...) ` is used to decode and populate a type's value from an encoded byte string. +* :any:`abi.BaseType.encode()` is used to encode a type's value into an encoded byte string. +* :any:`abi.BaseType.type_spec()` is used to get an instance of :any:`abi.TypeSpec` that describes that type. + +:code:`abi.TypeSpec` +^^^^^^^^^^^^^^^^^^^^ + +:any:`abi.TypeSpec` is an abstract base class used to describe ABI types. Every child class of :code:`abi.BaseType` also has a companion :code:`abi.TypeSpec` child class. The :code:`abi.TypeSpec` class has a few methods that return information about the type it represents, but one of the class's most important features is the method :any:`abi.TypeSpec.new_instance()`, which creates and returns a new :any:`abi.BaseType` instance of the ABI type it represents. + +Static vs Dynamic Types +^^^^^^^^^^^^^^^^^^^^^^^ + +An important property of an ABI type is whether it is static or dynamic. + +Static types are defined as types whose encoded length does not depend on the value of that type. This property allows encoding and decoding of static types to be more efficient. For example, the encoding of a :code:`boolean` type will always have a fixed length, regardless of whether the value is true or false. + +Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. For example, it's not possible to know the encoding size of a variable-sized :code:`string` type without also knowing its value. Due to this dependency on values, the code that PyTeal generates to encode, decode, and manipulate dynamic types is more complex and generally less efficient than the code needed for static types. + +Because of the difference in complexity and efficiency when working with static and dynamic types, **we strongly recommend using static types over dynamic types whenever possible**. Using static types generally makes your program's resource usage more predictable as well, so you can be more confident your app has enough computation budget and storage space when using static types. + +Instantiating Types +^^^^^^^^^^^^^^^^^^^ + +There are a few ways to create an instance of an ABI type. Each method produces the same result, but some may be more convenient than others in certain situations. + +.. note:: + The following examples reference specific ABI types, which will be introduced in the :ref:`Type Categories` section. + +With the Constructor +"""""""""""""""""""""" + +The most straightforward way is to use its constructor, like so: + +.. code-block:: python + + from pyteal import * + + my_uint8 = abi.Uint8() + my_uint64 = abi.Uint64() + my_array = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 12)) + +For simple types, using the constructor is straightforward and works as you would expect. However, compound types like :any:`abi.StaticArray` have type-level arguments, so their constructor must take an :any:`abi.TypeSpec` which fully defines all necessary arguments. These types can be created with a constructor, but it's often not the most convenient way to do so. + +With an :code:`abi.TypeSpec` Instance +"""""""""""""""""""""""""""""""""""""" + +Recall that :any:`abi.TypeSpec` has a :any:`new_instance() ` method which instantiates ABI types. This is another way of instantiating ABI types, if you have an :any:`abi.TypeSpec` instance available. For example: + +.. code-block:: python + + from pyteal import * + + my_uint_type = abi.Uint8TypeSpec() + my_uint = my_uint_type.new_instance() + + my_array_type = abi.StaticArrayTypeSpec(my_uint_type, 12) + my_array = my_array_type.new_instance() + +With :code:`abi.make` +""""""""""""""""""""" + +Using :code:`abi.TypeSpec.new_instance()` makes sense if you already have an instance of the right :any:`abi.TypeSpec`, but otherwise it's not much better than using the constructor. Because of this, we have the :any:`abi.make` method, which is perhaps the most convenient way to create a compound type. + +To use it, you pass in a `PEP 484 `__ Python type annotation that describes the ABI type, and :any:`abi.make` will create an instance of it for you. For example: + +.. code-block:: python + + from typing import Literal + from pyteal import * + + my_uint8 = abi.make(abi.Uint8) + my_uint64 = abi.make(abi.Uint64) + my_array = abi.make(abi.StaticArray[abi.Uint8, Literal[12]]) + +.. note:: + Since Python does not allow integers to be directly embedded in type annotations, you must wrap any integer arguments in the :code:`Literal` annotation from the :code:`typing` module. + +.. _Computed Values: + +Computed Values +^^^^^^^^^^^^^^^^^^^^^^^ + +With the introduction of ABI types, it's only natural for there to be functions and operations which return ABI values. In a conventional language, it would be enough to return an instance of the type directly from the operation. However, in PyTeal, these operations must actually return two values: + +1. An instance of the ABI type that will be populated with the right value +2. An :code:`Expr` object that contains the expressions necessary to compute and populate the value that the return type should have + +In order to combine these two pieces of information, the :any:`abi.ComputedValue[T] ` interface was introduced. Instead of directly returning an instance of the appropriate ABI type, functions that return ABI values will return an :any:`abi.ComputedValue` instance parameterized by the return type. + +For example, the :any:`abi.Tuple.__getitem__` function does not return an :any:`abi.BaseType`; instead, it returns an :code:`abi.TupleElement[abi.BaseType]` instance, which inherits from :code:`abi.ComputedValue[abi.BaseType]`. + +The :any:`abi.ComputedValue[T] ` abstract base class provides the following methods: + +* :any:`abi.ComputedValue[T].produced_type_spec() `: returns the :any:`abi.TypeSpec` representing the ABI type produced by this object. +* :any:`abi.ComputedValue[T].store_into(output: T) `: computes the value and store it into the ABI type instance :code:`output`. +* :any:`abi.ComputedValue[T].use(action: Callable[[T], Expr]) `: computes the value and passes it to the callable expression :code:`action`. This is offered as a convenience over the :code:`store_into(...)` method if you don't want to create a new variable to store the value before using it. + +.. note:: + If you call the methods :code:`store_into(...)` or :code:`use(...)` multiple times, the computation to determine the value will be repeated each time. For this reason, it's recommended to only issue a single call to either of these two methods. + +A brief example is below: + +.. code-block:: python + + from typing import Literal as L + from pyteal import * + + @Subroutine(TealType.none) + def assert_sum_equals( + array: abi.StaticArray[abi.Uint64, L[10]], expected_sum: Expr + ) -> Expr: + """This subroutine asserts that the sum of the elements in `array` equals `expected_sum`""" + i = ScratchVar(TealType.uint64) + actual_sum = ScratchVar(TealType.uint64) + tmp_value = abi.Uint64() + return Seq( + For(i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1))).Do( + If(i.load() <= Int(5)) + # Both branches of this If statement are equivalent + .Then( + # This branch showcases how to use `store_into` + Seq( + array[i.load()].store_into(tmp_value), + actual_sum.store(actual_sum.load() + tmp_value.get()), + ) + ).Else( + # This branch showcases how to use `use` + array[i.load()].use( + lambda value: actual_sum.store(actual_sum.load() + value.get()) + ) + ) + ), + Assert(actual_sum.load() == expected_sum), + ) + +.. _Type Categories: + +Type Categories +~~~~~~~~~~~~~~~~~~~~ + +There are three categories of ABI types: + +#. :ref:`Basic Types` +#. :ref:`Reference Types` +#. :ref:`Transaction Types` + +Each of which is described in detail in the following subsections. + +.. _Basic Types: + +Basic Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Basic types are the most straightforward category of ABI types. These types are used to hold values and they have no other special meaning, in contrast to the other categories of types. + +Definitions +""""""""""""""""""""" + +PyTeal supports the following basic types: + +============================================== ====================== =================================== ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +============================================== ====================== =================================== ======================================================================================================================================================= +:any:`abi.Uint8` :code:`uint8` Static An 8-bit unsigned integer +:any:`abi.Uint16` :code:`uint16` Static A 16-bit unsigned integer +:any:`abi.Uint32` :code:`uint32` Static A 32-bit unsigned integer +:any:`abi.Uint64` :code:`uint64` Static A 64-bit unsigned integer +:any:`abi.Bool` :code:`bool` Static A boolean value that can be either 0 or 1 +:any:`abi.Byte` :code:`byte` Static An 8-bit unsigned integer. This is an alias for :code:`abi.Uint8` that should be used to indicate non-numeric data, such as binary arrays. +:any:`abi.StaticArray[T,N] ` :code:`T[N]` Static when :code:`T` is static A fixed-length array of :code:`T` with :code:`N` elements +:any:`abi.Address` :code:`address` Static A 32-byte Algorand address. This is an alias for :code:`abi.StaticArray[abi.Byte, Literal[32]]`. +:any:`abi.DynamicArray[T] ` :code:`T[]` Dynamic A variable-length array of :code:`T` +:any:`abi.String` :code:`string` Dynamic A variable-length byte array assumed to contain UTF-8 encoded content. This is an alias for :code:`abi.DynamicArray[abi.Byte]`. +:any:`abi.Tuple`\* :code:`(...)` Static when all elements are static A tuple of multiple types +============================================== ====================== =================================== ======================================================================================================================================================= + +.. note:: + \*A proper implementation of :any:`abi.Tuple` requires a variable amount of generic arguments. Python 3.11 will support this with the introduction of `PEP 646 - Variadic Generics `_, but until then it will not be possible to make :any:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :any:`abi.Tuple` for tuples containing up to 5 generic arguments: + + * :any:`abi.Tuple0`: a tuple of zero values, :code:`()` + * :any:`abi.Tuple1[T1] `: a tuple of one value, :code:`(T1)` + * :any:`abi.Tuple2[T1,T2] `: a tuple of two values, :code:`(T1,T2)` + * :any:`abi.Tuple3[T1,T2,T3] `: a tuple of three values, :code:`(T1,T2,T3)` + * :any:`abi.Tuple4[T1,T2,T3,T4] `: a tuple of four values, :code:`(T1,T2,T3,T4)` + * :any:`abi.Tuple5[T1,T2,T3,T4,T5] `: a tuple of five values, :code:`(T1,T2,T3,T4,T5)` + +These ARC-4 types are not yet supported in PyTeal: + +* Non-power-of-2 unsigned integers under 64 bits, i.e. :code:`uint24`, :code:`uint48`, :code:`uint56` +* Unsigned integers larger than 64 bits +* Fixed point unsigned integers, i.e. :code:`ufixedx` + +Limitations +""""""""""""""""""""" + +Due to the nature of their encoding, dynamic container types, i.e. :any:`abi.DynamicArray[T] ` and :any:`abi.String`, have an implicit limit on the number of elements they may contain. This limit is :code:`2^16 - 1`, or 65535. However, the AVM has a stack size limit of 4096 for byte strings, so it's unlikely this encoding limit will be reached by your program. + +Static container types have no such limit. + +Usage +""""""""""""""""""""" + +Setting Values +'''''''''''''''' + +All basic types have a :code:`set()` method which can be used to assign a value. The arguments for this method differ depending on the ABI type. For convenience, here are links to the docs for each class's method: + +* :any:`abi.Uint.set(...) `, which is used by all :code:`abi.Uint` classes and :code:`abi.Byte` +* :any:`abi.Bool.set(...) ` +* :any:`abi.StaticArray[T, N].set(...) ` +* :any:`abi.Address.set(...) ` +* :any:`abi.DynamicArray[T].set(...) ` +* :any:`abi.String.set(...) ` +* :any:`abi.Tuple.set(...) ` + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from pyteal import * + + my_address = abi.make(abi.Address) + my_bool = abi.make(abi.Bool) + my_uint64 = abi.make(abi.Uint64) + my_tuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) + + program = Seq( + my_address.set(Txn.sender()), + my_bool.set(Txn.fee() == Int(0)), + # It's ok to set an abi.Uint to a Python integer. This is actually preferred since PyTeal + # can determine at compile-time that the value will fit in the integer type. + my_uint64.set(5000), + my_tuple.set(my_address, my_bool, my_uint64) + ) + +Getting Single Values +'''''''''''''''''''''' + +All basic types that represent a single value have a :code:`get()` method, which can be used to extract that value. The supported types and methods are: + +* :any:`abi.Uint.get()`, which is used by all :any:`abi.Uint` classes and :any:`abi.Byte` +* :any:`abi.Bool.get()` +* :any:`abi.Address.get()` +* :any:`abi.String.get()` + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.uint64) + def minimum(a: abi.Uint64, b: abi.Uint64) -> Expr: + """Return the minimum value of the two arguments.""" + return ( + If(a.get() < b.get()) + .Then(a.get()) + .Else(b.get()) + ) + +Getting Values at Indexes - Compound Types +'''''''''''''''''''''''''''''''''''''''''' + +The types :any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, :any:`abi.String`, and :any:`abi.Tuple` are compound types, meaning they contain other types whose values can be extracted. The :code:`__getitem__` method, accessible by using square brackets to "index into" an object, can be used to access these values. + +The supported methods are: + +* :any:`abi.StaticArray.__getitem__(index: int | Expr) `, used for :any:`abi.StaticArray` and :any:`abi.Address` +* :any:`abi.Array.__getitem__(index: int | Expr) `, used for :any:`abi.DynamicArray` and :any:`abi.String` +* :any:`abi.Tuple.__getitem__(index: int) ` + +.. note:: + Be aware that these methods return a :any:`ComputedValue`, similar to other PyTeal operations which return ABI types. More information about why that is necessary and how to use a :any:`ComputedValue` can be found in the :ref:`Computed Values` section. + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from typing import Literal as L + from pyteal import * + + @Subroutine(TealType.none) + def ensure_all_values_greater_than_5(array: abi.StaticArray[abi.Uint64, L[10]]) -> Expr: + """This subroutine asserts that every value in the input array is greater than 5.""" + i = ScratchVar(TealType.uint64) + return For( + i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1)) + ).Do( + array[i.load()].use(lambda value: Assert(value.get() > Int(5))) + ) + +.. _Reference Types: + +Reference Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Many applications require the caller to provide "foreign array" values when calling the app. These are the blockchain entities (such as accounts, assets, or other applications) that the application will interact with when executing this call. In the ABI, we have **Reference Types** to describe these requirements. + +Definitions +"""""""""""""""""""""""""""""""""""""""""" + +PyTeal supports the following Reference Types: + +====================== ====================== ================ ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +====================== ====================== ================ ======================================================================================================================================================= +:any:`abi.Account` :code:`account` Static Represents an account that the current transaction can access, stored in the :any:`Txn.accounts ` array +:any:`abi.Asset` :code:`asset` Static Represents an asset that the current transaction can access, stored in the :any:`Txn.assets ` array +:any:`abi.Application` :code:`application` Static Represents an application that the current transaction can access, stored in the :any:`Txn.applications ` array +====================== ====================== ================ ======================================================================================================================================================= + +These types all inherit from the abstract class :any:`abi.ReferenceType`. + +Limitations +"""""""""""""""""""""""""""""""""""""""""" + +Because References Types have a special meaning, they should not be directly created, and they cannot be assigned a value by a program. + +Additionally, Reference Types are only valid in the arguments of a method. They may not appear in a method's return value. + +Note that the AVM has `limitations on the maximum number of foreign references `_ an application call transaction may contain. At the time of writing, these limits are: + +* Accounts: 4 +* Assets: 8 +* Applications: 8 +* Sum of Accounts, Assets, and Applications: 8 + +.. warning:: + Because of these limits, methods that have a large amount of Reference Type arguments may be impossible to call as intended at runtime. + +Usage +"""""""""""""""""""""""""""""""""""""""""" + +Getting Referenced Values +'''''''''''''''''''''''''' + +Depending on the Reference Type, there are different methods available to obtain the value being referenced: + +* :any:`abi.Account.address()` +* :any:`abi.Asset.asset_id()` +* :any:`abi.Application.application_id()` + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def send_inner_txns( + receiver: abi.Account, asset_to_transfer: abi.Asset, app_to_call: abi.Application + ) -> Expr: + return Seq( + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.AssetTransfer, + TxnField.receiver: receiver.address(), + TxnField.xfer_asset: asset_to_transfer.asset_id(), + TxnField.amount: Int(1_000_000), + } + ), + InnerTxnBuilder.Submit(), + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.ApplicationCall, + TxnField.application_id: app_to_call.application_id(), + Txn.application_args: [Bytes("hello")], + } + ), + InnerTxnBuilder.Submit(), + ) + +Accessing Parameters of Referenced Values +'''''''''''''''''''''''''''''''''''''''''' + +Reference Types allow the program to access more information about them. Each Reference Type has a :code:`params()` method which can be used to access that object's parameters. These methods are listed below: + +* :any:`abi.Account.params()` returns an :any:`AccountParamObject` +* :any:`abi.Asset.params()` returns an :any:`AssetParamObject` +* :any:`abi.Application.params()` returns an :any:`AppParamObject` + +These method are provided for convenience. They expose the same properties accessible from the :any:`AccountParam`, :any:`AssetParam`, and :any:`AppParam` classes. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def referenced_params_example( + account: abi.Account, asset: abi.Asset, app: abi.Application + ) -> Expr: + return Seq( + account.params().auth_address().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Global.zero_address())) + ), + asset.params().total().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Int(1))) + ), + app.params().creator_address().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Txn.sender())) + ) + ) + +.. note:: + All returned parameters are instances of :any:`MaybeValue`, which is why the :any:`outputReducer(...) ` method is used. + +Accessing Asset Holdings +'''''''''''''''''''''''' + +Similar to the parameters above, asset holding properties can be accessed using one of the following methods: + +* :any:`abi.Account.asset_holding(asset: Expr | abi.Asset) `: given an asset, returns an :any:`AssetHoldingObject` +* :any:`abi.Asset.holding(account: Expr | abi.Account) `: given an account, returns an :any:`AssetHoldingObject` + +These method are provided for convenience. They expose the same properties accessible from the :any:`AssetHolding` class. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def ensure_asset_balance_is_nonzero(account: abi.Account, asset: abi.Asset) -> Expr: + return Seq( + account.asset_holding(asset) + .balance() + .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))), + # this check is equivalent + asset.holding(account) + .balance() + .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))), + ) + +.. _Transaction Types: + +Transaction Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Some application calls require that they are invoked as part of a larger transaction group containing specific additional transactions. In order to express these types of calls, the ABI has **Transaction Types**. + +Every Transaction Type argument represents a specific and unique transaction that must appear immediately before the application call in the same transaction group. A method may have multiple Transaction Type arguments, in which case they must appear in the same order as the method's arguments immediately before the method application call. + +Definitions +"""""""""""""""""""""""""""""""""""""""""" + +PyTeal supports the following Transaction Types: + +===================================== ====================== ================ ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +===================================== ====================== ================ ======================================================================================================================================================= +:any:`abi.Transaction` :code:`txn` Static A catch-all for any type of transaction +:any:`abi.PaymentTransaction` :code:`pay` Static A payment transaction +:any:`abi.KeyRegisterTransaction` :code:`keyreg` Static A key registration transaction +:any:`abi.AssetConfigTransaction` :code:`acfg` Static An asset configuration transaction +:any:`abi.AssetTransferTransaction` :code:`axfer` Static An asset transfer transaction +:any:`abi.AssetFreezeTransaction` :code:`afrz` Static An asset freeze transaction +:any:`abi.ApplicationCallTransaction` :code:`appl` Static An application call transaction +===================================== ====================== ================ ======================================================================================================================================================= + +Limitations +"""""""""""""""""""""""""""""""""""""""""" + +Due to the special meaning of Transaction Types, they **cannot** be used as the return value of a method. They can be used as method arguments, but only at the top-level. This means that it's not possible to embed a Transaction Type inside a tuple or array. + +Transaction Types should not be directly created, and they cannot be modified by a program. + +Because the AVM has a maximum of 16 transactions in a single group, at most 15 Transaction Types may be used in the arguments of a method. + +Usage +"""""""""""""""""""""""""""""""""""""""""" + +Getting the Transaction Group Index +'''''''''''''''''''''''''''''''''''' + +All Transaction Types implement the :any:`abi.Transaction.index()` method, which returns the absolute index of that transaction in the group. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def handle_txn_args( + any_txn: abi.Transaction, + pay: abi.PaymentTransaction, + axfer: abi.AssetTransferTransaction, + ) -> Expr: + return Seq( + Assert(any_txn.index() == Txn.group_index() - Int(3)), + Assert(pay.index() == Txn.group_index() - Int(2)), + Assert(axfer.index() == Txn.group_index() - Int(1)), + ) + +Accessing Transaction Fields +''''''''''''''''''''''''''''' + +All Transaction Types implement the :any:`abi.Transaction.get()` method, which returns a :any:`TxnObject` instance that can be used to access fields from that transaction. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + """This method receives a payment from an account opted into this app + and records it in their local state. + """ + return Seq( + Assert(payment.get().sender() == sender.address()), + Assert(payment.get().receiver() == Global.current_application_address()), + App.localPut( + sender.address(), + Bytes("balance"), + App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(), + ), + ) + + +Subroutines with ABI Types +-------------------------- + +Subroutines can be created that accept ABI types as arguments and produce ABI types as return values. PyTeal will type check all subroutine calls and ensure that the correct types are being passed to such subroutines and that their return values are used correctly. + +There are two different ways to use ABI types in subroutines, depending on whether the return value is an ABI type or a PyTeal :code:`Expr`. + +Subroutines that Return Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you'd like to create a subroutine that accepts some or all arguments as ABI types, but whose return value is a PyTeal :code:`Expr`, the :any:`@Subroutine ` decorator can be used. + +To indicate the type of each argument, `PEP 484 `__ Python type annotations are used. Unlike normal usage of Python type annotations which are ignored at runtime, type annotations for subroutines inform the PyTeal compiler about the inputs and outputs of a subroutine. Changing these values has a direct effect on the code PyTeal generates. + +An example of this type of subroutine is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.uint64) + def get_volume_of_rectangular_prism( + length: abi.Uint16, width: abi.Uint64, height: Expr + ) -> Expr: + return length.get() * width.get() * height + +Notice that this subroutine accepts the following arguments, not all of which are ABI types: + +* :code:`length`: an ABI :any:`abi.Uint16` type +* :code:`width`: an ABI :any:`abi.Uint64` type +* :code:`height`: a PyTeal :any:`Expr` type + +Despite some inputs being ABI types, calling this subroutine works the same as usual, except the values for the ABI type arguments must be the appropriate ABI type. + +The following example shows how to prepare the arguments for and call :code:`get_volume_of_rectangular_prism()`: + +.. code-block:: python + + # This is a continuation of the previous example + + length = abi.Uint16() + width = abi.Uint64() + height = Int(10) + program = Seq( + length.set(4), + width.set(9), + Assert(get_volume_of_rectangular_prism(length, width, height) > Int(0)) + ) + +Subroutines that Return ABI Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + :any:`ABIReturnSubroutine` is still taking shape and is subject to backwards incompatible changes. + + * For ABI Application entry point definition, feel encouraged to use :any:`ABIReturnSubroutine`. Expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + * For general purpose usage, use at your own risk. Based on feedback, the API and usage patterns will change more freely and with less effort to provide migration paths. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +In addition to accepting ABI types as arguments, it's also possible for a subroutine to return an ABI type value. + +As mentioned in the :ref:`Computed Values` section, operations which return ABI values instead of traditional :code:`Expr` objects need extra care. In order to solve this problem for subroutines, a new decorator, :any:`@ABIReturnSubroutine ` has been introduced. + +The :code:`@ABIReturnSubroutine` decorator should be used with subroutines that return an ABI value. Subroutines defined with this decorator will have two places to output information: the function return value, and a `keyword-only argument `_ called :code:`output`. The function return value must remain an :code:`Expr`, while the :code:`output` keyword argument will contain the ABI value the subroutine wishes to return. An example is below: + +.. code-block:: python + + from pyteal import * + + @ABIReturnSubroutine + def get_account_status( + account: abi.Address, *, output: abi.Tuple2[abi.Uint64, abi.Bool] + ) -> Expr: + balance = abi.Uint64() + is_admin = abi.Bool() + return Seq( + balance.set(App.localGet(account.get(), Bytes("balance"))), + is_admin.set(App.localGet(account.get(), Bytes("is_admin"))), + output.set(balance, is_admin), + ) + + account = abi.make(abi.Address) + status = abi.make(abi.Tuple2[abi.Uint64, abi.Bool]) + program = Seq( + account.set(Txn.sender()), + # NOTE! The return value of get_account_status(account) is actually a ComputedValue[abi.Tuple2[abi.Uint64, abi.Bool]] + get_account_status(account).store_into(status), + ) + +Notice that even though the original :code:`get_account_status` function returns an :code:`Expr` object, the :code:`@ABIReturnSubroutine` decorator automatically transforms the function's return value and the :code:`output` variable into a :code:`ComputedValue`. As a result, callers of this subroutine must work with a :code:`ComputedValue`. + +The only exception to this transformation is if the subroutine has no return value. Without a return value, a :code:`ComputedValue` is unnecessary and the subroutine will still return an :code:`Expr` to the caller. In this case, the :code:`@ABIReturnSubroutine` decorator acts identically the :code:`@Subroutine` decorator. + +Creating an ARC-4 Program +---------------------------------------------------- + +An ARC-4 program, like all other programs, can be called by application call transactions. ARC-4 programs respond to two specific subtypes of application call transactions: + +* **Method calls**, which encode a specific method to be called and arguments for that method, if needed. +* **Bare app calls**, which have no arguments and no return value. + +A method is a section of code intended to be invoked externally with an application call transaction. Methods may take arguments and may produce a return value. PyTeal implements methods as subroutines which are exposed to be externally callable. + +A bare app call is more limited than a method, since it takes no arguments and cannot return a value. For this reason, bare app calls are more suited to allow on completion actions to take place, such as opting into an app. + +To make it easier for an application to route across the many bare app calls and methods it may support, PyTeal introduces the :any:`Router` class. This class adheres to the ARC-4 ABI conventions with respect to when methods and bare app calls should be invoked. For methods, it also conveniently decodes all arguments and properly encodes and logs the return value as needed. + +The following sections explain how to register bare app calls and methods with the :any:`Router` class. + +.. warning:: + :any:`Router` usage is still taking shape and is subject to backwards incompatible changes. + + Feel encouraged to use :any:`Router` and expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +Registering Bare App Calls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The AVM supports 6 types of OnCompletion options that may be specified on an app call transaction. These actions are: + +#. **No-op**, the absence of an action, represented by :any:`OnComplete.NoOp` +#. **Opt in**, which allocates account-local storage for an app, represented by :any:`OnComplete.OptIn` +#. **Close out**, which removes account-local storage for an app, represented by :any:`OnComplete.CloseOut` +#. **Clear state**, which forcibly removes account-local storage for an app, represented by :any:`OnComplete.ClearState` +#. **Update application**, which updates an app, represented by :any:`OnComplete.UpdateApplication` +#. **Delete application**, which deletes an app, represented by :any:`OnComplete.DeleteApplication` + +In PyTeal, you have the ability to register a bare app call handler for each of these actions. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`). + +The :any:`BareCallActions` class is used to define a bare app call handler for on completion actions. Each bare app call handler must be an instance of the :any:`OnCompleteAction` class. + +The :any:`OnCompleteAction` class is responsible for holding the actual code for the bare app call handler (an instance of either :code:`Expr` or a subroutine that takes no args and returns nothing) as well as a :any:`CallConfig` option that indicates whether the action is able to be called during a creation app call, a non-creation app call, or either. + +All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def opt_in_handler() -> Expr: + return App.localPut(Txn.sender(), Bytes("opted_in_round"), Global.round()) + + + @Subroutine(TealType.none) + def assert_sender_is_creator() -> Expr: + return Assert(Txn.sender() == Global.creator_address()) + + + router = Router( + name="ExampleApp", + bare_calls=BareCallActions( + # Allow app creation with a no-op action + no_op=OnCompleteAction( + action=Approve(), call_config=CallConfig.CREATE + ), + + # Register the `opt_in_handler` to be called during opt in. + # + # Since we use `CallConfig.ALL`, this is also a valid way to create this app + # (if the creator wishes to immediately opt in). + opt_in=OnCompleteAction( + action=opt_in_handler, call_config=CallConfig.ALL + ), + + # Allow anyone who opted in to close out from the app. + close_out=OnCompleteAction( + action=Approve(), call_config=CallConfig.CALL + ), + + # Only approve update and delete operations if `assert_sender_is_creator` succeeds. + update_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + delete_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + ), + ) + +.. note:: + When deciding which :any:`CallConfig` value is appropriate for a bare app call or method, consider the question, should it be valid for someone to create my app with this operation? Most of the time the answer will be no, in which case :any:`CallConfig.CALL` should be used. + +Registering Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + The :any:`Router` **does not** validate inputs for compound types (:any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, :any:`abi.String`, or :any:`abi.Tuple`). + + **We strongly recommend** methods immediately access and validate compound type parameters *before* persisting arguments for later transactions. For validation, it is sufficient to attempt to extract each element your method will use. If there is an input error for an element, indexing into that element will fail. + + Notes: + + * This recommendation applies to recursively contained compound types as well. Successfully extracting an element which is a compound type does not guarantee the extracted value is valid; you must also inspect its elements as well. + * Because of this, :any:`abi.Address` is **not** guaranteed to have exactly 32 bytes. To defend against unintended behavior, manually verify the length is 32 bytes, i.e. :code:`Assert(Len(address.get()) == Int(32))`. + +There are two ways to register a method with the :any:`Router` class. + +The first way to register a method is with the :any:`Router.add_method_handler` method, which takes an existing subroutine decorated with :any:`@ABIReturnSubroutine `. An example of this is below: + +.. code-block:: python + + from pyteal import * + + router = Router( + name="Calculator", + bare_calls=BareCallActions( + # Allow this app to be created with a no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # Allow standalone user opt in and close out + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + ), + ) + + @ABIReturnSubroutine + def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments and returns the result. + + If addition will overflow a uint64, this method will fail. + """ + return output.set(a.get() + b.get()) + + + @ABIReturnSubroutine + def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments, returns the result, and stores it in the sender's local state. + + If addition will overflow a uint64, this method will fail. + + The sender must be opted into the app. Opt-in can occur during this call. + """ + return Seq( + output.set(a.get() + b.get()), + # store the result in the sender's local state too + App.localPut(Txn.sender(), Bytes("result", output.get())), + ) + + # Register the `add` method with the router, using the default `MethodConfig` + # (only no-op, non-creation calls allowed). + router.add_method_handler(add) + + # Register the `addAndStore` method with the router, using a `MethodConfig` that allows + # no-op and opt in non-creation calls. + router.add_method_handler( + addAndStore, + method_config=MethodConfig(no_op=CallConfig.CALL, opt_in=CallConfig.CALL), + ) + +This example registers two methods with the router, :code:`add` and :code:`addAndStore`. + +Because the :code:`add` method does not pass a value for the :code:`method_config` parameter of :any:`Router.add_method_handler`, it will use the default value, which will make it only callable with a transaction that is not an app creation and whose on completion value is :code:`OnComplete.NoOp`. + +On the other hand, the :code:`addAndStore` method does provide a :code:`method_config` value. A value of :code:`MethodConfig(no_op=CallConfig.CALL, opt_in=CallConfig.CALL)` indicates that this method can only be called with a transaction that is not an app creation and whose on completion value is one of :code:`OnComplete.NoOp` or :code:`OnComplete.OptIn`. + +The second way to register a method is with the :any:`Router.method` decorator placed directly on a function. This way is equivalent to the first, but has some properties that make it more convenient for some scenarios. Below is an example equivalent to the prior one, but using the :code:`Router.method` syntax: + +.. code-block:: python + + from pyteal import * + + my_router = Router( + name="Calculator", + bare_calls=BareCallActions( + # Allow this app to be created with a no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # Allow standalone user opt in and close out + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + ), + ) + + # NOTE: the first part of the decorator `@my_router.method` is the router variable's name + @my_router.method + def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments and returns the result. + + If addition will overflow a uint64, this method will fail. + """ + return output.set(a.get() + b.get()) + + @my_router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL) + def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments, returns the result, and stores it in the sender's local state. + + If addition will overflow a uint64, this method will fail. + + The sender must be opted into the app. Opt-in can occur during this call. + """ + return Seq( + output.set(a.get() + b.get()), + # store the result in the sender's local state too + App.localPut(Txn.sender(), Bytes("result", output.get())), + ) + +Compiling a Router Program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we know how to add bare app call and method call handlers to a :any:`Router`, the next step is to compile the :any:`Router` into TEAL code. + +The :any:`Router.compile_program` method exists for this purpose. It combines all registered methods and bare app calls into two ASTs, one for the approval program and one for clear state program, then internally calls :any:`compileTeal` to compile these expressions and create TEAL code. + +.. note:: + We recommend enabling the :code:`scratch_slots` optimization when compiling a program that uses ABI types, since PyTeal's ABI types implementation makes frequent use of scratch slots under-the-hood. See the :ref:`compiler_optimization` page for more information. + +In addition to receiving the approval and clear state programs, the :any:`Router.compile_program` method also returns a `Python SDK `_ :code:`Contract` object. This object represents an `ARC-4 Contract Description `_, which can be distributed to clients to enable them to call the methods on the contract. + +Here's an example of a complete application that uses the :any:`Router` class: + +.. literalinclude:: ../examples/application/abi/algobank.py + :language: python + +This example uses the :code:`Router.compile_program` method to create the approval program, clear state program, and contract description for the "AlgoBank" contract. The produced :code:`algobank.json` file is below: + +.. literalinclude:: ../examples/application/abi/algobank.json + :language: json + +Calling an ARC-4 Program +-------------------------- + +One of the advantages of developing an ABI-compliant PyTeal contract is that there is a standard way for clients to call your contract. + +Broadly, there are two categories of clients that may wish to call your contract: off-chain systems and other on-chain contracts. The following sections describe how each of these clients can call ABI methods implemented by your contract. + +Off-Chain, from an SDK or :code:`goal` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Off-chain systems can use the `Algorand SDKs `_ or the command-line tool `goal `_ to interact with ABI-compliant contracts. + +Every SDK contains an :code:`AtomicTransactionComposer` type that can be used to build and execute transaction groups, including groups containing ABI method calls. More information and examples of this are available on the `Algorand Developer Portal `_. + +The :code:`goal` CLI has subcommands for creating and submitting various types of transactions. The relevant ones for ABI-compliant contracts are mentioned below: + +* For bare app calls: + * For calls that create an app, :code:`goal app create` (`docs `__) can be used to construct and send an app creation bare app call. + * For non-creation calls, :code:`goal app ` can be used to construct and send a non-creation bare app call. The :code:`` keyword should be replaced with one of `"call" `_ (no-op), `"optin" `_, `"closeout" `_, `"clear" `_, `"update" `_, or `"delete" `_, depending on the on-completion value the caller wishes to use. +* For all method calls: + * :code:`goal app method` (`docs `__) can be used to construct, send, and read the return value of a method call. This command can be used for application creation as well, if the application allows this to happen in one of its methods. + +On-Chain, in an Inner Transaction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Algorand applications can issue `inner transactions `_, which can be used to invoke other applications. + +In PyTeal, this can be achieved using the :any:`InnerTxnBuilder` class and its functions. To invoke an ABI method, PyTeal has :any:`InnerTxnBuilder.MethodCall(...) ` to properly build a method call and encode its arguments. + +.. note:: + At the time of writing, there is no streamlined way to obtain a method return value. You must manually inspect the :any:`last_log() ` property of either :any:`InnerTxn` or :any:`Gitxn[\] ` to obtain the logged return value. `As described in ARC-4 `_, this value will be prefixed with the 4 bytes :code:`151f7c75` (shown in hex), and after this prefix the encoded ABI value will be available. You can use the :any:`decode(...) ` method on an instance of the appropriate ABI type in order to decode and use this value. diff --git a/docs/api.rst b/docs/api.rst index b427806b5..14e5b4666 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,3 +24,19 @@ PyTeal Package :annotation: = The most recently submitted inner transaction. This is an instance of :any:`TxnObject`. + + If a transaction group was submitted most recently, then this will be the last transaction in that group. + + .. data:: Gitxn + :annotation: = + + The most recently submitted inner transaction group. This is an instance of :any:`InnerTxnGroup`. + + If a single transaction was submitted most recently, then this will be a group of size 1. + +.. automodule:: pyteal.abi + :members: + :undoc-members: + :imported-members: + :special-members: __getitem__ + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index 9ff3f2fe8..5377bc076 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,6 +45,7 @@ PyTeal **hasn't been security audited**. Use it at your own risk. versions compiler_optimization opup + abi .. toctree:: :maxdepth: 3 diff --git a/docs/versions.rst b/docs/versions.rst index b64910451..08b5fcf3b 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -1,6 +1,6 @@ .. _versions: -TEAL Versions +Versions ============= Each version of PyTeal compiles contracts for a specific version of TEAL. Newer versions of TEAL @@ -18,14 +18,34 @@ TEAL Version PyTeal Version 6 >= 0.10.0 ============ ============== +.. _version pragmas: + +Version Pragmas +---------------- + +When writing a PyTeal smart contract, it's important to target a specific AVM version and to compile +with a single PyTeal version. This will ensure your compiled program remains consistent and has the +exact same behavior no matter when you compile it. + +The :any:`pragma` function can be used to assert that the current PyTeal version matches a constraint +of your choosing. This can help strengthen the dependency your source code has on the PyTeal package +version you used when writing it. + +If you are writing code for others to consume, or if your codebase has different PyTeal version +dependencies in different places, the :any:`Pragma` expression can be used to apply a pragma +constraint to only a section of the AST. + +PyTeal v0.5.4 and Below +----------------------- + In order to support TEAL v2, PyTeal v0.6.0 breaks backward compatibility with v0.5.4. PyTeal programs written for PyTeal version 0.5.4 and below will not compile properly and most likely will display an error of the form :code:`AttributeError: * object has no attribute 'teal'`. -**WARNING:** before updating PyTeal to a version with generates TEAL v2 contracts and fixing the -programs to use the global function :any:`compileTeal` rather the class method :code:`.teal()`, make -sure your program abides by the TEAL safety guidelines ``_. -Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2 -TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property -of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and -below. +.. warning:: + If you are updating from a v1 TEAL program, make + sure your program abides by the `TEAL safety guidelines `_. + Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2 + TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property + of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and + below. diff --git a/examples/application/abi/__init__.py b/examples/application/abi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/application/abi/algobank.json b/examples/application/abi/algobank.json new file mode 100644 index 000000000..a0e3ef26a --- /dev/null +++ b/examples/application/abi/algobank.json @@ -0,0 +1,54 @@ +{ + "name": "AlgoBank", + "methods": [ + { + "name": "deposit", + "args": [ + { + "type": "pay", + "name": "payment" + }, + { + "type": "account", + "name": "sender" + } + ], + "returns": { + "type": "void" + }, + "desc": "This method receives a payment from an account opted into this app and records it in their local state. The caller may opt into this app during this call." + }, + { + "name": "getBalance", + "args": [ + { + "type": "account", + "name": "user" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Lookup the balance of a user held by this app." + }, + { + "name": "withdraw", + "args": [ + { + "type": "uint64", + "name": "amount" + }, + { + "type": "account", + "name": "recipient" + } + ], + "returns": { + "type": "void" + }, + "desc": "Withdraw an amount of Algos held by this app. The sender of this method call will be the source of the Algos, and the destination will be the `recipient` argument. This may or may not be the same as the sender's address. This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of Algos held by this app for the sender. The Algos will be transferred to the recipient using an inner transaction whose fee is set to 0, meaning the caller's transaction must include a surplus fee to cover the inner transaction." + } + ], + "desc": null, + "networks": {} +} \ No newline at end of file diff --git a/examples/application/abi/algobank.py b/examples/application/abi/algobank.py new file mode 100644 index 000000000..6950825ee --- /dev/null +++ b/examples/application/abi/algobank.py @@ -0,0 +1,113 @@ +# This example is provided for informational purposes only and has not been audited for security. +from pyteal import * +import json + + +@Subroutine(TealType.none) +def assert_sender_is_creator() -> Expr: + return Assert(Txn.sender() == Global.creator_address()) + + +# move any balance that the user has into the "lost" amount when they close out or clear state +transfer_balance_to_lost = App.globalPut( + Bytes("lost"), + App.globalGet(Bytes("lost")) + App.localGet(Txn.sender(), Bytes("balance")), +) + +router = Router( + name="AlgoBank", + bare_calls=BareCallActions( + # approve a creation no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # approve opt-in calls during normal usage, and during creation as a convenience for the creator + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.ALL), + # move any balance that the user has into the "lost" amount when they close out or clear state + close_out=OnCompleteAction( + action=transfer_balance_to_lost, call_config=CallConfig.CALL + ), + clear_state=OnCompleteAction( + action=transfer_balance_to_lost, call_config=CallConfig.CALL + ), + # only the creator can update or delete the app + update_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + delete_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + ), +) + + +@router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL) +def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + """This method receives a payment from an account opted into this app and records it in + their local state. + + The caller may opt into this app during this call. + """ + return Seq( + Assert(payment.get().sender() == sender.address()), + Assert(payment.get().receiver() == Global.current_application_address()), + App.localPut( + sender.address(), + Bytes("balance"), + App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(), + ), + ) + + +@router.method +def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + """Lookup the balance of a user held by this app.""" + return output.set(App.localGet(user.address(), Bytes("balance"))) + + +@router.method +def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: + """Withdraw an amount of Algos held by this app. + + The sender of this method call will be the source of the Algos, and the destination will be + the `recipient` argument. This may or may not be the same as the sender's address. + + This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of + Algos held by this app for the sender. + + The Algos will be transferred to the recipient using an inner transaction whose fee is set + to 0, meaning the caller's transaction must include a surplus fee to cover the inner + transaction. + """ + return Seq( + # if amount is larger than App.localGet(Txn.sender(), Bytes("balance")), the subtraction + # will underflow and fail this method call + App.localPut( + Txn.sender(), + Bytes("balance"), + App.localGet(Txn.sender(), Bytes("balance")) - amount.get(), + ), + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.Payment, + TxnField.receiver: recipient.address(), + TxnField.amount: amount.get(), + TxnField.fee: Int(0), + } + ), + InnerTxnBuilder.Submit(), + ) + + +approval_program, clear_state_program, contract = router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) +) + +if __name__ == "__main__": + with open("algobank_approval.teal", "w") as f: + f.write(approval_program) + + with open("algobank_clear_state.teal", "w") as f: + f.write(clear_state_program) + + with open("algobank.json", "w") as f: + f.write(json.dumps(contract.dictify(), indent=4)) diff --git a/examples/application/abi/algobank_approval.teal b/examples/application/abi/algobank_approval.teal new file mode 100644 index 000000000..be89cd0d3 --- /dev/null +++ b/examples/application/abi/algobank_approval.teal @@ -0,0 +1,226 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l8 +txna ApplicationArgs 0 +method "deposit(pay,account)void" +== +bnz main_l7 +txna ApplicationArgs 0 +method "getBalance(account)uint64" +== +bnz main_l6 +txna ApplicationArgs 0 +method "withdraw(uint64,account)void" +== +bnz main_l5 +err +main_l5: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 3 +txna ApplicationArgs 2 +int 0 +getbyte +store 4 +load 3 +load 4 +callsub withdraw_3 +int 1 +return +main_l6: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +int 0 +getbyte +callsub getBalance_2 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return +main_l7: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +txna ApplicationArgs 1 +int 0 +getbyte +store 1 +txn GroupIndex +int 1 +- +store 0 +load 0 +gtxns TypeEnum +int pay +== +assert +load 0 +load 1 +callsub deposit_1 +int 1 +return +main_l8: +txn OnCompletion +int NoOp +== +bnz main_l18 +txn OnCompletion +int OptIn +== +bnz main_l17 +txn OnCompletion +int CloseOut +== +bnz main_l16 +txn OnCompletion +int UpdateApplication +== +bnz main_l15 +txn OnCompletion +int DeleteApplication +== +bnz main_l14 +err +main_l14: +txn ApplicationID +int 0 +!= +assert +callsub assertsenderiscreator_0 +int 1 +return +main_l15: +txn ApplicationID +int 0 +!= +assert +callsub assertsenderiscreator_0 +int 1 +return +main_l16: +txn ApplicationID +int 0 +!= +assert +byte "lost" +byte "lost" +app_global_get +txn Sender +byte "balance" +app_local_get ++ +app_global_put +int 1 +return +main_l17: +int 1 +return +main_l18: +txn ApplicationID +int 0 +== +assert +int 1 +return + +// assert_sender_is_creator +assertsenderiscreator_0: +txn Sender +global CreatorAddress +== +assert +retsub + +// deposit +deposit_1: +store 6 +store 5 +load 5 +gtxns Sender +load 6 +txnas Accounts +== +assert +load 5 +gtxns Receiver +global CurrentApplicationAddress +== +assert +load 6 +txnas Accounts +byte "balance" +load 6 +txnas Accounts +byte "balance" +app_local_get +load 5 +gtxns Amount ++ +app_local_put +retsub + +// getBalance +getBalance_2: +txnas Accounts +byte "balance" +app_local_get +retsub + +// withdraw +withdraw_3: +store 8 +store 7 +txn Sender +byte "balance" +txn Sender +byte "balance" +app_local_get +load 7 +- +app_local_put +itxn_begin +int pay +itxn_field TypeEnum +load 8 +txnas Accounts +itxn_field Receiver +load 7 +itxn_field Amount +int 0 +itxn_field Fee +itxn_submit +retsub \ No newline at end of file diff --git a/examples/application/abi/algobank_clear_state.teal b/examples/application/abi/algobank_clear_state.teal new file mode 100644 index 000000000..a917c873b --- /dev/null +++ b/examples/application/abi/algobank_clear_state.teal @@ -0,0 +1,17 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l2 +err +main_l2: +byte "lost" +byte "lost" +app_global_get +txn Sender +byte "balance" +app_local_get ++ +app_global_put +int 1 +return \ No newline at end of file diff --git a/pyteal/ast/abi/__init__.py b/pyteal/ast/abi/__init__.py index 2ab218427..e989f347b 100644 --- a/pyteal/ast/abi/__init__.py +++ b/pyteal/ast/abi/__init__.py @@ -35,14 +35,14 @@ from pyteal.ast.abi.array_static import StaticArrayTypeSpec, StaticArray from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec, DynamicArray from pyteal.ast.abi.reference_type import ( + ReferenceTypeSpec, + ReferenceType, Account, AccountTypeSpec, Asset, AssetTypeSpec, Application, ApplicationTypeSpec, - ReferenceType, - ReferenceTypeSpec, ReferenceTypeSpecs, ) from pyteal.ast.abi.transaction import ( @@ -78,6 +78,8 @@ "Account", "AccountTypeSpec", "Asset", + "ReferenceTypeSpec", + "ReferenceType", "AssetTypeSpec", "Application", "ApplicationTypeSpec", diff --git a/pyteal/ast/abi/address.py b/pyteal/ast/abi/address.py index 145b0f336..d0413cf6c 100644 --- a/pyteal/ast/abi/address.py +++ b/pyteal/ast/abi/address.py @@ -21,7 +21,7 @@ class AddressLength(IntEnum): Bytes = 32 -AddressLength.__module__ = "pyteal" +AddressLength.__module__ = "pyteal.abi" class AddressTypeSpec(StaticArrayTypeSpec): @@ -41,7 +41,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, AddressTypeSpec) -AddressTypeSpec.__module__ = "pyteal" +AddressTypeSpec.__module__ = "pyteal.abi" class Address(StaticArray[Byte, Literal[AddressLength.Bytes]]): @@ -52,20 +52,44 @@ def type_spec(self) -> AddressTypeSpec: return AddressTypeSpec() def get(self) -> Expr: + """Return the 32-byte value held by this Address as a PyTeal expression. + + The expression will have the type TealType.bytes. + """ return self.stored_value.load() def set( self, value: Union[ + str, + bytes, + Expr, Sequence[Byte], StaticArray[Byte, Literal[AddressLength.Bytes]], ComputedValue[StaticArray[Byte, Literal[AddressLength.Bytes]]], "Address", - str, - bytes, - Expr, + ComputedValue["Address"], ], ): + """Set the value of this Address to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`str`: set the value to the address from the encoded address string. This string must be a valid 58-character base-32 Algorand address with checksum. + * :code:`bytes`: set the value to the raw address bytes. This byte string must have length 32. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes. The program will fail if the evaluated byte string length is not 32. + * :code:`Sequence[Byte]`: set the bytes of this Address to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if the sequence length is not 32. + * :code:`StaticArray[Byte, 32]`: copy the bytes from a StaticArray of 32 bytes. + * :code:`ComputedValue[StaticArray[Byte, 32]]`: copy the bytes from a StaticArray of 32 bytes produced by a ComputedValue. + * :code:`Address`: copy the value from another Address. + * :code:`ComputedValue[Address]`: copy the value from an Address produced by a ComputedValue. + + Args: + value: The new value this Address should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Address. + """ match value: case ComputedValue(): @@ -117,4 +141,4 @@ def set( ) -Address.__module__ = "pyteal" +Address.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index 0ec369c5d..61a69c332 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -60,7 +60,7 @@ def _stride(self) -> int: return self.value_spec.byte_length_static() -ArrayTypeSpec.__module__ = "pyteal" +ArrayTypeSpec.__module__ = "pyteal.abi" class Array(BaseType, Generic[T]): @@ -162,15 +162,18 @@ def length(self) -> Expr: pass def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": - """Retrieve an ABI array element by an index (either a PyTeal expression or an integer). + """Retrieve an element by its index in this array. - If the argument index is integer, the function will raise an error if the index is negative. + Indexes start at 0. Args: - index: either an integer or a PyTeal expression that evaluates to a uint64. + index: either a Python integer or a PyTeal expression that evaluates to a TealType.uint64. + If a Python integer is used, this function will raise an error if its value is negative. + In either case, if the index is outside of the bounds of this array, the program will + fail at runtime. Returns: - An ArrayElement that represents the ABI array element at the index. + An ArrayElement that corresponds to the element at the given index. This type is a ComputedValue. """ if type(index) is int: if index < 0: @@ -179,7 +182,7 @@ def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": return ArrayElement(self, cast(Expr, index)) -Array.__module__ = "pyteal" +Array.__module__ = "pyteal.abi" class ArrayElement(ComputedValue[T]): @@ -280,4 +283,4 @@ def store_into(self, output: T) -> Expr: return output.decode(encodedArray, start_index=valueStart, length=valueLength) -ArrayElement.__module__ = "pyteal" +ArrayElement.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_dynamic.py b/pyteal/ast/abi/array_dynamic.py index 33c8e8309..e5aec04af 100644 --- a/pyteal/ast/abi/array_dynamic.py +++ b/pyteal/ast/abi/array_dynamic.py @@ -44,7 +44,7 @@ def __str__(self) -> str: return f"{self.value_type_spec()}[]" -DynamicArrayTypeSpec.__module__ = "pyteal" +DynamicArrayTypeSpec.__module__ = "pyteal.abi" class DynamicArray(Array[T]): @@ -60,26 +60,20 @@ def set( self, values: Union[Sequence[T], "DynamicArray[T]", ComputedValue["DynamicArray[T]"]], ) -> Expr: - """Set the ABI dynamic array with one of the following - * a sequence of ABI type variables - * or another ABI static array - * or a ComputedType with same TypeSpec + """ + Set the elements of this DynamicArray to the input values. - If the argument `values` is a ComputedType, we call `store_into` method - from ComputedType to store the internal ABI encoding into this StaticArray. + The behavior of this method depends on the input argument type: - This function determines if the argument `values` is an ABI dynamic array: - * if so: - * checks whether `values` is same type as this ABI dynamic array. - * stores the encoding of `values`. - * if not: - * calls the inherited `set` function and stores `values`. + * :code:`Sequence[T]`: set the elements of this DynamicArray to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if any element in the sequence does not match this DynamicArray's element type. + * :code:`DynamicArray[T]`: copy the elements from another DynamicArray. The argument's element type must exactly match this DynamicArray's element type, otherwise an error will occur. + * :code:`ComputedValue[DynamicArray[T]]`: copy the elements from a DynamicArray produced by a ComputedValue. The element type produced by the ComputedValue must exactly match this DynamicArray's element type, otherwise an error will occur. Args: - values: either a sequence of ABI typed values, or an ABI dynamic array. + values: The new elements this DynamicArray should have. This must follow the above constraints. Returns: - A PyTeal expression that stores encoded `values` in its internal ScratchVar. + An expression which stores the given value into this DynamicArray. """ if isinstance(values, ComputedValue): @@ -107,4 +101,4 @@ def length(self) -> Expr: ) -DynamicArray.__module__ = "pyteal" +DynamicArray.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py index b829237b7..6829106c2 100644 --- a/pyteal/ast/abi/array_static.py +++ b/pyteal/ast/abi/array_static.py @@ -66,7 +66,7 @@ def __str__(self) -> str: return f"{self.value_type_spec()}[{self.length_static()}]" -StaticArrayTypeSpec.__module__ = "pyteal" +StaticArrayTypeSpec.__module__ = "pyteal.abi" class StaticArray(Array[T], Generic[T, N]): @@ -84,27 +84,19 @@ def set( Sequence[T], "StaticArray[T, N]", ComputedValue["StaticArray[T, N]"] ], ) -> Expr: - """Set the ABI static array with one of the following: - * a sequence of ABI type variables - * or another ABI static array - * or a ComputedType with same TypeSpec - - If the argument `values` is a ComputedType, we call `store_into` method - from ComputedType to store the internal ABI encoding into this StaticArray. - - This function determines if the argument `values` is an ABI static array: - * if so: - * checks whether `values` is same type as this ABI staic array. - * stores the encoding of `values`. - * if not: - * checks whether static array length matches sequence length. - * calls the inherited `set` function and stores `values`. + """Set the elements of this StaticArray to the input values. + + The behavior of this method depends on the input argument type: + + * :code:`Sequence[T]`: set the elements of this StaticArray to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if any element in the sequence does not match this StaticArray's element type, or if the sequence length does not equal this StaticArray's length. + * :code:`StaticArray[T, N]`: copy the elements from another StaticArray. The argument's element type and length must exactly match this StaticArray's element type and length, otherwise an error will occur. + * :code:`ComputedValue[StaticArray[T, N]]`: copy the elements from a StaticArray produced by a ComputedValue. The element type and length produced by the ComputedValue must exactly match this StaticArray's element type and length, otherwise an error will occur. Args: - values: either a sequence of ABI typed values, or an ABI static array. + values: The new elements this StaticArray should have. This must follow the above constraints. Returns: - A PyTeal expression that stores encoded `values` in its internal ScratchVar. + An expression which stores the given value into this StaticArray. """ if isinstance(values, ComputedValue): return self._set_with_computed_type(values) @@ -130,9 +122,23 @@ def length(self) -> Expr: return Int(self.type_spec().length_static()) def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": + """Retrieve an element by its index in this StaticArray. + + Indexes start at 0. + + Args: + index: either a Python integer or a PyTeal expression that evaluates to a TealType.uint64. + If a Python integer is used, this function will raise an error if its value is negative + or if the index is equal to or greater than the length of this StaticArray. If a PyTeal + expression is used, the program will fail at runtime if the index is outside of the + bounds of this StaticArray. + + Returns: + An ArrayElement that corresponds to the element at the given index. This type is a ComputedValue. + """ if type(index) is int and index >= self.type_spec().length_static(): raise TealInputError(f"Index out of bounds: {index}") return super().__getitem__(index) -StaticArray.__module__ = "pyteal" +StaticArray.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index bf664b0a0..75a41a8d6 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Union, cast, Sequence, Callable +from typing import TypeVar, Union, Sequence, Callable from pyteal.types import TealType from pyteal.errors import TealInputError @@ -37,7 +37,7 @@ def __str__(self) -> str: return "bool" -BoolTypeSpec.__module__ = "pyteal" +BoolTypeSpec.__module__ = "pyteal.abi" class Bool(BaseType): @@ -45,14 +45,33 @@ def __init__(self) -> None: super().__init__(BoolTypeSpec()) def get(self) -> Expr: + """Return the value held by this Bool as a PyTeal expression. + + If the held value is true, an expression that evaluates to 1 will be returned. Otherwise, an + expression that evaluates to 0 will be returned. In either case, the expression will have the + type TealType.uint64. + """ return self.stored_value.load() def set(self, value: Union[bool, Expr, "Bool", ComputedValue["Bool"]]) -> Expr: + """Set the value of this Bool to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`bool`: set the value to a Python boolean value. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.uint64. The program will fail if the evaluated value is not 0 or 1. + * :code:`Bool`: copy the value from another Bool. + * :code:`ComputedValue[Bool]`: copy the value from a Bool produced by a ComputedValue. + + Args: + value: The new value this Bool should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Bool. + """ if isinstance(value, ComputedValue): return self._set_with_computed_type(value) - value = cast(Union[bool, Expr, "Bool"], value) - checked = False if type(value) is bool: value = Int(1 if value else 0) @@ -71,6 +90,7 @@ def set(self, value: Union[bool, Expr, "Bool", ComputedValue["Bool"]]) -> Expr: return Seq( self.stored_value.store(value), + # instead of failing if too high of a value is given, it's probably more consistent with the rest of the AVM to convert values >= 2 to 1 (the && and || opcodes do this) Assert(self.stored_value.load() < Int(2)), ) @@ -93,7 +113,7 @@ def encode(self) -> Expr: return SetBit(Bytes(b"\x00"), Int(0), self.get()) -Bool.__module__ = "pyteal" +Bool.__module__ = "pyteal.abi" def boolAwareStaticByteLength(types: Sequence[TypeSpec]) -> int: diff --git a/pyteal/ast/abi/method_return.py b/pyteal/ast/abi/method_return.py index 68c2d0962..24566b5bd 100644 --- a/pyteal/ast/abi/method_return.py +++ b/pyteal/ast/abi/method_return.py @@ -39,4 +39,4 @@ def has_return(self) -> bool: return False -MethodReturn.__module__ = "pyteal" +MethodReturn.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/reference_type.py b/pyteal/ast/abi/reference_type.py index 2fc7cda27..8ee17ac2f 100644 --- a/pyteal/ast/abi/reference_type.py +++ b/pyteal/ast/abi/reference_type.py @@ -38,6 +38,9 @@ def storage_type(self) -> TealType: return TealType.uint64 +ReferenceTypeSpec.__module__ = "pyteal.abi" + + class ReferenceType(BaseType): @abstractmethod def __init__(self, spec: ReferenceTypeSpec) -> None: @@ -78,6 +81,9 @@ def encode(self) -> Expr: raise TealInputError("A ReferenceType cannot be encoded") +ReferenceType.__module__ = "pyteal.abi" + + class AccountTypeSpec(ReferenceTypeSpec): def new_instance(self) -> "Account": return Account() @@ -92,7 +98,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, AccountTypeSpec) -AccountTypeSpec.__module__ = "pyteal" +AccountTypeSpec.__module__ = "pyteal.abi" class Account(ReferenceType): @@ -124,7 +130,7 @@ def asset_holding(self, asset: "Expr | Asset") -> AssetHoldingObject: return AssetHoldingObject(asset_ref, self.referenced_index()) -Account.__module__ = "pyteal" +Account.__module__ = "pyteal.abi" class AssetTypeSpec(ReferenceTypeSpec): @@ -141,7 +147,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, AssetTypeSpec) -AssetTypeSpec.__module__ = "pyteal" +AssetTypeSpec.__module__ = "pyteal.abi" class Asset(ReferenceType): @@ -173,7 +179,7 @@ def params(self) -> AssetParamObject: return AssetParamObject(self.referenced_index()) -Asset.__module__ = "pyteal" +Asset.__module__ = "pyteal.abi" class ApplicationTypeSpec(ReferenceTypeSpec): @@ -190,7 +196,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, ApplicationTypeSpec) -ApplicationTypeSpec.__module__ = "pyteal" +ApplicationTypeSpec.__module__ = "pyteal.abi" class Application(ReferenceType): @@ -206,7 +212,7 @@ def params(self) -> AppParamObject: return AppParamObject(self.referenced_index()) -Application.__module__ = "pyteal" +Application.__module__ = "pyteal.abi" ReferenceTypeSpecs: Final[List[TypeSpec]] = [ diff --git a/pyteal/ast/abi/string.py b/pyteal/ast/abi/string.py index 92b566d95..c4deb77ed 100644 --- a/pyteal/ast/abi/string.py +++ b/pyteal/ast/abi/string.py @@ -37,7 +37,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, StringTypeSpec) -StringTypeSpec.__module__ = "pyteal" +StringTypeSpec.__module__ = "pyteal.abi" class String(DynamicArray[Byte]): @@ -48,6 +48,10 @@ def type_spec(self) -> StringTypeSpec: return StringTypeSpec() def get(self) -> Expr: + """Return the value held by this String as a PyTeal expression. + + The expression will have the type TealType.bytes. + """ return Suffix( self.stored_value.load(), Int(Uint16TypeSpec().byte_length_static()) ) @@ -55,15 +59,35 @@ def get(self) -> Expr: def set( self, value: Union[ + str, + bytes, + Expr, Sequence[Byte], DynamicArray[Byte], ComputedValue[DynamicArray[Byte]], "String", - str, - bytes, - Expr, + ComputedValue["String"], ], ) -> Expr: + """Set the value of this String to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`str`: set the value to the Python string. + * :code:`bytes`: set the value to the Python byte string. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes. + * :code:`Sequence[Byte]`: set the bytes of this String to those contained in this Python sequence (e.g. a list or tuple). + * :code:`DynamicArray[Byte]`: copy the bytes from a DynamicArray. + * :code:`ComputedValue[DynamicArray[Byte]]`: copy the bytes from a DynamicArray produced by a ComputedValue. + * :code:`String`: copy the value from another String. + * :code:`ComputedValue[String]`: copy the value from a String produced by a ComputedValue. + + Args: + value: The new value this String should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this String. + """ match value: case ComputedValue(): @@ -89,4 +113,4 @@ def set( ) -String.__module__ = "pyteal" +String.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/transaction.py b/pyteal/ast/abi/transaction.py index 1f4b166a1..c392f8b64 100644 --- a/pyteal/ast/abi/transaction.py +++ b/pyteal/ast/abi/transaction.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import TypeVar, Union, cast, List, Final +from typing import Union, cast, List, Final from pyteal.ast.abi.type import BaseType, ComputedValue, TypeSpec from pyteal.ast.expr import Expr from pyteal.ast.int import Int @@ -8,8 +8,6 @@ from pyteal.types import TealType from pyteal.errors import TealInputError, TealInternalError -T = TypeVar("T", bound=BaseType) - class TransactionType(Enum): Any = "txn" @@ -21,7 +19,7 @@ class TransactionType(Enum): ApplicationCall = "appl" -TransactionType.__module__ = "pyteal" +TransactionType.__module__ = "pyteal.abi" class TransactionTypeSpec(TypeSpec): @@ -61,7 +59,7 @@ def __str__(self) -> str: return TransactionType.Any.value -TransactionTypeSpec.__module__ = "pyteal" +TransactionTypeSpec.__module__ = "pyteal.abi" class Transaction(BaseType): @@ -78,7 +76,7 @@ def get(self) -> TxnObject: return Gtxn[self.index()] def _set_index( - self: T, value: Union[int, Expr, "Transaction", ComputedValue[T]] + self, value: Union[int, Expr, "Transaction", ComputedValue["Transaction"]] ) -> Expr: match value: case ComputedValue(): @@ -109,7 +107,7 @@ def encode(self) -> Expr: raise TealInputError("A Transaction cannot be encoded") -Transaction.__module__ = "pyteal" +Transaction.__module__ = "pyteal.abi" class PaymentTransactionTypeSpec(TransactionTypeSpec): @@ -126,7 +124,7 @@ def __str__(self) -> str: return TransactionType.Payment.value -PaymentTransactionTypeSpec.__module__ = "pyteal" +PaymentTransactionTypeSpec.__module__ = "pyteal.abi" class PaymentTransaction(Transaction): @@ -134,7 +132,7 @@ def __init__(self): super().__init__(PaymentTransactionTypeSpec()) -PaymentTransaction.__module__ = "pyteal" +PaymentTransaction.__module__ = "pyteal.abi" class KeyRegisterTransactionTypeSpec(TransactionTypeSpec): @@ -151,7 +149,7 @@ def __str__(self) -> str: return TransactionType.KeyRegistration.value -KeyRegisterTransactionTypeSpec.__module__ = "pyteal" +KeyRegisterTransactionTypeSpec.__module__ = "pyteal.abi" class KeyRegisterTransaction(Transaction): @@ -159,7 +157,7 @@ def __init__(self): super().__init__(KeyRegisterTransactionTypeSpec()) -KeyRegisterTransaction.__module__ = "pyteal" +KeyRegisterTransaction.__module__ = "pyteal.abi" class AssetConfigTransactionTypeSpec(TransactionTypeSpec): @@ -176,7 +174,7 @@ def __str__(self) -> str: return TransactionType.AssetConfig.value -AssetConfigTransactionTypeSpec.__module__ = "pyteal" +AssetConfigTransactionTypeSpec.__module__ = "pyteal.abi" class AssetConfigTransaction(Transaction): @@ -184,7 +182,7 @@ def __init__(self): super().__init__(AssetConfigTransactionTypeSpec()) -AssetConfigTransaction.__module__ = "pyteal" +AssetConfigTransaction.__module__ = "pyteal.abi" class AssetFreezeTransactionTypeSpec(TransactionTypeSpec): @@ -201,7 +199,7 @@ def __str__(self) -> str: return TransactionType.AssetFreeze.value -AssetFreezeTransactionTypeSpec.__module__ = "pyteal" +AssetFreezeTransactionTypeSpec.__module__ = "pyteal.abi" class AssetFreezeTransaction(Transaction): @@ -209,7 +207,7 @@ def __init__(self): super().__init__(AssetFreezeTransactionTypeSpec()) -AssetFreezeTransaction.__module__ = "pyteal" +AssetFreezeTransaction.__module__ = "pyteal.abi" class AssetTransferTransactionTypeSpec(TransactionTypeSpec): @@ -226,7 +224,7 @@ def __str__(self) -> str: return TransactionType.AssetTransfer.value -AssetTransferTransactionTypeSpec.__module__ = "pyteal" +AssetTransferTransactionTypeSpec.__module__ = "pyteal.abi" class AssetTransferTransaction(Transaction): @@ -234,7 +232,7 @@ def __init__(self): super().__init__(AssetTransferTransactionTypeSpec()) -AssetTransferTransaction.__module__ = "pyteal" +AssetTransferTransaction.__module__ = "pyteal.abi" class ApplicationCallTransactionTypeSpec(TransactionTypeSpec): @@ -251,7 +249,7 @@ def __str__(self) -> str: return TransactionType.ApplicationCall.value -ApplicationCallTransactionTypeSpec.__module__ = "pyteal" +ApplicationCallTransactionTypeSpec.__module__ = "pyteal.abi" class ApplicationCallTransaction(Transaction): @@ -259,7 +257,7 @@ def __init__(self): super().__init__(ApplicationCallTransactionTypeSpec()) -ApplicationCallTransaction.__module__ = "pyteal" +ApplicationCallTransaction.__module__ = "pyteal.abi" TransactionTypeSpecs: Final[List[TypeSpec]] = [ TransactionTypeSpec(), diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index e7d8f2b8e..1bb28d8d1 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -6,6 +6,7 @@ TypeVar, cast, overload, + Any, ) from pyteal.types import TealType @@ -268,9 +269,7 @@ def __str__(self) -> str: return "({})".format(",".join(map(str, self.value_type_specs()))) -TupleTypeSpec.__module__ = "pyteal" - -T_tuple = TypeVar("T_tuple", bound="Tuple") +TupleTypeSpec.__module__ = "pyteal.abi" class Tuple(BaseType): @@ -295,13 +294,31 @@ def decode( @overload def set(self, *values: BaseType) -> Expr: - ... + pass @overload - def set(self: T_tuple, value: ComputedValue[T_tuple]) -> Expr: - ... + def set(self, values: ComputedValue["Tuple"]) -> Expr: + # TODO: should support values as a Tuple as well + pass def set(self, *values): + """ + set(*values: BaseType) -> Expr + set(values: ComputedValue[Tuple]) -> Expr + + Set the elements of this Tuple to the input values. + + The behavior of this method depends on the input argument type: + + * Variable number of :code:`BaseType` arguments: set the elements of this Tuple to the arguments to this method. A compiler error will occur if any argument does not match this Tuple's element type at the same index, or if the total argument count does not equal this Tuple's length. + * :code:`ComputedValue[Tuple]`: copy the elements from a Tuple produced by a ComputedValue. The element types and length produced by the ComputedValue must exactly match this Tuple's element types and length, otherwise an error will occur. + + Args: + values: The new elements this Tuple should have. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Tuple. + """ if len(values) == 1 and isinstance(values[0], ComputedValue): return self._set_with_computed_type(values[0]) @@ -325,13 +342,27 @@ def length(self) -> Expr: """Get the number of values this tuple holds as an Expr.""" return Int(self.type_spec().length_static()) - def __getitem__(self, index: int) -> "TupleElement": + def __getitem__(self, index: int) -> "TupleElement[Any]": + """Retrieve an element by its index in this Tuple. + + Indexes start at 0. + + Args: + index: a Python integer containing the index to access. This function will raise an error + if its value is negative or if the index is equal to or greater than the length of + this Tuple. + + Returns: + A TupleElement that corresponds to the element at the given index. This type is a + ComputedValue. Due to Python type limitations, the parameterized type of the + TupleElement is Any. + """ if not (0 <= index < self.type_spec().length_static()): raise TealInputError(f"Index out of bounds: {index}") return TupleElement(self, index) -Tuple.__module__ = "pyteal" +Tuple.__module__ = "pyteal.abi" T = TypeVar("T", bound=BaseType) @@ -356,7 +387,7 @@ def store_into(self, output: T) -> Expr: ) -TupleElement.__module__ = "pyteal" +TupleElement.__module__ = "pyteal.abi" # Until Python 3.11 is released with support for PEP 646 -- Variadic Generics, it's not possible for # the Tuple class to take an arbitrary number of template parameters. As a workaround, we define the @@ -378,7 +409,7 @@ def __init__(self) -> None: super().__init__(TupleTypeSpec()) -Tuple0.__module__ = "pyteal" +Tuple0.__module__ = "pyteal.abi" T1 = TypeVar("T1", bound=BaseType) @@ -391,7 +422,7 @@ def __init__(self, value_type_spec: TupleTypeSpec) -> None: super().__init__(value_type_spec) -Tuple1.__module__ = "pyteal" +Tuple1.__module__ = "pyteal.abi" T2 = TypeVar("T2", bound=BaseType) @@ -404,7 +435,7 @@ def __init__(self, value_type_spec: TupleTypeSpec) -> None: super().__init__(value_type_spec) -Tuple2.__module__ = "pyteal" +Tuple2.__module__ = "pyteal.abi" T3 = TypeVar("T3", bound=BaseType) @@ -420,7 +451,7 @@ def __init__( super().__init__(value_type_spec) -Tuple3.__module__ = "pyteal" +Tuple3.__module__ = "pyteal.abi" T4 = TypeVar("T4", bound=BaseType) @@ -436,7 +467,7 @@ def __init__( super().__init__(value_type_spec) -Tuple4.__module__ = "pyteal" +Tuple4.__module__ = "pyteal.abi" T5 = TypeVar("T5", bound=BaseType) @@ -452,4 +483,4 @@ def __init__( super().__init__(value_type_spec) -Tuple5.__module__ = "pyteal" +Tuple5.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index 5b1547308..4b0b1e0f3 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -62,7 +62,7 @@ def __str__(self) -> str: pass -TypeSpec.__module__ = "pyteal" +TypeSpec.__module__ = "pyteal.abi" class BaseType(ABC): @@ -134,7 +134,7 @@ def decode( """ pass - def _set_with_computed_type(self, value: "ComputedValue") -> Expr: + def _set_with_computed_type(self, value: "ComputedValue[BaseType]") -> Expr: target_type_spec = value.produced_type_spec() if self.type_spec() != target_type_spec: raise TealInputError( @@ -146,12 +146,12 @@ def __str__(self) -> str: return str(self.type_spec()) -BaseType.__module__ = "pyteal" +BaseType.__module__ = "pyteal.abi" -T = TypeVar("T", bound=BaseType) +T_co = TypeVar("T_co", bound=BaseType, covariant=True) -class ComputedValue(ABC, Generic[T]): +class ComputedValue(ABC, Generic[T_co]): """Represents an ABI Type whose value must be computed by an expression.""" @abstractmethod @@ -160,8 +160,12 @@ def produced_type_spec(self) -> TypeSpec: pass @abstractmethod - def store_into(self, output: T) -> Expr: - """Store the value of this computed type into an existing ABI type instance. + def store_into(self, output: T_co) -> Expr: # type: ignore[misc] + """Compute the value and store it into an existing ABI type instance. + + NOTE: If you call this method multiple times, the computation to determine the value will be + repeated each time. For this reason, it is recommended to only issue a single call to either + :code:`store_into` or :code:`use`. Args: output: The object where the computed value will be stored. This object must have the @@ -173,8 +177,12 @@ def store_into(self, output: T) -> Expr: """ pass - def use(self, action: Callable[[T], Expr]) -> Expr: - """Use the computed value represented by this class in a function or lambda expression. + def use(self, action: Callable[[T_co], Expr]) -> Expr: + """Compute the value and pass it to a callable expression. + + NOTE: If you call this method multiple times, the computation to determine the value will be + repeated each time. For this reason, it is recommended to only issue a single call to either + :code:`store_into` or :code:`use`. Args: action: A callable object that will receive an instance of this class's produced type @@ -182,14 +190,14 @@ def use(self, action: Callable[[T], Expr]) -> Expr: it must return an Expr to be included in the program's AST. Returns: - An expression which contains the returned expression from invoking action with the + An expression which contains the returned expression from invoking `action` with the computed value. """ - newInstance = cast(T, self.produced_type_spec().new_instance()) + newInstance = cast(T_co, self.produced_type_spec().new_instance()) return Seq(self.store_into(newInstance), action(newInstance)) -ComputedValue.__module__ = "pyteal" +ComputedValue.__module__ = "pyteal.abi" class ReturnedValue(ComputedValue): @@ -226,4 +234,4 @@ def store_into(self, output: BaseType) -> Expr: return output.stored_value.slot.store(self.computation) -ReturnedValue.__module__ = "pyteal" +ReturnedValue.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py index 885cb4c4c..7dddcba2e 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -1,5 +1,4 @@ from typing import ( - TypeVar, Union, Optional, Final, @@ -113,9 +112,6 @@ def uint_encode(size: int, uintVar: Expr | ScratchVar) -> Expr: raise ValueError("Unsupported uint size: {}".format(size)) -N = TypeVar("N", bound=int) - - class UintTypeSpec(TypeSpec): def __init__(self, bit_size: int) -> None: super().__init__() @@ -155,7 +151,7 @@ def __str__(self) -> str: return "uint{}".format(self.bit_size()) -UintTypeSpec.__module__ = "pyteal" +UintTypeSpec.__module__ = "pyteal.abi" class ByteTypeSpec(UintTypeSpec): @@ -172,7 +168,7 @@ def __str__(self) -> str: return "byte" -ByteTypeSpec.__module__ = "pyteal" +ByteTypeSpec.__module__ = "pyteal.abi" class Uint8TypeSpec(UintTypeSpec): @@ -186,7 +182,7 @@ def annotation_type(self) -> "type[Uint8]": return Uint8 -Uint8TypeSpec.__module__ = "pyteal" +Uint8TypeSpec.__module__ = "pyteal.abi" class Uint16TypeSpec(UintTypeSpec): @@ -200,7 +196,7 @@ def annotation_type(self) -> "type[Uint16]": return Uint16 -Uint16TypeSpec.__module__ = "pyteal" +Uint16TypeSpec.__module__ = "pyteal.abi" class Uint32TypeSpec(UintTypeSpec): @@ -214,7 +210,7 @@ def annotation_type(self) -> "type[Uint32]": return Uint32 -Uint32TypeSpec.__module__ = "pyteal" +Uint32TypeSpec.__module__ = "pyteal.abi" class Uint64TypeSpec(UintTypeSpec): @@ -228,10 +224,7 @@ def annotation_type(self) -> "type[Uint64]": return Uint64 -Uint32TypeSpec.__module__ = "pyteal" - - -T = TypeVar("T", bound="Uint") +Uint32TypeSpec.__module__ = "pyteal.abi" class Uint(BaseType): @@ -243,9 +236,32 @@ def type_spec(self) -> UintTypeSpec: return cast(UintTypeSpec, super().type_spec()) def get(self) -> Expr: + """Return the value held by this Uint as a PyTeal expression. + + The expression will have the type TealType.uint64. + """ return self.stored_value.load() - def set(self: T, value: Union[int, Expr, "Uint", ComputedValue[T]]) -> Expr: + def set(self, value: Union[int, Expr, "Uint", ComputedValue["Uint"]]) -> Expr: + """Set the value of this Uint to the input value. + + There are a variety of ways to express the input value. Regardless of the type used to + indicate the input value, this Uint type can only hold values in the range :code:`[0,2^N)`, + where :code:`N` is the bit size of this Uint. + + The behavior of this method depends on the input argument type: + + * :code:`int`: set the value to a Python integer. A compiler error will occur if this value overflows or underflows this integer type. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.uint64. The program will fail if the evaluated value overflows or underflows this integer type. + * :code:`Uint`: copy the value from another Uint. The argument's type must exactly match this integer's type, otherwise an error will occur. For example, it's not possible to set a Uint64 to a Uint8, or vice versa. + * :code:`ComputedValue[Uint]`: copy the value from a Uint produced by a ComputedValue. The type produced by the ComputedValue must exactly match this integer's type, otherwise an error will occur. + + Args: + value: The new value this Uint should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Uint. + """ if isinstance(value, ComputedValue): return self._set_with_computed_type(value) @@ -282,7 +298,7 @@ def encode(self) -> Expr: return uint_encode(self.type_spec().bit_size(), self.stored_value) -Uint.__module__ = "pyteal" +Uint.__module__ = "pyteal.abi" class Byte(Uint): @@ -290,7 +306,7 @@ def __init__(self) -> None: super().__init__(ByteTypeSpec()) -Byte.__module__ = "pyteal" +Byte.__module__ = "pyteal.abi" class Uint8(Uint): @@ -298,7 +314,7 @@ def __init__(self) -> None: super().__init__(Uint8TypeSpec()) -Uint8.__module__ = "pyteal" +Uint8.__module__ = "pyteal.abi" class Uint16(Uint): @@ -306,7 +322,7 @@ def __init__(self) -> None: super().__init__(Uint16TypeSpec()) -Uint16.__module__ = "pyteal" +Uint16.__module__ = "pyteal.abi" class Uint32(Uint): @@ -314,7 +330,7 @@ def __init__(self) -> None: super().__init__(Uint32TypeSpec()) -Uint32.__module__ = "pyteal" +Uint32.__module__ = "pyteal.abi" class Uint64(Uint): @@ -322,4 +338,4 @@ def __init__(self) -> None: super().__init__(Uint64TypeSpec()) -Uint64.__module__ = "pyteal" +Uint64.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/util_test.py b/pyteal/ast/abi/util_test.py index c3f755be4..2e791cee3 100644 --- a/pyteal/ast/abi/util_test.py +++ b/pyteal/ast/abi/util_test.py @@ -497,61 +497,61 @@ def test_size_of(): # ), # ), ( - "cannot map ABI transaction type spec None: - ... + pass @overload def __init__(self, arg1: str, arg2: str) -> None: - ... + pass def __init__(self, arg1: Union[str, bytes, bytearray], arg2: str = None) -> None: - """Create a new byte string. + """ + __init__(arg1: Union[str, bytes, bytearray]) -> None + __init__(self, arg1: str, arg2: str) -> None + + Create a new byte string. Depending on the encoding, there are different arguments to pass: diff --git a/pyteal/ast/pragma.py b/pyteal/ast/pragma.py index 521c35173..0fd5acead 100644 --- a/pyteal/ast/pragma.py +++ b/pyteal/ast/pragma.py @@ -23,6 +23,19 @@ def __init__(self, child: Expr, *, compiler_version: str, **kwargs: Any) -> None compiler_version: Acceptable versions of the compiler. Will fail if the current PyTeal version is not contained in the range. Follows the npm `semver range scheme `_ for specifying compatible versions. + + For example: + + .. code-block:: python + + @Subroutine(TealType.uint64) + def example() -> Expr: + # this will fail during compilation if the current PyTeal version does not satisfy + # the version constraint + return Pragma( + Seq(...), + compiler_version="^0.14.0" + ) """ super().__init__() diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py index f5a39925c..ff7ed1f0f 100644 --- a/pyteal/ast/router.py +++ b/pyteal/ast/router.py @@ -491,14 +491,18 @@ def program_construction(self) -> Expr: class Router: """ - Class that help constructs: - - a *Generalized* ARC-4 app's approval/clear-state programs - - and a contract JSON object allowing for easily read and call methods in the contract + The Router class helps construct the approval and clear state programs for an ARC-4 compliant + application. - *DISCLAIMER*: ABI-Router is still taking shape and is subject to backwards incompatible changes. + Additionally, this class can produce an ARC-4 contract description object for the application. + + **WARNING:** The ABI Router is still taking shape and is subject to backwards incompatible changes. * Based on feedback, the API and usage patterns are likely to change. - * Expect migration issues. + * Expect migration issues in future PyTeal versions. + + For these reasons, we strongly recommend using :any:`pragma` to pin the version of PyTeal in your + source code. """ def __init__( @@ -549,6 +553,19 @@ def add_method_handler( method_config: MethodConfig = None, description: str = None, ) -> ABIReturnSubroutine: + """Add a method call handler to this Router. + + Args: + method_call: An ABIReturnSubroutine that implements the method body. + overriding_name (optional): A name for this method. Defaults to the function name of + method_call. + method_config (optional): An object describing the on completion actions and + creation/non-creation call statuses that are valid for calling this method. All + invalid configurations will be rejected. Defaults to :code:`MethodConfig(no_op=CallConfig.CALL)` + (i.e. only the no-op action during a non-creation call is accepted) if none is provided. + description (optional): A description for this method. Defaults to the docstring of + method_call, if there is one. + """ if not isinstance(method_call, ABIReturnSubroutine): raise TealInputError( "for adding method handler, must be ABIReturnSubroutine" @@ -594,26 +611,37 @@ def method( /, *, name: str = None, + description: str = None, no_op: CallConfig = None, opt_in: CallConfig = None, close_out: CallConfig = None, clear_state: CallConfig = None, update_application: CallConfig = None, delete_application: CallConfig = None, - description: str = None, ): - """ - A decorator style method registration by decorating over a python function, - which is internally converted to ABIReturnSubroutine, and taking keyword arguments - for each OnCompletes' `CallConfig`. - - NOTE: - By default, all OnCompletes other than `NoOp` are set to `CallConfig.NEVER`, - while `no_op` field is always `CALL`. - If one wants to change `no_op`, we need to change `no_op = CallConfig.ALL`, - for example, as a decorator argument. - """ + """This is an alternative way to register a method, as supposed to :code:`add_method_handler`. + + This is a decorator that's meant to be used over a Python function, which is internally + wrapped with ABIReturnSubroutine. Additional keyword arguments on this decorator can be used + to specify the OnCompletion statuses that are valid for the registered method. + NOTE: By default, all OnCompletion actions other than `no_op` are set to `CallConfig.NEVER`, + while `no_op` field is set to `CallConfig.CALL`. However, if you provide any keywords for + OnCompletion actions, then the `no_op` field will default to `CallConfig.NEVER`. + + Args: + func: A function that implements the method body. This should *NOT* be wrapped with the + :code:`ABIReturnSubroutine` decorator yet. + name (optional): A name for this method. Defaults to the function name of func. + description (optional): A description for this method. Defaults to the docstring of + func, if there is one. + no_op (optional): The allowed calls during :code:`OnComplete.NoOp`. + opt_in (optional): The allowed calls during :code:`OnComplete.OptIn`. + close_out (optional): The allowed calls during :code:`OnComplete.CloseOut`. + clear_state (optional): The allowed calls during :code:`OnComplete.ClearState`. + update_application (optional): The allowed calls during :code:`OnComplete.UpdateApplication`. + delete_application (optional): The allowed calls during :code:`OnComplete.DeleteApplication`. + """ # we use `is None` extensively for CallConfig to distinguish 2 following cases # - None # - CallConfig.Never @@ -659,30 +687,32 @@ def none_to_never(x: None | CallConfig): return wrap(func) def contract_construct(self) -> sdk_abi.Contract: - """A helper function in constructing contract JSON object. + """A helper function in constructing a `Contract` object. It takes out the method spec from approval program methods, and constructs an `Contract` object. Returns: - contract: a dictified `Contract` object constructed from - approval program's method specs, `self.name`, and `self.descr`. + A Python SDK `Contract` object constructed from the registered methods on this router. """ return sdk_abi.Contract(self.name, self.methods, self.descr) def build_program(self) -> tuple[Expr, Expr, sdk_abi.Contract]: """ - Constructs ASTs for approval and clear-state programs from the registered methods in the router, - also generates a JSON object of contract to allow client read and call the methods easily. + Constructs ASTs for approval and clear-state programs from the registered methods and bare + app calls in the router, and also generates a Contract object to allow client read and call + the methods easily. Note that if no methods or bare app call actions have been registered to either the approval or clear state programs, then that program will reject all transactions. Returns: - approval_program: AST for approval program - clear_state_program: AST for clear-state program - contract: JSON object of contract to allow client start off-chain call + A tuple of three objects. + + * approval_program: an AST for approval program + * clear_state_program: an AST for clear-state program + * contract: a Python SDK Contract object to allow clients to make off-chain calls """ return ( self.approval_ast.program_construction(), @@ -698,16 +728,21 @@ def compile_program( optimize: OptimizeOptions = None, ) -> tuple[str, str, sdk_abi.Contract]: """ - Combining `build_program` and `compileTeal`, compiles built Approval and ClearState programs - and returns Contract JSON object for off-chain calling. + Constructs and compiles approval and clear-state programs from the registered methods and + bare app calls in the router, and also generates a Contract object to allow client read and call + the methods easily. + + This method combines :any:`Router.build_program` and :any:`compileTeal`. Note that if no methods or bare app call actions have been registered to either the approval or clear state programs, then that program will reject all transactions. Returns: - approval_program: compiled approval program - clear_state_program: compiled clear-state program - contract: JSON object of contract to allow client start off-chain call + A tuple of three objects. + + * approval_program: compiled approval program string + * clear_state_program: compiled clear-state program string + * contract: a Python SDK Contract object to allow clients to make off-chain calls """ ap, csp, contract = self.build_program() ap_compiled = compileTeal( diff --git a/pyteal/ast/seq.py b/pyteal/ast/seq.py index 4185cd5cc..39901cbd2 100644 --- a/pyteal/ast/seq.py +++ b/pyteal/ast/seq.py @@ -13,15 +13,19 @@ class Seq(Expr): """A control flow expression to represent a sequence of expressions.""" @overload - def __init__(self, *exprs: Expr): - ... + def __init__(self, *exprs: Expr) -> None: + pass @overload - def __init__(self, exprs: List[Expr]): - ... + def __init__(self, exprs: List[Expr]) -> None: + pass def __init__(self, *exprs): - """Create a new Seq expression. + """ + __init__(*exprs: Expr) -> None + __init__(exprs: List[Expr]) -> None + + Create a new Seq expression. The new Seq expression will take on the return value of the final expression in the sequence. diff --git a/pyteal/ast/subroutine_test.py b/pyteal/ast/subroutine_test.py index 2e9df0ac2..2c90c70a1 100644 --- a/pyteal/ast/subroutine_test.py +++ b/pyteal/ast/subroutine_test.py @@ -1023,15 +1023,15 @@ def fnWithMixedAnnsABIRet2( ), ( fnWithMixedAnnsABIRet1, - "Function has return of disallowed type pyteal.StaticArray[pyteal.Uint32, typing.Literal[10]]. " + "Function has return of disallowed type pyteal.abi.StaticArray[pyteal.abi.Uint32, typing.Literal[10]]. " "Only Expr is allowed", - "Function has return of disallowed type pyteal.StaticArray[pyteal.Uint32, typing.Literal[10]]. " + "Function has return of disallowed type pyteal.abi.StaticArray[pyteal.abi.Uint32, typing.Literal[10]]. " "Only Expr is allowed", ), ( fnWithMixedAnnsABIRet2, - "Function has return of disallowed type . Only Expr is allowed", - "Function has return of disallowed type . Only Expr is allowed", + "Function has return of disallowed type . Only Expr is allowed", + "Function has return of disallowed type . Only Expr is allowed", ), ) diff --git a/pyteal/ir/tealblock.py b/pyteal/ir/tealblock.py index 5fd2d5a42..7ce6259bc 100644 --- a/pyteal/ir/tealblock.py +++ b/pyteal/ir/tealblock.py @@ -255,8 +255,10 @@ def MatchScratchSlotReferences( A mapping is defined as follows: * The actual and expected lists must have the same length. * For every ScratchSlot referenced by either list: + * If the slot appears in both lists, it must appear the exact same number of times and at the exact same indexes in both lists. + * If the slot appears only in one list, for each of its appearances in that list, there must be a ScratchSlot in the other list that appears the exact same number of times and at the exact same indexes. diff --git a/pyteal/ir/tealcomponent.py b/pyteal/ir/tealcomponent.py index f723e20f6..0dc69cd9e 100644 --- a/pyteal/ir/tealcomponent.py +++ b/pyteal/ir/tealcomponent.py @@ -74,6 +74,7 @@ def ignoreScratchSlotEquality(cls): likely want to also use use the following code after comparing with this option enabled: .. code-block:: python + TealBlock.MatchScratchSlotReferences( TealBlock.GetReferencedScratchSlots(actual), TealBlock.GetReferencedScratchSlots(expected), diff --git a/pyteal/pragma/pragma.py b/pyteal/pragma/pragma.py index 8f8906666..745b28e28 100644 --- a/pyteal/pragma/pragma.py +++ b/pyteal/pragma/pragma.py @@ -107,6 +107,14 @@ def pragma( compiler_version: Acceptable versions of the compiler. Will fail if the current PyTeal version is not contained in the range. Follows the npm `semver range scheme `_ for specifying compatible versions. + + For example: + + .. code-block:: python + + # this will immediately fail if the current PyTeal version does not satisfy the + # version constraint + pragma(compiler_version="^0.14.0") """ pkg_version = pkg_resources.require("pyteal")[0].version pyteal_version = Version(__convert_pep440_compiler_version(pkg_version)) diff --git a/tests/unit/compile_test.py b/tests/unit/compile_test.py index 9f901da26..427c7e4c6 100644 --- a/tests/unit/compile_test.py +++ b/tests/unit/compile_test.py @@ -1,9 +1,40 @@ from pathlib import Path import pytest +import json import pyteal as pt +def test_abi_algobank(): + from examples.application.abi.algobank import ( + approval_program, + clear_state_program, + contract, + ) + + target_dir = Path.cwd() / "examples" / "application" / "abi" + + with open( + target_dir / "algobank_approval.teal", "r" + ) as expected_approval_program_file: + expected_approval_program = "".join( + expected_approval_program_file.readlines() + ).strip() + assert approval_program == expected_approval_program + + with open( + target_dir / "algobank_clear_state.teal", "r" + ) as expected_clear_state_program_file: + expected_clear_state_program = "".join( + expected_clear_state_program_file.readlines() + ).strip() + assert clear_state_program == expected_clear_state_program + + with open(target_dir / "algobank.json", "r") as expected_contract_file: + expected_contract = json.load(expected_contract_file) + assert contract.dictify() == expected_contract + + def test_basic_bank(): from examples.signature.basic import bank_for_account