From e5c3101358ac15acaa2d4f67a25f90fd9eee6c09 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 16 Jun 2022 14:46:36 -0700 Subject: [PATCH 01/29] Initial commit --- docs/abi.rst | 227 +++++++++++++++++++++++++++++++ docs/api.rst | 7 + docs/index.rst | 1 + pyteal/ast/abi/__init__.py | 4 + pyteal/ast/abi/address.py | 6 +- pyteal/ast/abi/array_base.py | 6 +- pyteal/ast/abi/array_dynamic.py | 4 +- pyteal/ast/abi/array_static.py | 4 +- pyteal/ast/abi/bool.py | 2 +- pyteal/ast/abi/method_return.py | 2 +- pyteal/ast/abi/reference_type.py | 18 ++- pyteal/ast/abi/string.py | 4 +- pyteal/ast/abi/transaction.py | 30 ++-- pyteal/ast/abi/tuple.py | 18 +-- pyteal/ast/abi/type.py | 8 +- pyteal/ast/abi/uint.py | 24 ++-- pyteal/ir/tealcomponent.py | 1 + 17 files changed, 306 insertions(+), 60 deletions(-) create mode 100644 docs/abi.rst diff --git a/docs/abi.rst b/docs/abi.rst new file mode 100644 index 000000000..5168401a3 --- /dev/null +++ b/docs/abi.rst @@ -0,0 +1,227 @@ +.. _abi: + +ABI Support +=========== + +TODO: brief description + +TODO: top-level warning about experimental nature of some APIs +TODO: warning about how ABI types are intended to be used at the moment (for standard serialization at the incoming and outgoing layers of contract) - internal use may be extremely inefficient with ABI types +TODO: warning about how internal representation of ABI types may change over time + +Types +------ + +The ABI supports a variety of types whose encodings are standardized. + +Before we introduce these ABI types, it's important to understand the fundamentals of how PyTeal's ABI type system works. + +: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. + +:any:`abi.TypeSpec` is an abstract base class used to describe types. TODO: finish description + +ABI types are divided into a few categories, each explained in the following sections. + +Basic Types +~~~~~~~~~~~ + +TODO: brief intro + +Definitions +^^^^^^^^^^^^^^^^^^^^^^^^ + +PyTeal supports the following basic static 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 if :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 if all elements are static A tuple of multiple types +============================================== ====================== ================================= ======================================================================================================================================================= + +\*A note about :any:`abi.Tuple`: a proper implementation of this type 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 :code:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :code:`abi.Tuple` for fixed amounts of 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` + +Static vs Dynamic Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +An important property of the above types are whether they are 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. + +Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. Due to this dependency, the code that PyTeal generates to encode, decode, and manipulate dynamic types is more complex and 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. + +Usage +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: explain usage and show examples + +Limitations +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: warn about code inefficiencies and type size limitations + +Reference Types +~~~~~~~~~~~~~~~~~ + +In addition to the basic types defined above, + +Definitions +^^^^^^^^^^^^^^^^^^^^^^^^ + +PyTeal supports the following reference types: + +====================== ====================== ================ ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +====================== ====================== ================ ======================================================================================================================================================= +:any:`abi.Account` :code:`account` Static Represents an additional account that the current transaction can access, stored in the :any:`Txn.accounts ` array +:any:`abi.Asset` :code:`asset` Static Represents an additional asset that the current transaction can access, stored in the :any:`Txn.assets ` array +:any:`abi.Application` :code:`application` Static Represents an additional application that the current transaction can access, stored in the :any:`Txn.applications ` array +====================== ====================== ================ ======================================================================================================================================================= + +Usage +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: explain usage and show examples + +Limitations +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: explain limitations, such as can't be created directly, or used as method return value + +Transaction Types +~~~~~~~~~~~~~~~~~ + +TODO: brief description + +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 transaction type +: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.AssetTransferTransaction` :code:`appl` Static An application call transaction +=================================== ====================== ================ ======================================================================================================================================================= + +Usage +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: explain usage and show examples + +Limitations +^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO: explain limitations, such as can't be created directly, used as method return value, or embedded in other types + +Computed Values +~~~~~~~~~~~~~~~~~ + +TODO: explain what ComputedValue is, where it appears, how to use it, and why it's necessary + +Subroutines with ABI Types +-------------------------- + +TODO: brief description + +ABI Arguments with the Subroutine decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: brief description + +Definition +~~~~~~~~~~~~~~~~~ + +TODO: explain how to create a subroutine with the existing Subroutine decorator and ABI arguments + +Usage +~~~~~~~~~~~~~~~~~ + +TODO: explain how to call a subroutine with ABI arguments + +ABIReturnSubroutine +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: warning again about experimental design +TODO: brief overview of why this is necessary and when it should be used + +Definition +~~~~~~~~~~~~~~~~~ + +TODO: explain how to create a subroutine using ABIReturnSubroutine with ABI return values + +Usage +~~~~~~~~~~~~~~~~~ + +TODO: explain how to call an ABIReturnSubroutine and how to process the return value + +Creating an ARC-4 Program with the ABI Router +---------------------------------------------------- + +TODO: brief intro +TODO: warning again about experimental design + +Registering Bare App Calls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: explain bare app calls and how they can be added to a Router + +Registering Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: explain methods and how they can be added to a Router +TODO: warning about input type validity -- no verification is done for you (right now) + +Building and Compiling a Router Program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: explain how to build/compile a Router program to get the TEAL code + contract JSON + +Calling an ARC-4 Program +-------------------------- + +TODO: brief intro + +Off-Chain, from an SDK or :code:`goal` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: leave pointers to SDK/goal documentation about how to invoke ABI calls + +On-Chain, in an Inner Transaction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +TODO: explain how this is possible but there is no simple way to do it in PyTeal yet; once it is, we should update this section diff --git a/docs/api.rst b/docs/api.rst index b427806b5..d5d8faf82 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,3 +24,10 @@ PyTeal Package :annotation: = The most recently submitted inner transaction. This is an instance of :any:`TxnObject`. + +.. 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/pyteal/ast/abi/__init__.py b/pyteal/ast/abi/__init__.py index 81947e97b..9e7739f67 100644 --- a/pyteal/ast/abi/__init__.py +++ b/pyteal/ast/abi/__init__.py @@ -35,6 +35,8 @@ 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, @@ -76,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 1e8da1f0f..eef183d70 100644 --- a/pyteal/ast/abi/address.py +++ b/pyteal/ast/abi/address.py @@ -17,7 +17,7 @@ class AddressLength(IntEnum): Bytes = 32 -AddressLength.__module__ = "pyteal" +AddressLength.__module__ = "pyteal.abi" class AddressTypeSpec(StaticArrayTypeSpec): @@ -37,7 +37,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]]): @@ -104,4 +104,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 3ed714574..296949a2d 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]): @@ -179,7 +179,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]): @@ -278,4 +278,4 @@ def store_into(self, output: T) -> Expr: return output.decode(encodedArray, startIndex=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..edb40a718 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]): @@ -107,4 +107,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..51f938228 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]): @@ -135,4 +135,4 @@ def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": 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 52b8e513c..ccdefd536 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -37,7 +37,7 @@ def __str__(self) -> str: return "bool" -BoolTypeSpec.__module__ = "pyteal" +BoolTypeSpec.__module__ = "pyteal.abi" class Bool(BaseType): diff --git a/pyteal/ast/abi/method_return.py b/pyteal/ast/abi/method_return.py index b8058456a..7c4d10b46 100644 --- a/pyteal/ast/abi/method_return.py +++ b/pyteal/ast/abi/method_return.py @@ -36,4 +36,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 269fe57dc..d3c848449 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..a75706046 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]): @@ -89,4 +89,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 ca11a0bd2..2bb4327cb 100644 --- a/pyteal/ast/abi/transaction.py +++ b/pyteal/ast/abi/transaction.py @@ -21,7 +21,7 @@ class TransactionType(Enum): ApplicationCall = "appl" -TransactionType.__module__ = "pyteal" +TransactionType.__module__ = "pyteal.abi" class TransactionTypeSpec(TypeSpec): @@ -50,7 +50,7 @@ def __str__(self) -> str: return TransactionType.Any.value -TransactionTypeSpec.__module__ = "pyteal" +TransactionTypeSpec.__module__ = "pyteal.abi" class Transaction(BaseType): @@ -96,7 +96,7 @@ def encode(self) -> Expr: raise TealInputError("A Transaction cannot be encoded") -Transaction.__module__ = "pyteal" +Transaction.__module__ = "pyteal.abi" class PaymentTransactionTypeSpec(TransactionTypeSpec): @@ -110,7 +110,7 @@ def __str__(self) -> str: return TransactionType.Payment.value -PaymentTransactionTypeSpec.__module__ = "pyteal" +PaymentTransactionTypeSpec.__module__ = "pyteal.abi" class PaymentTransaction(Transaction): @@ -118,7 +118,7 @@ def __init__(self): super().__init__(PaymentTransactionTypeSpec()) -PaymentTransaction.__module__ = "pyteal" +PaymentTransaction.__module__ = "pyteal.abi" class KeyRegisterTransactionTypeSpec(TransactionTypeSpec): @@ -132,7 +132,7 @@ def __str__(self) -> str: return TransactionType.KeyRegistration.value -KeyRegisterTransactionTypeSpec.__module__ = "pyteal" +KeyRegisterTransactionTypeSpec.__module__ = "pyteal.abi" class KeyRegisterTransaction(Transaction): @@ -140,7 +140,7 @@ def __init__(self): super().__init__(KeyRegisterTransactionTypeSpec()) -KeyRegisterTransaction.__module__ = "pyteal" +KeyRegisterTransaction.__module__ = "pyteal.abi" class AssetConfigTransactionTypeSpec(TransactionTypeSpec): @@ -154,7 +154,7 @@ def __str__(self) -> str: return TransactionType.AssetConfig.value -AssetConfigTransactionTypeSpec.__module__ = "pyteal" +AssetConfigTransactionTypeSpec.__module__ = "pyteal.abi" class AssetConfigTransaction(Transaction): @@ -162,7 +162,7 @@ def __init__(self): super().__init__(AssetConfigTransactionTypeSpec()) -AssetConfigTransaction.__module__ = "pyteal" +AssetConfigTransaction.__module__ = "pyteal.abi" class AssetFreezeTransactionTypeSpec(TransactionTypeSpec): @@ -176,7 +176,7 @@ def __str__(self) -> str: return TransactionType.AssetFreeze.value -AssetFreezeTransactionTypeSpec.__module__ = "pyteal" +AssetFreezeTransactionTypeSpec.__module__ = "pyteal.abi" class AssetFreezeTransaction(Transaction): @@ -184,7 +184,7 @@ def __init__(self): super().__init__(AssetFreezeTransactionTypeSpec()) -AssetFreezeTransaction.__module__ = "pyteal" +AssetFreezeTransaction.__module__ = "pyteal.abi" class AssetTransferTransactionTypeSpec(TransactionTypeSpec): @@ -198,7 +198,7 @@ def __str__(self) -> str: return TransactionType.AssetTransfer.value -AssetTransferTransactionTypeSpec.__module__ = "pyteal" +AssetTransferTransactionTypeSpec.__module__ = "pyteal.abi" class AssetTransferTransaction(Transaction): @@ -206,7 +206,7 @@ def __init__(self): super().__init__(AssetTransferTransactionTypeSpec()) -AssetTransferTransaction.__module__ = "pyteal" +AssetTransferTransaction.__module__ = "pyteal.abi" class ApplicationCallTransactionTypeSpec(TransactionTypeSpec): @@ -220,7 +220,7 @@ def __str__(self) -> str: return TransactionType.ApplicationCall.value -ApplicationCallTransactionTypeSpec.__module__ = "pyteal" +ApplicationCallTransactionTypeSpec.__module__ = "pyteal.abi" class ApplicationCallTransaction(Transaction): @@ -228,7 +228,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 17c9bb631..273a3c313 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -268,7 +268,7 @@ def __str__(self) -> str: return "({})".format(",".join(map(str, self.value_type_specs()))) -TupleTypeSpec.__module__ = "pyteal" +TupleTypeSpec.__module__ = "pyteal.abi" T_tuple = TypeVar("T_tuple", bound="Tuple") @@ -331,7 +331,7 @@ def __getitem__(self, index: int) -> "TupleElement": return TupleElement(self, index) -Tuple.__module__ = "pyteal" +Tuple.__module__ = "pyteal.abi" T = TypeVar("T", bound=BaseType) @@ -356,7 +356,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 +378,7 @@ def __init__(self) -> None: super().__init__(TupleTypeSpec()) -Tuple0.__module__ = "pyteal" +Tuple0.__module__ = "pyteal.abi" T1 = TypeVar("T1", bound=BaseType) @@ -391,7 +391,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 +404,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 +420,7 @@ def __init__( super().__init__(value_type_spec) -Tuple3.__module__ = "pyteal" +Tuple3.__module__ = "pyteal.abi" T4 = TypeVar("T4", bound=BaseType) @@ -436,7 +436,7 @@ def __init__( super().__init__(value_type_spec) -Tuple4.__module__ = "pyteal" +Tuple4.__module__ = "pyteal.abi" T5 = TypeVar("T5", bound=BaseType) @@ -452,4 +452,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 adcba417a..908546a64 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): @@ -143,7 +143,7 @@ def _set_with_computed_type(self, value: "ComputedValue") -> Expr: return value.store_into(self) -BaseType.__module__ = "pyteal" +BaseType.__module__ = "pyteal.abi" T = TypeVar("T", bound=BaseType) @@ -186,7 +186,7 @@ def use(self, action: Callable[[T], Expr]) -> Expr: return Seq(self.store_into(newInstance), action(newInstance)) -ComputedValue.__module__ = "pyteal" +ComputedValue.__module__ = "pyteal.abi" class ReturnedValue(ComputedValue): @@ -223,4 +223,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 1474ab7b2..28109e0c4 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -151,7 +151,7 @@ def __str__(self) -> str: return "uint{}".format(self.bit_size()) -UintTypeSpec.__module__ = "pyteal" +UintTypeSpec.__module__ = "pyteal.abi" class ByteTypeSpec(UintTypeSpec): @@ -168,7 +168,7 @@ def __str__(self) -> str: return "byte" -ByteTypeSpec.__module__ = "pyteal" +ByteTypeSpec.__module__ = "pyteal.abi" class Uint8TypeSpec(UintTypeSpec): @@ -182,7 +182,7 @@ def annotation_type(self) -> "type[Uint8]": return Uint8 -Uint8TypeSpec.__module__ = "pyteal" +Uint8TypeSpec.__module__ = "pyteal.abi" class Uint16TypeSpec(UintTypeSpec): @@ -196,7 +196,7 @@ def annotation_type(self) -> "type[Uint16]": return Uint16 -Uint16TypeSpec.__module__ = "pyteal" +Uint16TypeSpec.__module__ = "pyteal.abi" class Uint32TypeSpec(UintTypeSpec): @@ -210,7 +210,7 @@ def annotation_type(self) -> "type[Uint32]": return Uint32 -Uint32TypeSpec.__module__ = "pyteal" +Uint32TypeSpec.__module__ = "pyteal.abi" class Uint64TypeSpec(UintTypeSpec): @@ -224,7 +224,7 @@ def annotation_type(self) -> "type[Uint64]": return Uint64 -Uint32TypeSpec.__module__ = "pyteal" +Uint32TypeSpec.__module__ = "pyteal.abi" T = TypeVar("T", bound="Uint") @@ -278,7 +278,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): @@ -286,7 +286,7 @@ def __init__(self) -> None: super().__init__(ByteTypeSpec()) -Byte.__module__ = "pyteal" +Byte.__module__ = "pyteal.abi" class Uint8(Uint): @@ -294,7 +294,7 @@ def __init__(self) -> None: super().__init__(Uint8TypeSpec()) -Uint8.__module__ = "pyteal" +Uint8.__module__ = "pyteal.abi" class Uint16(Uint): @@ -302,7 +302,7 @@ def __init__(self) -> None: super().__init__(Uint16TypeSpec()) -Uint16.__module__ = "pyteal" +Uint16.__module__ = "pyteal.abi" class Uint32(Uint): @@ -310,7 +310,7 @@ def __init__(self) -> None: super().__init__(Uint32TypeSpec()) -Uint32.__module__ = "pyteal" +Uint32.__module__ = "pyteal.abi" class Uint64(Uint): @@ -318,4 +318,4 @@ def __init__(self) -> None: super().__init__(Uint64TypeSpec()) -Uint64.__module__ = "pyteal" +Uint64.__module__ = "pyteal.abi" 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), From d6e85019961844c5489a6666bb3b9af7cfb4a5ca Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Fri, 17 Jun 2022 11:48:23 -0400 Subject: [PATCH 02/29] Fix abi-docs tests (#402) --- pyteal/ast/abi/util_test.py | 20 ++++++++++---------- pyteal/ast/subroutine_test.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyteal/ast/abi/util_test.py b/pyteal/ast/abi/util_test.py index f4fdbb2b8..eb18d86c4 100644 --- a/pyteal/ast/abi/util_test.py +++ b/pyteal/ast/abi/util_test.py @@ -491,61 +491,61 @@ def test_size_of(): # ), (algosdk.abi.TupleType([]), "()", abi.TupleTypeSpec(), abi.Tuple0), ( - "cannot map ABI transaction type spec . 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", ), ) From 9e017613660add5965e5a88fe6b9894891089b94 Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Fri, 17 Jun 2022 11:49:12 -0400 Subject: [PATCH 03/29] Fix abi-docs Sphinx warnings (#401) --- pyteal/ast/abi/array_dynamic.py | 8 +++++++- pyteal/ast/abi/array_static.py | 7 +++++++ pyteal/ir/tealblock.py | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pyteal/ast/abi/array_dynamic.py b/pyteal/ast/abi/array_dynamic.py index edb40a718..41e8e802b 100644 --- a/pyteal/ast/abi/array_dynamic.py +++ b/pyteal/ast/abi/array_dynamic.py @@ -60,7 +60,8 @@ def set( self, values: Union[Sequence[T], "DynamicArray[T]", ComputedValue["DynamicArray[T]"]], ) -> Expr: - """Set the ABI dynamic array with one of the following + """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 @@ -69,10 +70,15 @@ def set( from ComputedType to store the internal ABI encoding into this StaticArray. 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`. Args: diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py index 51f938228..c88daed22 100644 --- a/pyteal/ast/abi/array_static.py +++ b/pyteal/ast/abi/array_static.py @@ -85,6 +85,7 @@ def set( ], ) -> 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 @@ -93,11 +94,17 @@ def set( 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`. Args: 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. From a581965fa053ad43554193b4b14ccb2ba506f9db Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Tue, 21 Jun 2022 13:10:13 -0400 Subject: [PATCH 04/29] Extend abi-docs with experimental design language (#403) --- docs/abi.rst | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 5168401a3..82647d072 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -5,7 +5,16 @@ ABI Support TODO: brief description -TODO: top-level warning about experimental nature of some APIs +.. 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. + TODO: warning about how ABI types are intended to be used at the moment (for standard serialization at the incoming and outgoing layers of contract) - internal use may be extremely inefficient with ABI types TODO: warning about how internal representation of ABI types may change over time @@ -176,7 +185,12 @@ TODO: explain how to call a subroutine with ABI arguments ABIReturnSubroutine ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: warning again about experimental design +.. 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. + TODO: brief overview of why this is necessary and when it should be used Definition @@ -193,7 +207,11 @@ Creating an ARC-4 Program with the ABI Router ---------------------------------------------------- TODO: brief intro -TODO: warning again about experimental design + +.. 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. Registering Bare App Calls ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From a5af855577cd3bda349590e83ddc0e2e1c37dd43 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 21 Jun 2022 11:09:13 -0700 Subject: [PATCH 05/29] Add type fundamentals section --- docs/abi.rst | 134 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 33 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 82647d072..597c05163 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -23,25 +23,103 @@ Types The ABI supports a variety of types whose encodings are standardized. +Fundamentals +~~~~~~~~~~~~ + Before we introduce these ABI types, it's important to understand the fundamentals of how PyTeal's ABI type system works. +: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. -:any:`abi.TypeSpec` is an abstract base class used to describe types. TODO: finish description +: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. + +Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. Due to this dependency, 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**. + +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. + +With the Constructor +"""""""""""""""""""""" + +The most obvious way is to use its constructor, like so: + +.. code-block:: python + + myUint8 = abi.Uint8() + myUint64 = abi.Uint64() + myArrayOf12Uint8s = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 12)) + +For simple types, using the constructor is straightforward and works as you would expect. However, more complex 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 +"""""""""""""""""""""""""""""""""""""" + +You may remember that :code:`abi.TypeSpec` has a :any:`new_instance() ` method that can be used to instantiate ABI types. This is another way of instantiating ABI types, if you happen to have an :code:`abi.TypeSpec` instance available. For example: + +.. code-block:: python + + myUintType = abi.Uint8TypeSpec() + myUint8 = myUintType.new_instance() + + myArrayType = abi.StaticArrayTypeSpec(myUintType, 12) + myArrayOf12Uint8s = myArrayType.new_instance() + +With :code:`abi.make` +""""""""""""""""""""" + +Using :code:`abi.TypeSpec.new_instance()` makes sense if you already have an instance of the right :code:`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 complex type. + +To use it, you pass in a Python type annotation that describes the ABI type, and :code:`abi.make` will create an instance of it for you. For example: + +.. code-block:: python + + from typing import Literal + + myUint8 = abi.make(abi.Uint8) + myUint64 = abi.make(abi.Uint64) + myArrayOf12Uint8s = abi.make(abi.StaticArray[abi.Uint8, Literal[12]]) -ABI types are divided into a few categories, each explained in the following sections. +.. 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. + +Categories +~~~~~~~~~~ + +There are three categories of ABI types: + +1. Basic types +2. Reference types +3. Transaction types + +Each of which is described in detail in the following subsections. Basic Types -~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^ TODO: brief intro Definitions -^^^^^^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""""" PyTeal supports the following basic static types: @@ -61,14 +139,15 @@ PyTeal Type ARC-4 Type Dynamic / :any:`abi.Tuple`\* :code:`(...)` Static if all elements are static A tuple of multiple types ============================================== ====================== ================================= ======================================================================================================================================================= -\*A note about :any:`abi.Tuple`: a proper implementation of this type 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 :code:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :code:`abi.Tuple` for fixed amounts of generic arguments: +.. 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 :code:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :code:`abi.Tuple` for fixed amounts of 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)` + * :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: @@ -76,34 +155,23 @@ These ARC-4 types are not yet supported in PyTeal: * Unsigned integers larger than 64 bits * Fixed point unsigned integers, i.e. :code:`ufixedx` -Static vs Dynamic Types -^^^^^^^^^^^^^^^^^^^^^^^^ - -An important property of the above types are whether they are 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. - -Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. Due to this dependency, the code that PyTeal generates to encode, decode, and manipulate dynamic types is more complex and 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. - Usage -^^^^^^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""""" TODO: explain usage and show examples Limitations -^^^^^^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""""" TODO: warn about code inefficiencies and type size limitations Reference Types -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^ -In addition to the basic types defined above, +TODO: brief description Definitions -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" PyTeal supports the following reference types: @@ -116,22 +184,22 @@ PyTeal Type ARC-4 Type Dynamic / Static Description ====================== ====================== ================ ======================================================================================================================================================= Usage -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" TODO: explain usage and show examples Limitations -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" TODO: explain limitations, such as can't be created directly, or used as method return value Transaction Types -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^ TODO: brief description Definitions -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" PyTeal supports the following transaction types: @@ -148,12 +216,12 @@ PyTeal Type ARC-4 Type Dynamic / Static Desc =================================== ====================== ================ ======================================================================================================================================================= Usage -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" TODO: explain usage and show examples Limitations -^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""""""" TODO: explain limitations, such as can't be created directly, used as method return value, or embedded in other types From 4f1ec487634c3db1332b38da6dab9ec9679af922 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 22 Jun 2022 17:06:41 -0700 Subject: [PATCH 06/29] Add basic type usage and some docstrings for referenced methods --- docs/abi.rst | 80 ++++++++++++++++++++++++++++++++-- pyteal/ast/abi/address.py | 27 ++++++++++-- pyteal/ast/abi/array_static.py | 29 +++--------- pyteal/ast/abi/bool.py | 23 +++++++++- pyteal/ast/abi/tuple.py | 6 +-- pyteal/ast/abi/uint.py | 32 ++++++++++---- 6 files changed, 155 insertions(+), 42 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 597c05163..2f4de371a 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -116,12 +116,12 @@ Each of which is described in detail in the following subsections. Basic Types ^^^^^^^^^^^^^^^^^^^^^^^^ -TODO: brief intro +Basic types are the most straightforward category of ABI types. These types are used to hold values and they have no other side effects, in contrast to the other categories of types. Definitions """"""""""""""""""""" -PyTeal supports the following basic static types: +PyTeal supports the following basic types: ============================================== ====================== ================================= ======================================================================================================================================================= PyTeal Type ARC-4 Type Dynamic / Static Description @@ -158,7 +158,81 @@ These ARC-4 types are not yet supported in PyTeal: Usage """"""""""""""""""""" -TODO: explain usage and show examples +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.set()` +* :any:`abi.Address.set()` +* :any:`abi.DynamicArray.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 + + myAddress = abi.make(abi.Address) + myBool = abi.make(abi.Bool) + myUint64 = abi.make(abi.Uint64) + myTuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) + + program = Seq( + myAddress.set(Txn.sender()), + myBool.set(Txn.fee() == Int(0)), + myUint64.set(5000), + myTuple.set(myAddress, myBool, myUint64) + ) + +Getting 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 :code:`abi.Uint` classes and :code:`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 + + myUint64 = abi.make(abi.Uint64) + + program = Seq( + myUint64.decode(Txn.application_args[1]), + Assert(myUint64.get() != Int(0)) + ) + +Getting Values at Indexes +'''''''''''''''''''''''''' + +The types :code:`abi.StaticArray`, :code:`abi.Address`, :code:`abi.DynamicArray`, :code:`abi.String`, and :code:`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 extract these values. + +The supported methods are: + +* :any:`abi.StaticArray.__getitem__`, used for :code:`abi.StaticArray` and :code:`abi.Address` +* :any:`abi.Array.__getitem__`, used for :code:`abi.DynamicArray` and :code:`abi.String` +* :any:`abi.Tuple.__getitem__` + +Be aware that these methods return a :code:`ComputedValue`, TODO link to Computed Value 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 + + myTuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) + myBool = abi.make(abi.Bool) + + program = Seq( + myTuple.decode(Txn.application_args[1]), + myBool.set(myTuple[2]), + Assert(myBool.get()) + ) Limitations """"""""""""""""""""" diff --git a/pyteal/ast/abi/address.py b/pyteal/ast/abi/address.py index eef183d70..baa5cd2eb 100644 --- a/pyteal/ast/abi/address.py +++ b/pyteal/ast/abi/address.py @@ -53,15 +53,35 @@ def get(self) -> Expr: 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. + * :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(): @@ -95,6 +115,7 @@ def set( f"Got bytes with length {len(value)}, expected {AddressLength.Bytes}" ) case Expr(): + # TODO: need to validate length of value is 32 return self.stored_value.store(value) case CollectionSequence(): return super().set(cast(Sequence[Byte], value)) diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py index c88daed22..12d16b405 100644 --- a/pyteal/ast/abi/array_static.py +++ b/pyteal/ast/abi/array_static.py @@ -84,34 +84,19 @@ def set( Sequence[T], "StaticArray[T, N]", ComputedValue["StaticArray[T, N]"] ], ) -> Expr: - """Set the ABI static array with one of the following: + """Set the elements of this StaticArray to the input values. - * a sequence of ABI type variables - * or another ABI static array - * or a ComputedType with same TypeSpec + The behavior of this method depends on the input argument type: - 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`. + * :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. + value: 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) diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index ccdefd536..c23ed57a5 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -45,14 +45,33 @@ def __init__(self) -> None: super().__init__(BoolTypeSpec()) def get(self) -> Expr: + """Return the value held by this Bool variable 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) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 273a3c313..2f7ee910d 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -270,8 +270,6 @@ def __str__(self) -> str: TupleTypeSpec.__module__ = "pyteal.abi" -T_tuple = TypeVar("T_tuple", bound="Tuple") - class Tuple(BaseType): def __init__(self, tuple_type_spec: TupleTypeSpec) -> None: @@ -298,7 +296,7 @@ def set(self, *values: BaseType) -> Expr: ... @overload - def set(self: T_tuple, value: ComputedValue[T_tuple]) -> Expr: + def set(self, value: ComputedValue["Tuple"]) -> Expr: ... def set(self, *values): @@ -325,7 +323,7 @@ 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[BaseType]": if not (0 <= index < self.type_spec().length_static()): raise TealInputError(f"Index out of bounds: {index}") return TupleElement(self, index) diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py index 28109e0c4..ab348db8d 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -1,5 +1,4 @@ from typing import ( - TypeVar, Union, Optional, Final, @@ -109,9 +108,6 @@ def uint_encode(size: int, uintVar: 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__() @@ -227,9 +223,6 @@ def annotation_type(self) -> "type[Uint64]": Uint32TypeSpec.__module__ = "pyteal.abi" -T = TypeVar("T", bound="Uint") - - class Uint(BaseType): @abstractmethod def __init__(self, spec: UintTypeSpec) -> None: @@ -239,9 +232,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 does not fit in 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 does not fit in 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) From 9a04c10f032bb033592411b0b74414ebe97ae629 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 23 Jun 2022 12:01:33 -0700 Subject: [PATCH 07/29] Finish documenting set and fix overloaded method docs --- pyteal/ast/abi/array_dynamic.py | 28 ++++++++-------------------- pyteal/ast/abi/array_static.py | 2 +- pyteal/ast/abi/bool.py | 2 +- pyteal/ast/abi/string.py | 26 +++++++++++++++++++++++--- pyteal/ast/abi/tuple.py | 26 ++++++++++++++++++++++---- pyteal/ast/bytes.py | 10 +++++++--- pyteal/ast/seq.py | 16 ++++++++++------ 7 files changed, 72 insertions(+), 38 deletions(-) diff --git a/pyteal/ast/abi/array_dynamic.py b/pyteal/ast/abi/array_dynamic.py index 41e8e802b..e5aec04af 100644 --- a/pyteal/ast/abi/array_dynamic.py +++ b/pyteal/ast/abi/array_dynamic.py @@ -60,32 +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 - - 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 dynamic array: - - * if so: - - * checks whether `values` is same type as this ABI dynamic array. - - * stores the encoding of `values`. + """ + Set the elements of this DynamicArray to the input values. - * if not: + The behavior of this method depends on the input argument type: - * 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): diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py index 12d16b405..a0d26e516 100644 --- a/pyteal/ast/abi/array_static.py +++ b/pyteal/ast/abi/array_static.py @@ -93,7 +93,7 @@ def set( * :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: - value: The new elements this StaticArray should have. This must follow the above constraints. + values: The new elements this StaticArray should have. This must follow the above constraints. Returns: An expression which stores the given value into this StaticArray. diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index c23ed57a5..c4b2ae80c 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 diff --git a/pyteal/ast/abi/string.py b/pyteal/ast/abi/string.py index a75706046..b398117b2 100644 --- a/pyteal/ast/abi/string.py +++ b/pyteal/ast/abi/string.py @@ -55,15 +55,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(): diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 2f7ee910d..c0f7b1cf9 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -293,13 +293,31 @@ def decode( @overload def set(self, *values: BaseType) -> Expr: - ... + pass @overload - def set(self, value: ComputedValue["Tuple"]) -> Expr: - ... + def set(self, values: ComputedValue["Tuple"]) -> Expr: + # TODO: should support value as a Tuple as well + pass - def set(self, *values): + def set(self, *values) -> Expr: + """ + 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]) diff --git a/pyteal/ast/bytes.py b/pyteal/ast/bytes.py index e88163611..4c4fb4d6e 100644 --- a/pyteal/ast/bytes.py +++ b/pyteal/ast/bytes.py @@ -15,14 +15,18 @@ class Bytes(LeafExpr): @overload def __init__(self, arg1: Union[str, bytes, bytearray]) -> 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/seq.py b/pyteal/ast/seq.py index 4185cd5cc..f00cf7093 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. + def __init__(self, *exprs) -> None: + """ + __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. From 7ef9537f44e47674595fa9fc144a6dd172cc2f64 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 23 Jun 2022 14:29:25 -0700 Subject: [PATCH 08/29] Add docstrings for get and __getitem__ --- pyteal/ast/abi/address.py | 4 ++++ pyteal/ast/abi/array_base.py | 11 +++++++---- pyteal/ast/abi/array_static.py | 14 ++++++++++++++ pyteal/ast/abi/bool.py | 3 ++- pyteal/ast/abi/string.py | 4 ++++ pyteal/ast/abi/tuple.py | 14 +++++++++++++- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/pyteal/ast/abi/address.py b/pyteal/ast/abi/address.py index baa5cd2eb..36de793ca 100644 --- a/pyteal/ast/abi/address.py +++ b/pyteal/ast/abi/address.py @@ -48,6 +48,10 @@ 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( diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index 296949a2d..dbe52f7a5 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -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: diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py index a0d26e516..6829106c2 100644 --- a/pyteal/ast/abi/array_static.py +++ b/pyteal/ast/abi/array_static.py @@ -122,6 +122,20 @@ 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) diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index c4b2ae80c..1f383076f 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -45,7 +45,7 @@ def __init__(self) -> None: super().__init__(BoolTypeSpec()) def get(self) -> Expr: - """Return the value held by this Bool variable as a PyTeal expression. + """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 @@ -90,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)), ) diff --git a/pyteal/ast/abi/string.py b/pyteal/ast/abi/string.py index b398117b2..c4deb77ed 100644 --- a/pyteal/ast/abi/string.py +++ b/pyteal/ast/abi/string.py @@ -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()) ) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index c0f7b1cf9..f67e9a7fa 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -297,7 +297,7 @@ def set(self, *values: BaseType) -> Expr: @overload def set(self, values: ComputedValue["Tuple"]) -> Expr: - # TODO: should support value as a Tuple as well + # TODO: should support values as a Tuple as well pass def set(self, *values) -> Expr: @@ -342,6 +342,18 @@ def length(self) -> Expr: return Int(self.type_spec().length_static()) def __getitem__(self, index: int) -> "TupleElement[BaseType]": + """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. + """ if not (0 <= index < self.type_spec().length_static()): raise TealInputError(f"Index out of bounds: {index}") return TupleElement(self, index) From 0c7fe6fc7956e36eed35d1b995c26939a251c8dd Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Fri, 24 Jun 2022 15:18:28 -0700 Subject: [PATCH 09/29] Add reference type docs --- docs/abi.rst | 162 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 145 insertions(+), 17 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 2f4de371a..2a6e1aa81 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -201,12 +201,14 @@ A brief example is below. Please consult the documentation linked above for each .. code-block:: python - myUint64 = abi.make(abi.Uint64) - - program = Seq( - myUint64.decode(Txn.application_args[1]), - Assert(myUint64.get() != Int(0)) - ) + @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 '''''''''''''''''''''''''' @@ -225,24 +227,27 @@ A brief example is below. Please consult the documentation linked above for each .. code-block:: python - myTuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) - myBool = abi.make(abi.Bool) - - program = Seq( - myTuple.decode(Txn.application_args[1]), - myBool.set(myTuple[2]), - Assert(myBool.get()) - ) + @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))) + ) Limitations """"""""""""""""""""" -TODO: warn about code inefficiencies and type size limitations +TODO: explain type size limitations Reference Types ^^^^^^^^^^^^^^^^^^^^^^^^ -TODO: brief description +Some AVM operations require specific values to be placed in the "foreign arrays" of the app call transaction. Reference types allow methods to describe these requirements. + +Reference types are only valid in the arguments of a method. They may not appear in a method's return value in any form. Definitions """""""""""""""""""""""""""""""""""""""""" @@ -257,10 +262,133 @@ PyTeal Type ARC-4 Type Dynamic / Static Description :any:`abi.Application` :code:`application` Static Represents an additional 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`. + Usage """""""""""""""""""""""""""""""""""""""""" -TODO: explain usage and show examples +Getting Referenced Indexes +'''''''''''''''''''''''''' + +Because reference types represent values placed into one of the transaction's foreign arrays, each reference type value is associated with a specific index into the appropriate array. + +All reference types implement the method :any:`abi.ReferenceType.referenced_index()` which can be used to access this index. + +A brief example is below: + +.. code-block:: python + + @Subroutine(TealType.none) + def referenced_index_example( + account: abi.Account, asset: abi.Asset, app: abi.Application + ) -> Expr: + return Seq( + # The accounts array has Txn.accounts.length() + 1 elements in it (the +1 is the txn sender) + Assert(account.referenced_index() <= Txn.accounts.length()), + # The assets array has Txn.assets.length() elements in it + Assert(asset.referenced_index() < Txn.assets.length()), + # The applications array has Txn.applications.length() + 1 elements in it (the +1 is the current app) + Assert(app.referenced_index() <= Txn.applications.length()), + ) + +Getting Referenced Values +'''''''''''''''''''''''''' + +Perhaps more important than the index of a referenced type is its value. 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 + + @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 + + @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`. + +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()`: given an asset, returns an :any:`AssetHoldingObject` +* :any:`abi.Asset.holding()`: 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 + + @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)))), + ) Limitations """""""""""""""""""""""""""""""""""""""""" From 33da26c82b8e18c54c1ae5b766da1d35fcb37d41 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 27 Jun 2022 16:47:10 -0700 Subject: [PATCH 10/29] Add txn type examples --- docs/abi.rst | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 2a6e1aa81..bcb58ae9b 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -398,7 +398,9 @@ TODO: explain limitations, such as can't be created directly, or used as method Transaction Types ^^^^^^^^^^^^^^^^^^^^^^^^ -TODO: brief description +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, unique, transaction that must appear immediately before the application call. Definitions """""""""""""""""""""""""""""""""""""""""" @@ -420,7 +422,50 @@ PyTeal Type ARC-4 Type Dynamic / Static Desc Usage """""""""""""""""""""""""""""""""""""""""" -TODO: explain usage and show examples +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 + + @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 + + @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(), + ), + ) Limitations """""""""""""""""""""""""""""""""""""""""" From 6a77a65bc24b74470b443d83694f123487b62a78 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 28 Jun 2022 11:09:11 -0700 Subject: [PATCH 11/29] Fix errors --- pyteal/ast/abi/transaction.py | 8 +++++--- pyteal/ast/abi/tuple.py | 2 +- pyteal/ast/abi/uint.py | 5 ++++- pyteal/ast/seq.py | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyteal/ast/abi/transaction.py b/pyteal/ast/abi/transaction.py index 2bb4327cb..53c774961 100644 --- a/pyteal/ast/abi/transaction.py +++ b/pyteal/ast/abi/transaction.py @@ -8,8 +8,6 @@ from pyteal.types import TealType from pyteal.errors import TealInputError -T = TypeVar("T", bound=BaseType) - class TransactionType(Enum): Any = "txn" @@ -52,6 +50,8 @@ def __str__(self) -> str: TransactionTypeSpec.__module__ = "pyteal.abi" +Self = TypeVar("Self", bound="Transaction") + class Transaction(BaseType): def __init__(self, spec: TransactionTypeSpec = None) -> None: @@ -66,7 +66,9 @@ def type_spec(self) -> TransactionTypeSpec: def get(self) -> TxnObject: return Gtxn[self.index()] - def set(self: T, value: Union[int, Expr, "Transaction", ComputedValue[T]]) -> Expr: + def set( + self: Self, value: Union[int, Expr, "Transaction", ComputedValue[Self]] + ) -> Expr: match value: case ComputedValue(): return self._set_with_computed_type(value) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index f67e9a7fa..a46f3aa07 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -300,7 +300,7 @@ def set(self, values: ComputedValue["Tuple"]) -> Expr: # TODO: should support values as a Tuple as well pass - def set(self, *values) -> Expr: + def set(self, *values): """ set(*values: BaseType) -> Expr set(values: ComputedValue[Tuple]) -> Expr diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py index ab348db8d..1978edcf6 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -3,6 +3,7 @@ Optional, Final, cast, + TypeVar, ) from abc import abstractmethod @@ -222,6 +223,8 @@ def annotation_type(self) -> "type[Uint64]": Uint32TypeSpec.__module__ = "pyteal.abi" +Self = TypeVar("Self", bound="Uint") + class Uint(BaseType): @abstractmethod @@ -238,7 +241,7 @@ def get(self) -> Expr: """ return self.stored_value.load() - def set(self, value: Union[int, Expr, "Uint", ComputedValue["Uint"]]) -> Expr: + def set(self: Self, value: Union[int, Expr, "Uint", ComputedValue[Self]]) -> 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 diff --git a/pyteal/ast/seq.py b/pyteal/ast/seq.py index f00cf7093..39901cbd2 100644 --- a/pyteal/ast/seq.py +++ b/pyteal/ast/seq.py @@ -20,7 +20,7 @@ def __init__(self, *exprs: Expr) -> None: def __init__(self, exprs: List[Expr]) -> None: pass - def __init__(self, *exprs) -> None: + def __init__(self, *exprs): """ __init__(*exprs: Expr) -> None __init__(exprs: List[Expr]) -> None From 5e61e1817477253196e4255457141847409759fa Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 29 Jun 2022 09:48:03 -0700 Subject: [PATCH 12/29] Make ComputedValue parameter type covariant --- pyteal/ast/abi/transaction.py | 6 ++---- pyteal/ast/abi/type.py | 12 ++++++------ pyteal/ast/abi/uint.py | 5 +---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/pyteal/ast/abi/transaction.py b/pyteal/ast/abi/transaction.py index 53c774961..be9ddb89f 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 @@ -50,8 +50,6 @@ def __str__(self) -> str: TransactionTypeSpec.__module__ = "pyteal.abi" -Self = TypeVar("Self", bound="Transaction") - class Transaction(BaseType): def __init__(self, spec: TransactionTypeSpec = None) -> None: @@ -67,7 +65,7 @@ def get(self) -> TxnObject: return Gtxn[self.index()] def set( - self: Self, value: Union[int, Expr, "Transaction", ComputedValue[Self]] + self, value: Union[int, Expr, "Transaction", ComputedValue["Transaction"]] ) -> Expr: match value: case ComputedValue(): diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index 908546a64..62689690b 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -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( @@ -145,10 +145,10 @@ def _set_with_computed_type(self, value: "ComputedValue") -> Expr: 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 @@ -157,7 +157,7 @@ def produced_type_spec(self) -> TypeSpec: pass @abstractmethod - def store_into(self, output: T) -> Expr: + def store_into(self, output: T_co) -> Expr: # type: ignore[misc] """Store the value of this computed type into an existing ABI type instance. Args: @@ -170,7 +170,7 @@ def store_into(self, output: T) -> Expr: """ pass - def use(self, action: Callable[[T], Expr]) -> Expr: + def use(self, action: Callable[[T_co], Expr]) -> Expr: """Use the computed value represented by this class in a function or lambda expression. Args: @@ -182,7 +182,7 @@ def use(self, action: Callable[[T], Expr]) -> Expr: 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)) diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py index 1978edcf6..ab348db8d 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -3,7 +3,6 @@ Optional, Final, cast, - TypeVar, ) from abc import abstractmethod @@ -223,8 +222,6 @@ def annotation_type(self) -> "type[Uint64]": Uint32TypeSpec.__module__ = "pyteal.abi" -Self = TypeVar("Self", bound="Uint") - class Uint(BaseType): @abstractmethod @@ -241,7 +238,7 @@ def get(self) -> Expr: """ return self.stored_value.load() - def set(self: Self, value: Union[int, Expr, "Uint", ComputedValue[Self]]) -> 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 From 41d8e853d4570e7e32808b69f8528452f31ca8dc Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 29 Jun 2022 16:13:14 -0700 Subject: [PATCH 13/29] ComputedValue and subroutine sections --- pyteal/ast/abi/type.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index 62689690b..6a812da15 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -158,7 +158,11 @@ def produced_type_spec(self) -> TypeSpec: @abstractmethod def store_into(self, output: T_co) -> Expr: # type: ignore[misc] - """Store the value of this computed type into an existing ABI type instance. + """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 @@ -171,7 +175,11 @@ def store_into(self, output: T_co) -> Expr: # type: ignore[misc] pass def use(self, action: Callable[[T_co], Expr]) -> Expr: - """Use the computed value represented by this class in a function or lambda expression. + """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 @@ -179,7 +187,7 @@ def use(self, action: Callable[[T_co], 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_co, self.produced_type_spec().new_instance()) From b5d00a0f21897b2088a6d53d6676dfdc4c8ce615 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 29 Jun 2022 16:15:16 -0700 Subject: [PATCH 14/29] ...wasn't included in previous commit --- docs/abi.rst | 162 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 127 insertions(+), 35 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index bcb58ae9b..d3757a1c6 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -33,7 +33,7 @@ Before we introduce these ABI types, it's important to understand the fundamenta :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.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. @@ -102,6 +102,59 @@ To use it, you pass in a Python type annotation that describes the ABI type, and .. 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 +^^^^^^^^^^^^^^^^^^^^^^^ + +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 ABI type directly from the operation, as usual. However, in PyTeal, there operations must actually return two things: + +1. An instance of the ABI type 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]) -> 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 is recommended to only issue a single call to either of these two method. + +A brief example is below: + +.. code-block:: python + + @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), + ) + Categories ~~~~~~~~~~ @@ -163,13 +216,13 @@ 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.set()` -* :any:`abi.Address.set()` -* :any:`abi.DynamicArray.set()` -* :any:`abi.String.set()` -* :any:`abi.Tuple.set()` +* :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. @@ -217,9 +270,9 @@ The types :code:`abi.StaticArray`, :code:`abi.Address`, :code:`abi.DynamicArray` The supported methods are: -* :any:`abi.StaticArray.__getitem__`, used for :code:`abi.StaticArray` and :code:`abi.Address` -* :any:`abi.Array.__getitem__`, used for :code:`abi.DynamicArray` and :code:`abi.String` -* :any:`abi.Tuple.__getitem__` +* :any:`abi.StaticArray.__getitem__(index: int | Expr) `, used for :code:`abi.StaticArray` and :code:`abi.Address` +* :any:`abi.Array.__getitem__(index: int | Expr) `, used for :code:`abi.DynamicArray` and :code:`abi.String` +* :any:`abi.Tuple.__getitem__(index: int) ` Be aware that these methods return a :code:`ComputedValue`, TODO link to Computed Value section @@ -369,8 +422,8 @@ 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()`: given an asset, returns an :any:`AssetHoldingObject` -* :any:`abi.Asset.holding()`: given an account, returns an :any:`AssetHoldingObject` +* :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. @@ -472,32 +525,51 @@ Limitations TODO: explain limitations, such as can't be created directly, used as method return value, or embedded in other types -Computed Values -~~~~~~~~~~~~~~~~~ - -TODO: explain what ComputedValue is, where it appears, how to use it, and why it's necessary Subroutines with ABI Types -------------------------- -TODO: brief description +Subroutines can be created that accept ABI types are 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. -ABI Arguments with the Subroutine decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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`. -TODO: brief description +Subroutines that Return Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Definition -~~~~~~~~~~~~~~~~~ +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 normal :any:`@Subroutine ` decorator can be used. -TODO: explain how to create a subroutine with the existing Subroutine decorator and ABI arguments +To indicate the type of each argument, 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 affect on the code PyTeal generates. -Usage -~~~~~~~~~~~~~~~~~ +An example of this type of subroutine is below: -TODO: explain how to call a subroutine with ABI arguments +.. code-block:: python + + @Subroutine(TealType.uint64) + def get_volume_of_rectangular_prism( + length: abi.Uint16, width: abi.Uint64, height: Expr + ) -> Expr: + return length.get() * width.get() * height -ABIReturnSubroutine +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 expression 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. For example: + +.. code-block:: python + + 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:: @@ -506,17 +578,37 @@ ABIReturnSubroutine * 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. -TODO: brief overview of why this is necessary and when it should be used +In addition to accepting ABI types as arguments, it's also possible for a subroutine to return an ABI type value. -Definition -~~~~~~~~~~~~~~~~~ +As mentioned in the Computed Value section (TODO: link), 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. -TODO: explain how to create a subroutine using ABIReturnSubroutine with ABI return values +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 new 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: -Usage -~~~~~~~~~~~~~~~~~ +.. code-block:: python + + @ABIReturnSubroutine + def get_account_status( + account: abi.Account, *, output: abi.Tuple2[abi.Uint64, abi.Bool] + ) -> Expr: + balance = abi.Uint64() + is_admin = abi.Bool() + return Seq( + balance.set(App.localGet(account.address(), Bytes("balance"))), + is_admin.set(App.localGet(account.address(), 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`. -TODO: explain how to call an ABIReturnSubroutine and how to process the return value +The only exception to this transformation is if the subroutine has no return value, in which case a :code:`ComputedValue` is unnecessary, so the subroutine will still return an :code:`Expr` to the caller. In this case the :code:`@ABIReturnSubroutine` decorator acts identically the original :code:`@Subroutine` decorator. Creating an ARC-4 Program with the ABI Router ---------------------------------------------------- From 35b60fe1a279a5b534b587c9c1f2c52a10ddca39 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 5 Jul 2022 14:44:45 -0700 Subject: [PATCH 15/29] Add bare app call and method registration examples --- docs/abi.rst | 170 +++++++++++++++++++++++++++++++++++++++++-- pyteal/ast/router.py | 48 +++++++++--- 2 files changed, 198 insertions(+), 20 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index d3757a1c6..aa9d2854c 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -118,7 +118,7 @@ The :any:`abi.ComputedValue[T] ` abstract base class provides * :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]) -> 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. +* :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 is recommended to only issue a single call to either of these two method. @@ -588,13 +588,13 @@ The :code:`@ABIReturnSubroutine` decorator should be used with subroutines that @ABIReturnSubroutine def get_account_status( - account: abi.Account, *, output: abi.Tuple2[abi.Uint64, abi.Bool] + 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.address(), Bytes("balance"))), - is_admin.set(App.localGet(account.address(), Bytes("is_admin"))), + balance.set(App.localGet(account.get(), Bytes("balance"))), + is_admin.set(App.localGet(account.get(), Bytes("is_admin"))), output.set(balance, is_admin), ) @@ -613,24 +613,178 @@ The only exception to this transformation is if the subroutine has no return val Creating an ARC-4 Program with the ABI Router ---------------------------------------------------- -TODO: brief intro - .. 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. +An ARC-4 ABI compatible program can be called in up to two ways: + +* Through a method call, which chooses a specific method implemented by the contract and calls it with the appropriate arguments. +* Through a bare app call, which has 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 between 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 :code:`Router` class. + Registering Bare App Calls ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: explain bare app calls and how they can be added to a Router +The AVM supports 6 types of OnCompletion options that may be specified on an app call transaction. These actions are: + +#. No op +#. Opt in +#. Close out +#. Clear state +#. Update application +#. Delete application + +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, during a non-creation app call, or during either. + +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 + + @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( + no_op=OnCompleteAction( + action=Approve(), call_config=CallConfig.CREATE + ), + opt_in=OnCompleteAction( + action=opt_in_handler, call_config=CallConfig.ALL + ), + close_out=OnCompleteAction( + action=Approve(), call_config=CallConfig.CALL + ), + update_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + delete_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + ), + ) + +TODO: explain example + +When deciding which :code:`CallConfig` value is appropriate for a bare app call or method, ask yourself the question, should should it be valid for someone to create my app by calling this method? Registering Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: explain methods and how they can be added to a Router TODO: warning about input type validity -- no verification is done for you (right now) +Method can be registered in two ways. + +The first way to register a method is with the :any:`Router.add_method_handler` method, which takes an existing subroutine decorated with :code:`@ABIReturnSubroutine`. An example of this is below: + +.. code-block:: python + + router = Router( + name="Calculator", + bare_calls=BareCallActions( + # allow this app to be created with a NoOp 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()) + + router.add_method_handler(add) + + @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())), + ) + + 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 only be 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 + + my_router = Router( + name="Calculator", + bare_calls=BareCallActions( + # allow this app to be created with a NoOp 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())), + ) + Building and Compiling a Router Program ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py index 994af7e07..666d4c2cc 100644 --- a/pyteal/ast/router.py +++ b/pyteal/ast/router.py @@ -539,6 +539,19 @@ def add_method_handler( method_config: MethodConfig = None, description: str = None, ) -> None: + """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" @@ -583,26 +596,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 From 67edead1b6d56b72a72808e04f94ad62106a6ef0 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 6 Jul 2022 11:15:36 -0700 Subject: [PATCH 16/29] Add router e2e example and compilation explanation --- docs/abi.rst | 18 +- examples/application/abi/__init__.py | 0 examples/application/abi/algobank.json | 54 +++++ examples/application/abi/algobank.py | 113 +++++++++ .../application/abi/algobank_approval.teal | 219 ++++++++++++++++++ .../application/abi/algobank_clear_state.teal | 17 ++ pyteal/ast/router.py | 33 +-- tests/unit/compile_test.py | 31 +++ 8 files changed, 470 insertions(+), 15 deletions(-) create mode 100644 examples/application/abi/__init__.py create mode 100644 examples/application/abi/algobank.json create mode 100644 examples/application/abi/algobank.py create mode 100644 examples/application/abi/algobank_approval.teal create mode 100644 examples/application/abi/algobank_clear_state.teal diff --git a/docs/abi.rst b/docs/abi.rst index aa9d2854c..b57a0e854 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -785,10 +785,24 @@ The second way to register a method is with the :any:`Router.method` decorator p App.localPut(Txn.sender(), Bytes("result", output.get())), ) -Building and Compiling a Router Program +Compiling a Router Program ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: explain how to build/compile a Router program to get the TEAL code + contract JSON +Now that we know how to add bare app call and method call handlers to a :code:`Router`, the next step is to compile the :code:`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. + +In addition to receiving the approval and clear state programs, the :code:`Router.compile_program` method also returns a :code:`Contract` object `from the Python SDK `_. 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 :code:`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 -------------------------- 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..42dee701f --- /dev/null +++ b/examples/application/abi/algobank_approval.teal @@ -0,0 +1,219 @@ +#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 2 +txna ApplicationArgs 2 +int 0 +getbyte +store 3 +load 2 +load 3 +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 1 +byte 0x151f7c75 +load 1 +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 0 +txn GroupIndex +int 1 +- +load 0 +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 5 +store 4 +load 4 +gtxns Sender +load 5 +txnas Accounts +== +assert +load 4 +gtxns Receiver +global CurrentApplicationAddress +== +assert +load 5 +txnas Accounts +byte "balance" +load 5 +txnas Accounts +byte "balance" +app_local_get +load 4 +gtxns Amount ++ +app_local_put +retsub + +// getBalance +getBalance_2: +txnas Accounts +byte "balance" +app_local_get +retsub + +// withdraw +withdraw_3: +store 7 +store 6 +txn Sender +byte "balance" +txn Sender +byte "balance" +app_local_get +load 6 +- +app_local_put +itxn_begin +int pay +itxn_field TypeEnum +load 7 +txnas Accounts +itxn_field Receiver +load 6 +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/router.py b/pyteal/ast/router.py index 666d4c2cc..731871a46 100644 --- a/pyteal/ast/router.py +++ b/pyteal/ast/router.py @@ -670,30 +670,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 and `self.name`. + A Python SDK `Contract` object constructed from the registered methods on this router. """ return sdk_abi.Contract(self.name, self.methods) 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(), @@ -709,16 +711,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/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 From cd5d21240a7015c6221d703f19e8b902a66db2f4 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 6 Jul 2022 11:59:42 -0700 Subject: [PATCH 17/29] Fix post-merge linter/test failures --- .flake8 | 1 + .../application/abi/algobank_approval.teal | 41 +++++++++++-------- pyteal/ast/abi/__init__.py | 2 - pyteal/ast/abi/tuple.py | 7 +++- 4 files changed, 30 insertions(+), 21 deletions(-) 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/examples/application/abi/algobank_approval.teal b/examples/application/abi/algobank_approval.teal index 42dee701f..be89cd0d3 100644 --- a/examples/application/abi/algobank_approval.teal +++ b/examples/application/abi/algobank_approval.teal @@ -27,13 +27,13 @@ int 0 assert txna ApplicationArgs 1 btoi -store 2 +store 3 txna ApplicationArgs 2 int 0 getbyte -store 3 -load 2 +store 4 load 3 +load 4 callsub withdraw_3 int 1 return @@ -50,9 +50,9 @@ txna ApplicationArgs 1 int 0 getbyte callsub getBalance_2 -store 1 +store 2 byte 0x151f7c75 -load 1 +load 2 itob concat log @@ -78,11 +78,18 @@ assert txna ApplicationArgs 1 int 0 getbyte -store 0 +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 @@ -160,27 +167,27 @@ retsub // deposit deposit_1: +store 6 store 5 -store 4 -load 4 -gtxns Sender load 5 +gtxns Sender +load 6 txnas Accounts == assert -load 4 +load 5 gtxns Receiver global CurrentApplicationAddress == assert -load 5 +load 6 txnas Accounts byte "balance" -load 5 +load 6 txnas Accounts byte "balance" app_local_get -load 4 +load 5 gtxns Amount + app_local_put @@ -195,23 +202,23 @@ retsub // withdraw withdraw_3: +store 8 store 7 -store 6 txn Sender byte "balance" txn Sender byte "balance" app_local_get -load 6 +load 7 - app_local_put itxn_begin int pay itxn_field TypeEnum -load 7 +load 8 txnas Accounts itxn_field Receiver -load 6 +load 7 itxn_field Amount int 0 itxn_field Fee diff --git a/pyteal/ast/abi/__init__.py b/pyteal/ast/abi/__init__.py index 711e6c998..e989f347b 100644 --- a/pyteal/ast/abi/__init__.py +++ b/pyteal/ast/abi/__init__.py @@ -43,8 +43,6 @@ AssetTypeSpec, Application, ApplicationTypeSpec, - ReferenceType, - ReferenceTypeSpec, ReferenceTypeSpecs, ) from pyteal.ast.abi.transaction import ( diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 1ab5c6987..d2eb3ab31 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 @@ -341,7 +342,7 @@ 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[BaseType]": + def __getitem__(self, index: int) -> "TupleElement[Any]": """Retrieve an element by its index in this Tuple. Indexes start at 0. @@ -352,7 +353,9 @@ def __getitem__(self, index: int) -> "TupleElement[BaseType]": this Tuple. Returns: - A TupleElement that corresponds to the element at the given index. This type is a ComputedValue. + 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}") From 96b2cbd277b18bd9e1da77c66caecdca7b9f10aa Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 6 Jul 2022 15:47:51 -0700 Subject: [PATCH 18/29] Add calling documentation --- docs/abi.rst | 23 ++++++++++++++++++++--- docs/api.rst | 9 +++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index b57a0e854..7d294f454 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -807,14 +807,31 @@ This example uses the :code:`Router.compile_program` method to create the approv Calling an ARC-4 Program -------------------------- -TODO: brief intro +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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: leave pointers to SDK/goal documentation about how to invoke ABI calls +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, and documentation for these `can be found here `_: + +* For bare app calls: + * For calls that create an app, :code:`goal app create` can be used to construct and send an app creation bare app call. + * For non-creation calls, :code:`goal app `, where :code:`` is one of :code:`call` (no-op), :code:`optin`, :code:`closeout`, :code:`clear`, :code:`update`, or :code:`delete` can be used to construct and send a non-creation bare app call. +* For all method calls: + * :code:`goal app method` 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: explain how this is possible but there is no simple way to do it in PyTeal yet; once it is, we should update this section +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 d5d8faf82..cb7643b79 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,6 +25,15 @@ PyTeal Package The most recently submitted inner transaction. This is an instance of :any:`TxnObject`. + If an 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: From e3c73123e6fd8e05dd53ca45b99fbe1eebb25486 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 6 Jul 2022 18:05:59 -0700 Subject: [PATCH 19/29] Partially address feedback --- docs/abi.rst | 264 ++++++++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 117 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 7d294f454..1ae89abfd 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -3,8 +3,6 @@ ABI Support =========== -TODO: brief description - .. 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. @@ -15,18 +13,24 @@ TODO: brief description * :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. +`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. + TODO: warning about how ABI types are intended to be used at the moment (for standard serialization at the incoming and outgoing layers of contract) - internal use may be extremely inefficient with ABI types TODO: warning about how internal representation of ABI types may change over time Types ------ -The ABI supports a variety of types whose encodings are standardized. +The ABI supports a variety of data types whose encodings are standardized. + +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 and 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 we introduce these ABI types, it's important to understand the fundamentals of how PyTeal's ABI type system works. +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` ^^^^^^^^^^^^^^^^^^^^ @@ -47,67 +51,74 @@ 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. +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. Due to this dependency, 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. +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**. +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 confidant 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. +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. With the Constructor """""""""""""""""""""" -The most obvious way is to use its constructor, like so: +The most straightforward way is to use its constructor, like so: .. code-block:: python - myUint8 = abi.Uint8() - myUint64 = abi.Uint64() - myArrayOf12Uint8s = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 12)) + 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, more complex 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. +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 """""""""""""""""""""""""""""""""""""" -You may remember that :code:`abi.TypeSpec` has a :any:`new_instance() ` method that can be used to instantiate ABI types. This is another way of instantiating ABI types, if you happen to have an :code:`abi.TypeSpec` instance available. For example: +Recall that :code:`abi.TypeSpec` has a :any:`new_instance() ` method which instantiates ABI types. This is another way of instantiating ABI types, if you have an :code:`abi.TypeSpec` instance available. For example: .. code-block:: python - myUintType = abi.Uint8TypeSpec() - myUint8 = myUintType.new_instance() + from pyteal import * - myArrayType = abi.StaticArrayTypeSpec(myUintType, 12) - myArrayOf12Uint8s = myArrayType.new_instance() + 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 :code:`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 complex type. +Using :code:`abi.TypeSpec.new_instance()` makes sense if you already have an instance of the right :code:`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 Python type annotation that describes the ABI type, and :code:`abi.make` will create an instance of it for you. For example: .. code-block:: python from typing import Literal + from pyteal import * - myUint8 = abi.make(abi.Uint8) - myUint64 = abi.make(abi.Uint64) - myArrayOf12Uint8s = abi.make(abi.StaticArray[abi.Uint8, Literal[12]]) + 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 ABI type directly from the operation, as usual. However, in PyTeal, there operations must actually return two things: +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 populated with the right value +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. @@ -121,12 +132,15 @@ The :any:`abi.ComputedValue[T] ` abstract base class provides * :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 is recommended to only issue a single call to either of these two method. + 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 @@ -155,45 +169,47 @@ A brief example is below: Assert(actual_sum.load() == expected_sum), ) -Categories -~~~~~~~~~~ +Type Categories +~~~~~~~~~~~~~~~~~~~~ There are three categories of ABI types: -1. Basic types -2. Reference types -3. Transaction 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 side effects, in contrast to the other categories of 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 if :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 if all elements are static A tuple of multiple 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 :code:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :code:`abi.Tuple` for fixed amounts of generic arguments: + \*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)` @@ -228,24 +244,28 @@ A brief example is below. Please consult the documentation linked above for each .. code-block:: python - myAddress = abi.make(abi.Address) - myBool = abi.make(abi.Bool) - myUint64 = abi.make(abi.Uint64) - myTuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) + 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( - myAddress.set(Txn.sender()), - myBool.set(Txn.fee() == Int(0)), - myUint64.set(5000), - myTuple.set(myAddress, myBool, myUint64) + 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 Values +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 :code:`abi.Uint` classes and :code:`abi.Byte` +* :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()` @@ -253,6 +273,8 @@ All basic types that represent a single value have a :code:`get()` method, which 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: @@ -266,20 +288,24 @@ A brief example is below. Please consult the documentation linked above for each Getting Values at Indexes '''''''''''''''''''''''''' -The types :code:`abi.StaticArray`, :code:`abi.Address`, :code:`abi.DynamicArray`, :code:`abi.String`, and :code:`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 extract these values. +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 :code:`abi.StaticArray` and :code:`abi.Address` -* :any:`abi.Array.__getitem__(index: int | Expr) `, used for :code:`abi.DynamicArray` and :code:`abi.String` +* :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) ` -Be aware that these methods return a :code:`ComputedValue`, TODO link to Computed Value section +.. 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.""" @@ -295,24 +321,26 @@ Limitations TODO: explain type size limitations +.. _Reference Types: + Reference Types ^^^^^^^^^^^^^^^^^^^^^^^^ -Some AVM operations require specific values to be placed in the "foreign arrays" of the app call transaction. Reference types allow methods to describe these requirements. +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. -Reference types are only valid in the arguments of a method. They may not appear in a method's return value in any form. +Reference Types are only valid in the arguments of a method. They may not appear in a method's return value. Definitions """""""""""""""""""""""""""""""""""""""""" -PyTeal supports the following reference types: +PyTeal supports the following Reference Types: ====================== ====================== ================ ======================================================================================================================================================= PyTeal Type ARC-4 Type Dynamic / Static Description ====================== ====================== ================ ======================================================================================================================================================= -:any:`abi.Account` :code:`account` Static Represents an additional account that the current transaction can access, stored in the :any:`Txn.accounts ` array -:any:`abi.Asset` :code:`asset` Static Represents an additional asset that the current transaction can access, stored in the :any:`Txn.assets ` array -:any:`abi.Application` :code:`application` Static Represents an additional application that the current transaction can access, stored in the :any:`Txn.applications ` array +: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`. @@ -320,34 +348,10 @@ These types all inherit from the abstract class :any:`abi.ReferenceType`. Usage """""""""""""""""""""""""""""""""""""""""" -Getting Referenced Indexes -'''''''''''''''''''''''''' - -Because reference types represent values placed into one of the transaction's foreign arrays, each reference type value is associated with a specific index into the appropriate array. - -All reference types implement the method :any:`abi.ReferenceType.referenced_index()` which can be used to access this index. - -A brief example is below: - -.. code-block:: python - - @Subroutine(TealType.none) - def referenced_index_example( - account: abi.Account, asset: abi.Asset, app: abi.Application - ) -> Expr: - return Seq( - # The accounts array has Txn.accounts.length() + 1 elements in it (the +1 is the txn sender) - Assert(account.referenced_index() <= Txn.accounts.length()), - # The assets array has Txn.assets.length() elements in it - Assert(asset.referenced_index() < Txn.assets.length()), - # The applications array has Txn.applications.length() + 1 elements in it (the +1 is the current app) - Assert(app.referenced_index() <= Txn.applications.length()), - ) - Getting Referenced Values '''''''''''''''''''''''''' -Perhaps more important than the index of a referenced type is its value. Depending on the reference type, there are different methods available to obtain the value being referenced: +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()` @@ -357,6 +361,8 @@ 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 @@ -386,7 +392,7 @@ A brief example is below: 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: +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` @@ -398,6 +404,8 @@ 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 @@ -431,6 +439,8 @@ 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( @@ -448,29 +458,31 @@ Limitations TODO: explain limitations, such as can't be created directly, or used as method return value +.. _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. +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, unique, transaction that must appear immediately before the application call. +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 supports the following Transaction Types: -=================================== ====================== ================ ======================================================================================================================================================= -PyTeal Type ARC-4 Type Dynamic / Static Description -=================================== ====================== ================ ======================================================================================================================================================= -:any:`abi.Transaction` :code:`txn` Static A catch-all for any transaction type -: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.AssetTransferTransaction` :code:`appl` Static An application call transaction -=================================== ====================== ================ ======================================================================================================================================================= +===================================== ====================== ================ ======================================================================================================================================================= +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 +===================================== ====================== ================ ======================================================================================================================================================= Usage """""""""""""""""""""""""""""""""""""""""" @@ -478,12 +490,14 @@ 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. +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, @@ -499,16 +513,18 @@ A brief example is below: 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. +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. + """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()), @@ -529,21 +545,23 @@ TODO: explain limitations, such as can't be created directly, used as method ret Subroutines with ABI Types -------------------------- -Subroutines can be created that accept ABI types are 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. +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 normal :any:`@Subroutine ` decorator can be used. +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, 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 affect on the code PyTeal generates. +To indicate the type of each argument, 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 @@ -554,12 +572,16 @@ Notice that this subroutine accepts the following arguments, not all of which ar * :code:`length`: an ABI :any:`abi.Uint16` type * :code:`width`: an ABI :any:`abi.Uint64` type -* :code:`height`: a PyTeal expression 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. -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. For example: +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) @@ -582,10 +604,12 @@ In addition to accepting ABI types as arguments, it's also possible for a subrou As mentioned in the Computed Value section (TODO: link), 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 new 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: +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] @@ -655,6 +679,8 @@ 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()) @@ -701,6 +727,8 @@ The first way to register a method is with the :any:`Router.add_method_handler` .. code-block:: python + from pyteal import * + router = Router( name="Calculator", bare_calls=BareCallActions( @@ -751,6 +779,8 @@ The second way to register a method is with the :any:`Router.method` decorator p .. code-block:: python + from pyteal import * + my_router = Router( name="Calculator", bare_calls=BareCallActions( From fe5d68ba242f50c6d3f4f7b84fce27dd7021fdcd Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 18 Jul 2022 14:06:10 -0700 Subject: [PATCH 20/29] Respond to feedback --- docs/abi.rst | 115 ++++++++++++++++++++++++----------------- docs/api.rst | 2 +- pyteal/ast/abi/uint.py | 4 +- 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 1ae89abfd..617c7ef3d 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -25,7 +25,8 @@ Types The ABI supports a variety of data types whose encodings are standardized. -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 and computation logic, as the more basic :any:`TealType.uint64` and :any:`TealType.bytes` :any:`Expr` types are far more efficient for these purposes. +.. 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 ~~~~~~~~~~~~ @@ -55,13 +56,16 @@ Static types are defined as types whose encoded length does not depend on the va 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 confidant your app has enough computation budget and storage space when using 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 """""""""""""""""""""" @@ -80,7 +84,7 @@ For simple types, using the constructor is straightforward and works as you woul With an :code:`abi.TypeSpec` Instance """""""""""""""""""""""""""""""""""""" -Recall that :code:`abi.TypeSpec` has a :any:`new_instance() ` method which instantiates ABI types. This is another way of instantiating ABI types, if you have an :code:`abi.TypeSpec` instance available. For example: +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 @@ -95,9 +99,9 @@ Recall that :code:`abi.TypeSpec` has a :any:`new_instance() `__ 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 @@ -169,6 +173,8 @@ A brief example is below: Assert(actual_sum.load() == expected_sum), ) +.. _Type Categories: + Type Categories ~~~~~~~~~~~~~~~~~~~~ @@ -423,7 +429,7 @@ A brief example is below: ) .. note:: - All returned parameters are instances of :any:`MaybeValue`. + All returned parameters are instances of :any:`MaybeValue`, which is why the :any:`outputReducer(...) ` method is used. Accessing Asset Holdings '''''''''''''''''''''''' @@ -554,7 +560,7 @@ 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, 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. +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: @@ -602,7 +608,7 @@ Subroutines that Return ABI Types 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 Computed Value section (TODO: link), 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. +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: @@ -632,42 +638,42 @@ The :code:`@ABIReturnSubroutine` decorator should be used with subroutines that 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, in which case a :code:`ComputedValue` is unnecessary, so the subroutine will still return an :code:`Expr` to the caller. In this case the :code:`@ABIReturnSubroutine` decorator acts identically the original :code:`@Subroutine` decorator. +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 with the ABI Router +Creating an ARC-4 Program ---------------------------------------------------- -.. warning:: - :any:`Router` usage is still taking shape and is subject to backwards incompatible changes. +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: - Feel encouraged to use :any:`Router` and expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. +* **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. -An ARC-4 ABI compatible program can be called in up to two ways: +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. -* Through a method call, which chooses a specific method implemented by the contract and calls it with the appropriate arguments. -* Through a bare app call, which has no arguments and no return value. +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. -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. +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. -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. +The following sections explain how to register bare app calls and methods with the :any:`Router` class. -To make it easier for an application to route between 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. +.. warning:: + :any:`Router` usage is still taking shape and is subject to backwards incompatible changes. -The following sections explain how to register bare app calls and methods with the :code:`Router` class. + Feel encouraged to use :any:`Router` and expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. 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 -#. Opt in -#. Close out -#. Clear state -#. Update application -#. Delete application +#. **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, during a non-creation app call, or during either. +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. @@ -694,15 +700,25 @@ A brief example is below: 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 ), @@ -712,18 +728,17 @@ A brief example is below: ), ) -TODO: explain example - -When deciding which :code:`CallConfig` value is appropriate for a bare app call or method, ask yourself the question, should should it be valid for someone to create my app by calling this method? +.. 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TODO: warning about input type validity -- no verification is done for you (right now) -Method can be registered in two ways. +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 :code:`@ABIReturnSubroutine`. An example of this is below: +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 @@ -732,9 +747,9 @@ The first way to register a method is with the :any:`Router.add_method_handler` router = Router( name="Calculator", bare_calls=BareCallActions( - # allow this app to be created with a NoOp call + # 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 + # 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), ), @@ -748,7 +763,6 @@ The first way to register a method is with the :any:`Router.add_method_handler` """ return output.set(a.get() + b.get()) - router.add_method_handler(add) @ABIReturnSubroutine def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: @@ -763,7 +777,13 @@ The first way to register a method is with the :any:`Router.add_method_handler` # 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), @@ -771,7 +791,7 @@ The first way to register a method is with the :any:`Router.add_method_handler` 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 only be callable with a transaction that is not an app creation and whose on completion value is :code:`OnComplete.NoOp`. +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`. @@ -784,9 +804,9 @@ The second way to register a method is with the :any:`Router.method` decorator p my_router = Router( name="Calculator", bare_calls=BareCallActions( - # allow this app to be created with a NoOp call + # 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 + # 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), ), @@ -818,13 +838,16 @@ The second way to register a method is with the :any:`Router.method` decorator p Compiling a Router Program ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that we know how to add bare app call and method call handlers to a :code:`Router`, the next step is to compile the :code:`Router` into TEAL code. +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. -In addition to receiving the approval and clear state programs, the :code:`Router.compile_program` method also returns a :code:`Contract` object `from the Python SDK `_. This object represents an `ARC-4 Contract Description `_, which can be distributed to clients to enable them to call the methods on the contract. +.. 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 :code:`Router` class: +Here's an example of a complete application that uses the :any:`Router` class: .. literalinclude:: ../examples/application/abi/algobank.py :language: python @@ -848,13 +871,13 @@ Off-chain systems can use the `Algorand SDKs `_. -The :code:`goal` CLI has subcommands for creating and submitting various types of transactions. The relevant ones for ABI-compliant contracts are mentioned below, and documentation for these `can be found here `_: +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` can be used to construct and send an app creation bare app call. - * For non-creation calls, :code:`goal app `, where :code:`` is one of :code:`call` (no-op), :code:`optin`, :code:`closeout`, :code:`clear`, :code:`update`, or :code:`delete` can be used to construct and send a non-creation bare app call. + * 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` 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. + * :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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -864,4 +887,4 @@ Algorand applications can issue `inner transactions ` 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. + 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 cb7643b79..14e5b4666 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,7 +25,7 @@ PyTeal Package The most recently submitted inner transaction. This is an instance of :any:`TxnObject`. - If an transaction group was submitted most recently, then this will be the last transaction in that group. + If a transaction group was submitted most recently, then this will be the last transaction in that group. .. data:: Gitxn :annotation: = diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py index 091f9e529..7dddcba2e 100644 --- a/pyteal/ast/abi/uint.py +++ b/pyteal/ast/abi/uint.py @@ -251,8 +251,8 @@ def set(self, value: Union[int, Expr, "Uint", ComputedValue["Uint"]]) -> Expr: 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 does not fit in 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 does not fit in this integer 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. From 7aabc2ec3d256f00e687db8496a0aeeee6c2031d Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 18 Jul 2022 16:15:38 -0700 Subject: [PATCH 21/29] Resolve TODOs --- docs/abi.rst | 69 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 617c7ef3d..177f575f6 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -17,9 +17,6 @@ ABI Support This page will introduce and explain the relevant concepts necessary to build a PyTeal application that adheres to the ARC-4 ABI standards. -TODO: warning about how ABI types are intended to be used at the moment (for standard serialization at the incoming and outgoing layers of contract) - internal use may be extremely inefficient with ABI types -TODO: warning about how internal representation of ABI types may change over time - Types ------ @@ -230,6 +227,13 @@ These ARC-4 types are not yet supported in PyTeal: * 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. Do not attempt to store more elements than this in these types. + +Static container types have no such limit. + Usage """"""""""""""""""""" @@ -322,19 +326,12 @@ A brief example is below. Please consult the documentation linked above for each array[i.load()].use(lambda value: Assert(value.get() > Int(5))) ) -Limitations -""""""""""""""""""""" - -TODO: explain type size limitations - .. _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. - -Reference Types are only valid in the arguments of a method. They may not appear in a method's return value. +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 """""""""""""""""""""""""""""""""""""""""" @@ -351,6 +348,22 @@ PyTeal Type ARC-4 Type Dynamic / Static Description 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 + +Because of this, methods that have a large amount of Reference Type arguments may be impossible to call as intended at runtime. + Usage """""""""""""""""""""""""""""""""""""""""" @@ -459,17 +472,12 @@ A brief example is below: .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))), ) -Limitations -"""""""""""""""""""""""""""""""""""""""""" - -TODO: explain limitations, such as can't be created directly, or used as method return value - .. _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". +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. @@ -490,6 +498,15 @@ PyTeal Type ARC-4 Type Dynamic / Static De :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 """""""""""""""""""""""""""""""""""""""""" @@ -542,11 +559,6 @@ A brief example is below: ), ) -Limitations -"""""""""""""""""""""""""""""""""""""""""" - -TODO: explain limitations, such as can't be created directly, used as method return value, or embedded in other types - Subroutines with ABI Types -------------------------- @@ -734,7 +746,18 @@ A brief example is below: Registering Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: warning about input type validity -- no verification is done for you (right now) +.. warning:: + A challenge for any system exposed to the public, including smart contracts, is input validation. At the moment, PyTeal's :any:`Router` class only performs a limited set of input validation. The largest hole in this validation is compound types, i.e. :any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, and :any:`abi.String`. + + The :any:`Router` class performs no input validation against compound types when they are decoded as method inputs; however, if an invalid encoding is given to a method, an error may occur when the program accesses an element contained in the compound type value. + + This means that methods will not fail immediately when given invalid inputs for compound types. Rather, they will fail when elements are extracted from the invalid value. Depending on the nature of the invalid encoding, only some elements may produce an error, while others are able to be accessed without issue. + + For these reasons, **we strongly recommend** that methods which take compound types as inputs do not delay accessing the elements from these inputs. In other words, if your method takes a compound type argument, do not persist the argument to state and access elements from it in later transactions that you did not access in the method call it was introduced. This is because it's possible some elements may produce an error, and if so, you want that error to happen in the method call that introduces the value, since it will prevent the value from propagating further into your app's state. + + Note that the above advice applies recursively to compound types contained in other compound types as well. + + Also note that as a result of the limited input validation of compound types, :any:`abi.Address` is not guaranteed to have exactly 32 bytes. For many uses this does not matter, since AVM opcodes that expect addresses will validate the address length. However, if your app intends to persist an address to state and not immediately call one of these methods, we recommend manually verifying its length is 32 bytes. :any:`abi.Account` does not suffer from this issue. There are two ways to register a method with the :any:`Router` class. From 13a81abcd7ad4e0771c01de9fd778b0c05e902fa Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 18 Jul 2022 16:27:01 -0700 Subject: [PATCH 22/29] Add pragma references --- pyteal/ast/router.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py index 5ab6a016f..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__( From bf2c233eb5459dc260d38db78a20a0ec64bbe434 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 18 Jul 2022 16:32:34 -0700 Subject: [PATCH 23/29] didn't make it into the previous commit --- docs/abi.rst | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 177f575f6..0b18efb78 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -4,14 +4,16 @@ 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. + 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:`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. + * :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. `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. @@ -613,10 +615,12 @@ Subroutines that Return ABI Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. warning:: - :any:`ABIReturnSubroutine` is still taking shape and is subject to backwards incompatible changes. + :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 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. In addition to accepting ABI types as arguments, it's also possible for a subroutine to return an ABI type value. @@ -669,9 +673,11 @@ To make it easier for an application to route across the many bare app calls and 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. + :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. - 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. Registering Bare App Calls ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 7eae32c284e0ceb41db9acd0e4e65713252ec4b8 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 18 Jul 2022 16:34:31 -0700 Subject: [PATCH 24/29] Fix Bool.__module__ --- pyteal/ast/abi/bool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index 9552b1d40..75a41a8d6 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -113,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: From e10f84fa465b3bc14037de6ed5a66bd8563811be Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 19 Jul 2022 09:00:57 -0700 Subject: [PATCH 25/29] Mention stack size limit --- docs/abi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/abi.rst b/docs/abi.rst index 0b18efb78..354d945e2 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -232,7 +232,7 @@ These ARC-4 types are not yet supported in PyTeal: 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. Do not attempt to store more elements than this in these types. +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. From fdbbc9639a3d84a59f97f768fe8a80611dc2eba6 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 19 Jul 2022 14:49:07 -0700 Subject: [PATCH 26/29] More pragma documentation --- docs/abi.rst | 6 +++--- docs/versions.rst | 36 ++++++++++++++++++++++++++++-------- pyteal/ast/pragma.py | 13 +++++++++++++ pyteal/pragma/pragma.py | 8 ++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index 354d945e2..c9eb20c26 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -13,7 +13,7 @@ ABI Support * :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. + 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. @@ -620,7 +620,7 @@ Subroutines that Return ABI Types * 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. + 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. @@ -677,7 +677,7 @@ The following sections explain how to register bare app calls and methods with t 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. + 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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/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/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)) From f49ec24e2e867260d261b25f14c295ff378812e2 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 19 Jul 2022 15:04:50 -0700 Subject: [PATCH 27/29] Address other feedback --- docs/abi.rst | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/abi.rst b/docs/abi.rst index c9eb20c26..cd9a76bcb 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -297,8 +297,8 @@ A brief example is below. Please consult the documentation linked above for each .Else(b.get()) ) -Getting Values at Indexes -'''''''''''''''''''''''''' +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. @@ -357,7 +357,7 @@ Because References Types have a special meaning, they should not be directly cre 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: +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 @@ -753,17 +753,14 @@ Registering Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. warning:: - A challenge for any system exposed to the public, including smart contracts, is input validation. At the moment, PyTeal's :any:`Router` class only performs a limited set of input validation. The largest hole in this validation is compound types, i.e. :any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, and :any:`abi.String`. - - The :any:`Router` class performs no input validation against compound types when they are decoded as method inputs; however, if an invalid encoding is given to a method, an error may occur when the program accesses an element contained in the compound type value. + 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`). - This means that methods will not fail immediately when given invalid inputs for compound types. Rather, they will fail when elements are extracted from the invalid value. Depending on the nature of the invalid encoding, only some elements may produce an error, while others are able to be accessed without issue. + **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. - For these reasons, **we strongly recommend** that methods which take compound types as inputs do not delay accessing the elements from these inputs. In other words, if your method takes a compound type argument, do not persist the argument to state and access elements from it in later transactions that you did not access in the method call it was introduced. This is because it's possible some elements may produce an error, and if so, you want that error to happen in the method call that introduces the value, since it will prevent the value from propagating further into your app's state. - - Note that the above advice applies recursively to compound types contained in other compound types as well. - - Also note that as a result of the limited input validation of compound types, :any:`abi.Address` is not guaranteed to have exactly 32 bytes. For many uses this does not matter, since AVM opcodes that expect addresses will validate the address length. However, if your app intends to persist an address to state and not immediately call one of these methods, we recommend manually verifying its length is 32 bytes. :any:`abi.Account` does not suffer from this issue. + 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. From bf85959d80b057715e2f9093b6d2be33c607a62e Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 20 Jul 2022 10:11:50 -0700 Subject: [PATCH 28/29] Warn about reference type limits --- docs/abi.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/abi.rst b/docs/abi.rst index cd9a76bcb..ce4a6bdc7 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -364,7 +364,8 @@ Note that the AVM has `limitations on the maximum number of foreign references < * Applications: 8 * Sum of Accounts, Assets, and Applications: 8 -Because of this, methods that have a large amount of Reference Type arguments may be impossible to call as intended at runtime. +.. 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 """""""""""""""""""""""""""""""""""""""""" From 44ea401850159655bf6b87ab5b20f0b32a081e43 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 20 Jul 2022 11:04:20 -0700 Subject: [PATCH 29/29] **cannot** --- docs/abi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/abi.rst b/docs/abi.rst index ce4a6bdc7..9186b0ee1 100644 --- a/docs/abi.rst +++ b/docs/abi.rst @@ -504,7 +504,7 @@ PyTeal Type ARC-4 Type Dynamic / Static De 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. +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.