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.

    • ABIReturnSubroutine usage for ABI Application entry point definition.

    • Router usage for defining how to route program invocations.

  • For general purpose 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 pragma or the Pragma expression to pin the version of PyTeal in your source code. See Version Pragmas for more information.

ARC-4 introduces a set of standards that increase the interoperability of smart contracts in the Algorand ecosystem. This set of standards is commonly referred to as Algorand’s application binary interface, or ABI.

This page will introduce and explain the relevant concepts necessary to build a PyTeal application that adheres to the ARC-4 ABI standards.

Types

The ABI supports a variety of data types whose encodings are standardized.

Note

Be aware that the ABI type system in PyTeal has been designed specifically for the limited use case of describing a program’s inputs and outputs. At the time of writing, we do not recommend using ABI types in a program’s internal storage or computation logic, as the more basic TealType.uint64 and TealType.bytes Expr types are far more efficient for these purposes.

Fundamentals

Before diving into the specific ABI types, let us first explain the the fundamentals of PyTeal’s ABI type system, which includes behavior common to all ABI types.

abi.BaseType

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:

abi.TypeSpec

abi.TypeSpec is an abstract base class used to describe ABI types. Every child class of abi.BaseType also has a companion abi.TypeSpec child class. The 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 abi.TypeSpec.new_instance(), which creates and returns a new abi.BaseType instance of the ABI type it represents.

Static vs Dynamic Types

An important property of an ABI type is whether it is static or dynamic.

Static types are defined as types whose encoded length does not depend on the value of that type. This property allows encoding and decoding of static types to be more efficient. For example, the encoding of a boolean type will always have a fixed length, regardless of whether the value is true or false.

Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. For example, it’s not possible to know the encoding size of a variable-sized string type without also knowing its value. Due to this dependency on values, the code that PyTeal generates to encode, decode, and manipulate dynamic types is more complex and generally less efficient than the code needed for static types.

Because of the difference in complexity and efficiency when working with static and dynamic types, we strongly recommend using static types over dynamic types whenever possible. Using static types generally makes your program’s resource usage more predictable as well, so you can be more confident your app has enough computation budget and storage space when using static types.

Instantiating Types

There are a few ways to create an instance of an ABI type. Each method produces the same result, but some may be more convenient than others in certain situations.

Note

The following examples reference specific ABI types, which will be introduced in the Type Categories section.

With the Constructor

The most straightforward way is to use its constructor, like so:

from pyteal import *

my_uint8 = abi.Uint8()
my_uint64 = abi.Uint64()
my_array = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 12))

For simple types, using the constructor is straightforward and works as you would expect. However, compound types like abi.StaticArray have type-level arguments, so their constructor must take an 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 abi.TypeSpec Instance

Recall that abi.TypeSpec has a new_instance() method which instantiates ABI types. This is another way of instantiating ABI types, if you have an abi.TypeSpec instance available. For example:

from pyteal import *

my_uint_type = abi.Uint8TypeSpec()
my_uint = my_uint_type.new_instance()

my_array_type = abi.StaticArrayTypeSpec(my_uint_type, 12)
my_array = my_array_type.new_instance()
With abi.make

Using abi.TypeSpec.new_instance() makes sense if you already have an instance of the right abi.TypeSpec, but otherwise it’s not much better than using the constructor. Because of this, we have the abi.make method, which is perhaps the most convenient way to create a compound type.

To use it, you pass in a PEP 484 Python type annotation that describes the ABI type, and abi.make will create an instance of it for you. For example:

from typing import Literal
from pyteal import *

my_uint8 = abi.make(abi.Uint8)
my_uint64 = abi.make(abi.Uint64)
my_array = abi.make(abi.StaticArray[abi.Uint8, Literal[12]])

Note

Since Python does not allow integers to be directly embedded in type annotations, you must wrap any integer arguments in the Literal annotation from the 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 type directly from the operation. However, in PyTeal, these operations must actually return two values:

  1. An instance of the ABI type that will be populated with the right value

  2. An 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 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 abi.ComputedValue instance parameterized by the return type.

For example, the abi.Tuple.__getitem__ function does not return an abi.BaseType; instead, it returns an abi.TupleElement[abi.BaseType] instance, which inherits from abi.ComputedValue[abi.BaseType].

The abi.ComputedValue[T] abstract base class provides the following methods:

Note

If you call the methods store_into(...) or 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:

from typing import Literal as L
from pyteal import *

@Subroutine(TealType.none)
def assert_sum_equals(
    array: abi.StaticArray[abi.Uint64, L[10]], expected_sum: Expr
) -> Expr:
    """This subroutine asserts that the sum of the elements in `array` equals `expected_sum`"""
    i = ScratchVar(TealType.uint64)
    actual_sum = ScratchVar(TealType.uint64)
    tmp_value = abi.Uint64()
    return Seq(
        For(i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1))).Do(
            If(i.load() <= Int(5))
            # Both branches of this If statement are equivalent
            .Then(
                # This branch showcases how to use `store_into`
                Seq(
                    array[i.load()].store_into(tmp_value),
                    actual_sum.store(actual_sum.load() + tmp_value.get()),
                )
            ).Else(
                # This branch showcases how to use `use`
                array[i.load()].use(
                    lambda value: actual_sum.store(actual_sum.load() + value.get())
                )
            )
        ),
        Assert(actual_sum.load() == expected_sum),
    )

Type Categories

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

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

abi.Uint8

uint8

Static

An 8-bit unsigned integer

abi.Uint16

uint16

Static

A 16-bit unsigned integer

abi.Uint32

uint32

Static

A 32-bit unsigned integer

abi.Uint64

uint64

Static

A 64-bit unsigned integer

abi.Bool

bool

Static

A boolean value that can be either 0 or 1

abi.Byte

byte

Static

An 8-bit unsigned integer. This is an alias for abi.Uint8 that should be used to indicate non-numeric data, such as binary arrays.

abi.StaticArray[T,N]

T[N]

Static when T is static

A fixed-length array of T with N elements

abi.Address

address

Static

A 32-byte Algorand address. This is an alias for abi.StaticArray[abi.Byte, Literal[32]].

abi.DynamicArray[T]

T[]

Dynamic

A variable-length array of T

abi.String

string

Dynamic

A variable-length byte array assumed to contain UTF-8 encoded content. This is an alias for abi.DynamicArray[abi.Byte].

abi.Tuple*, abi.NamedTuple

(...)

Static when all elements are static

A tuple of multiple types

Note

*A proper implementation of 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 abi.Tuple a generic type. As a workaround, we have introduced the following subclasses of abi.Tuple for tuples containing up to 5 generic arguments:

While we are still on PyTeal 3.10, we have a workaround for abi.Tuple by abi.NamedTuple, which allows one to define a tuple with more than 5 generic arguments, and access tuple elements by field name. For example:

from pyteal import *
from typing import Literal as L

class InheritedFromNamedTuple(abi.NamedTuple):
    acct_address: abi.Field[abi.Address]
    amount: abi.Field[abi.Uint64]
    retrivable: abi.Field[abi.Bool]
    desc: abi.Field[abi.String]
    list_of_addrs: abi.Field[abi.DynamicArray[abi.Address]]
    balance_list: abi.Field[abi.StaticArray[abi.Uint64, L[10]]]

These ARC-4 types are not yet supported in PyTeal:

  • Non-power-of-2 unsigned integers under 64 bits, i.e. uint24, uint48, uint56

  • Unsigned integers larger than 64 bits

  • Fixed point unsigned integers, i.e. ufixed<N>x<M>

Limitations

Due to the nature of their encoding, dynamic container types, i.e. abi.DynamicArray[T] and abi.String, have an implicit limit on the number of elements they may contain. This limit is 2^16 - 1, or 65535. However, the AVM has a stack size limit of 4096 for byte strings, so it’s unlikely this encoding limit will be reached by your program.

Static container types have no such limit.

Usage
Setting Values

All basic types have a 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:

A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior.

from pyteal import *

my_address = abi.make(abi.Address)
my_bool = abi.make(abi.Bool)
my_uint64 = abi.make(abi.Uint64)
my_tuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64])

program = Seq(
    my_address.set(Txn.sender()),
    my_bool.set(Txn.fee() == Int(0)),
    # It's ok to set an abi.Uint to a Python integer. This is actually preferred since PyTeal
    # can determine at compile-time that the value will fit in the integer type.
    my_uint64.set(5000),
    my_tuple.set(my_address, my_bool, my_uint64)
)
Getting Single Values

All basic types that represent a single value have a get() method, which can be used to extract that value. The supported types and methods are:

A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior.

from pyteal import *

@Subroutine(TealType.uint64)
def minimum(a: abi.Uint64, b: abi.Uint64) -> Expr:
    """Return the minimum value of the two arguments."""
    return (
        If(a.get() < b.get())
        .Then(a.get())
        .Else(b.get())
    )
Getting Values at Indexes - Compound Types

The types abi.StaticArray, abi.Address, abi.DynamicArray, abi.String, and abi.Tuple are compound types, meaning they contain other types whose values can be extracted. The __getitem__ method, accessible by using square brackets to “index into” an object, can be used to access these values.

The supported methods are:

Note

Be aware that these methods return a ComputedValue, similar to other PyTeal operations which return ABI types. More information about why that is necessary and how to use a ComputedValue can be found in the Computed Values section.

Note

*For abi.NamedTuple, one can access tuple elements through both methods

A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior.

from typing import Literal as L
from pyteal import *

@Subroutine(TealType.none)
def ensure_all_values_greater_than_5(array: abi.StaticArray[abi.Uint64, L[10]]) -> Expr:
    """This subroutine asserts that every value in the input array is greater than 5."""
    i = ScratchVar(TealType.uint64)
    return For(
        i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1))
    ).Do(
        array[i.load()].use(lambda value: Assert(value.get() > Int(5)))
    )

Reference Types

Many applications require the caller to provide “foreign array” values when calling the app. These are the blockchain entities (such as accounts, assets, or other applications) that the application will interact with when executing this call. In the ABI, we have Reference Types to describe these requirements.

Definitions

PyTeal supports the following Reference Types:

PyTeal Type

ARC-4 Type

Dynamic / Static

Description

abi.Account

account

Static

Represents an account that the current transaction can access, stored in the Txn.accounts array

abi.Asset

asset

Static

Represents an asset that the current transaction can access, stored in the Txn.assets array

abi.Application

application

Static

Represents an application that the current transaction can access, stored in the Txn.applications array

These types all inherit from the abstract class abi.ReferenceType.

Limitations

Because References Types have a special meaning, they should not be directly created, and they cannot be assigned a value by a program.

Additionally, Reference Types are only valid in the arguments of a method. They may not appear in a method’s return value.

Note that the AVM has limitations on the maximum number of foreign references an application call transaction may contain. At the time of writing, these limits are:

  • Accounts: 4

  • Assets: 8

  • Applications: 8

  • Sum of Accounts, Assets, and Applications: 8

Warning

Because of these limits, methods that have a large amount of Reference Type arguments may be impossible to call as intended at runtime.

Usage
Getting Referenced Values

Depending on the Reference Type, there are different methods available to obtain the value being referenced:

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def send_inner_txns(
    receiver: abi.Account, asset_to_transfer: abi.Asset, app_to_call: abi.Application
) -> Expr:
    return Seq(
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.AssetTransfer,
                TxnField.receiver: receiver.address(),
                TxnField.xfer_asset: asset_to_transfer.asset_id(),
                TxnField.amount: Int(1_000_000),
            }
        ),
        InnerTxnBuilder.Submit(),
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.ApplicationCall,
                TxnField.application_id: app_to_call.application_id(),
                Txn.application_args: [Bytes("hello")],
            }
        ),
        InnerTxnBuilder.Submit(),
    )
Accessing Parameters of Referenced Values

Reference Types allow the program to access more information about them. Each Reference Type has a params() method which can be used to access that object’s parameters. These methods are listed below:

These method are provided for convenience. They expose the same properties accessible from the AccountParam, AssetParam, and AppParam classes.

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def referenced_params_example(
    account: abi.Account, asset: abi.Asset, app: abi.Application
) -> Expr:
    return Seq(
        account.params().auth_address().outputReducer(
            lambda value, has_value: Assert(And(has_value, value == Global.zero_address()))
        ),
        asset.params().total().outputReducer(
            lambda value, has_value: Assert(And(has_value, value == Int(1)))
        ),
        app.params().creator_address().outputReducer(
            lambda value, has_value: Assert(And(has_value, value == Txn.sender()))
        )
    )

Note

All returned parameters are instances of MaybeValue, which is why the outputReducer(...) method is used.

Accessing Asset Holdings

Similar to the parameters above, asset holding properties can be accessed using one of the following methods:

These method are provided for convenience. They expose the same properties accessible from the AssetHolding class.

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def ensure_asset_balance_is_nonzero(account: abi.Account, asset: abi.Asset) -> Expr:
    return Seq(
        account.asset_holding(asset)
        .balance()
        .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))),
        # this check is equivalent
        asset.holding(account)
        .balance()
        .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))),
    )

Transaction Types

Some application calls require that they are invoked as part of a larger transaction group containing specific additional transactions. In order to express these types of calls, the ABI has Transaction Types.

Every Transaction Type argument represents a specific and unique transaction that must appear immediately before the application call in the same transaction group. A method may have multiple Transaction Type arguments, in which case they must appear in the same order as the method’s arguments immediately before the method application call.

Definitions

PyTeal supports the following Transaction Types:

PyTeal Type

ARC-4 Type

Dynamic / Static

Description

abi.Transaction

txn

Static

A catch-all for any type of transaction

abi.PaymentTransaction

pay

Static

A payment transaction

abi.KeyRegisterTransaction

keyreg

Static

A key registration transaction

abi.AssetConfigTransaction

acfg

Static

An asset configuration transaction

abi.AssetTransferTransaction

axfer

Static

An asset transfer transaction

abi.AssetFreezeTransaction

afrz

Static

An asset freeze transaction

abi.ApplicationCallTransaction

appl

Static

An application call transaction

Limitations

Due to the special meaning of Transaction Types, they cannot be used as the return value of a method. They can be used as method arguments, but only at the top-level. This means that it’s not possible to embed a Transaction Type inside a tuple or array.

Transaction Types should not be directly created, and they cannot be modified by a program.

Because the AVM has a maximum of 16 transactions in a single group, at most 15 Transaction Types may be used in the arguments of a method.

Usage
Getting the Transaction Group Index

All Transaction Types implement the abi.Transaction.index() method, which returns the absolute index of that transaction in the group.

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def handle_txn_args(
    any_txn: abi.Transaction,
    pay: abi.PaymentTransaction,
    axfer: abi.AssetTransferTransaction,
) -> Expr:
    return Seq(
        Assert(any_txn.index() == Txn.group_index() - Int(3)),
        Assert(pay.index() == Txn.group_index() - Int(2)),
        Assert(axfer.index() == Txn.group_index() - Int(1)),
    )
Accessing Transaction Fields

All Transaction Types implement the abi.Transaction.get() method, which returns a TxnObject instance that can be used to access fields from that transaction.

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:
    """This method receives a payment from an account opted into this app
    and records it in their local state.
    """
    return Seq(
        Assert(payment.get().sender() == sender.address()),
        Assert(payment.get().receiver() == Global.current_application_address()),
        App.localPut(
            sender.address(),
            Bytes("balance"),
            App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(),
        ),
    )

Subroutines with ABI Types

Subroutines can be created that accept ABI types as arguments and produce ABI types as return values. PyTeal will type check all subroutine calls and ensure that the correct types are being passed to such subroutines and that their return values are used correctly.

There are two different ways to use ABI types in subroutines, depending on whether the return value is an ABI type or a PyTeal 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 Expr, the @Subroutine decorator can be used.

To indicate the type of each argument, PEP 484 Python type annotations are used. Unlike normal usage of Python type annotations which are ignored at runtime, type annotations for subroutines inform the PyTeal compiler about the inputs and outputs of a subroutine. Changing these values has a direct effect on the code PyTeal generates.

An example of this type of subroutine is below:

from pyteal import *

@Subroutine(TealType.uint64)
def get_volume_of_rectangular_prism(
    length: abi.Uint16, width: abi.Uint64, height: Expr
) -> Expr:
    return length.get() * width.get() * height

Notice that this subroutine accepts the following arguments, not all of which are ABI types:

Despite some inputs being ABI types, calling this subroutine works the same as usual, except the values for the ABI type arguments must be the appropriate ABI type.

The following example shows how to prepare the arguments for and call get_volume_of_rectangular_prism():

# This is a continuation of the previous example

length = abi.Uint16()
width = abi.Uint64()
height = Int(10)
program = Seq(
    length.set(4),
    width.set(9),
    Assert(get_volume_of_rectangular_prism(length, width, height) > Int(0))
)

Subroutines that Return ABI Types

Warning

ABIReturnSubroutine is still taking shape and is subject to backwards incompatible changes.

  • For ABI Application entry point definition, feel encouraged to use 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 pragma or the Pragma expression to pin the version of PyTeal in your source code. See Version Pragmas for more information.

In addition to accepting ABI types as arguments, it’s also possible for a subroutine to return an ABI type value.

As mentioned in the Computed Values section, operations which return ABI values instead of traditional Expr objects need extra care. In order to solve this problem for subroutines, a new decorator, @ABIReturnSubroutine has been introduced.

The @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 output. The function return value must remain an Expr, while the output keyword argument will contain the ABI value the subroutine wishes to return. An example is below:

from pyteal import *

@ABIReturnSubroutine
def get_account_status(
    account: abi.Address, *, output: abi.Tuple2[abi.Uint64, abi.Bool]
) -> Expr:
    balance = abi.Uint64()
    is_admin = abi.Bool()
    return Seq(
        balance.set(App.localGet(account.get(), Bytes("balance"))),
        is_admin.set(App.localGet(account.get(), Bytes("is_admin"))),
        output.set(balance, is_admin),
    )

account = abi.make(abi.Address)
status = abi.make(abi.Tuple2[abi.Uint64, abi.Bool])
program = Seq(
    account.set(Txn.sender()),
    # NOTE! The return value of get_account_status(account) is actually a ComputedValue[abi.Tuple2[abi.Uint64, abi.Bool]]
    get_account_status(account).store_into(status),
)

Notice that even though the original get_account_status function returns an Expr object, the @ABIReturnSubroutine decorator automatically transforms the function’s return value and the output variable into a ComputedValue. As a result, callers of this subroutine must work with a ComputedValue.

The only exception to this transformation is if the subroutine has no return value. Without a return value, a ComputedValue is unnecessary and the subroutine will still return an Expr to the caller. In this case, the @ABIReturnSubroutine decorator acts identically the @Subroutine decorator.

Creating an ARC-4 Program

An ARC-4 program, like all other programs, can be called by application call transactions. ARC-4 programs respond to two specific subtypes of application call transactions:

  • Method calls, which encode a specific method to be called and arguments for that method, if needed.

  • Bare app calls, which have no arguments and no return value.

A method is a section of code intended to be invoked externally with an application call transaction. Methods may take arguments and may produce a return value. PyTeal implements methods as subroutines which are exposed to be externally callable.

A bare app call is more limited than a method, since it takes no arguments and cannot return a value. For this reason, bare app calls are more suited to allow on completion actions to take place, such as opting into an app.

To make it easier for an application to route across the many bare app calls and methods it may support, PyTeal introduces the 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 Router class.

Warning

Router usage is still taking shape and is subject to backwards incompatible changes.

Feel encouraged to use Router and expect a best-effort attempt to minimize backwards incompatible changes along with a migration path.

For these reasons, we strongly recommend using pragma or the Pragma expression to pin the version of PyTeal in your source code. See Version Pragmas for more information.

Registering Bare App Calls

The AVM supports 6 types of OnCompletion options that may be specified on an app call transaction. These actions are:

  1. No-op, the absence of an action, represented by OnComplete.NoOp

  2. Opt in, which allocates account-local storage for an app, represented by OnComplete.OptIn

  3. Close out, which removes account-local storage for an app, represented by OnComplete.CloseOut

  4. Clear state, which forcibly removes account-local storage for an app, represented by OnComplete.ClearState

  5. Update application, which updates an app, represented by OnComplete.UpdateApplication

  6. Delete application, which deletes an app, represented by 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 (CallConfig.CREATE), during a non-creation app call (CallConfig.CALL), or during either (CallConfig.ALL).

The 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 OnCompleteAction class.

The OnCompleteAction class is responsible for holding the actual code for the bare app call handler (an instance of either Expr or a subroutine that takes no args and returns nothing) as well as a 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 Router.__init__ method.

A brief example is below:

from pyteal import *

@Subroutine(TealType.none)
def opt_in_handler() -> Expr:
    return App.localPut(Txn.sender(), Bytes("opted_in_round"), Global.round())


@Subroutine(TealType.none)
def assert_sender_is_creator() -> Expr:
    return Assert(Txn.sender() == Global.creator_address())


router = Router(
    name="ExampleApp",
    bare_calls=BareCallActions(
        # Allow app creation with a no-op action
        no_op=OnCompleteAction(
            action=Approve(), call_config=CallConfig.CREATE
        ),

        # Register the `opt_in_handler` to be called during opt in.
        #
        # Since we use `CallConfig.ALL`, this is also a valid way to create this app
        # (if the creator wishes to immediately opt in).
        opt_in=OnCompleteAction(
            action=opt_in_handler, call_config=CallConfig.ALL
        ),

        # Allow anyone who opted in to close out from the app.
        close_out=OnCompleteAction(
            action=Approve(), call_config=CallConfig.CALL
        ),

        # Only approve update and delete operations if `assert_sender_is_creator` succeeds.
        update_application=OnCompleteAction(
            action=assert_sender_is_creator, call_config=CallConfig.CALL
        ),
        delete_application=OnCompleteAction(
            action=assert_sender_is_creator, call_config=CallConfig.CALL
        ),
    ),
)

Note

When deciding which 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 CallConfig.CALL should be used.

Registering Methods

Warning

The Router does not validate inputs for compound types (abi.StaticArray, abi.Address, abi.DynamicArray, abi.String, or abi.Tuple).

We strongly recommend methods immediately access and validate compound type parameters before persisting arguments for later transactions. For validation, it is sufficient to attempt to extract each element your method will use. If there is an input error for an element, indexing into that element will fail.

Notes:

  • This recommendation applies to recursively contained compound types as well. Successfully extracting an element which is a compound type does not guarantee the extracted value is valid; you must also inspect its elements as well.

  • Because of this, abi.Address is not guaranteed to have exactly 32 bytes. To defend against unintended behavior, manually verify the length is 32 bytes, i.e. Assert(Len(address.get()) == Int(32)).

There are two ways to register a method with the Router class.

The first way to register a method is with the Router.add_method_handler method, which takes an existing subroutine decorated with @ABIReturnSubroutine. An example of this is below:

from pyteal import *

router = Router(
    name="Calculator",
    bare_calls=BareCallActions(
        # Allow this app to be created with a no-op call
        no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE),
        # Allow standalone user opt in and close out
        opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
        close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
    ),
)

@ABIReturnSubroutine
def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr:
    """Adds the two arguments and returns the result.

    If addition will overflow a uint64, this method will fail.
    """
    return output.set(a.get() + b.get())


@ABIReturnSubroutine
def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr:
    """Adds the two arguments, returns the result, and stores it in the sender's local state.

    If addition will overflow a uint64, this method will fail.

    The sender must be opted into the app. Opt-in can occur during this call.
    """
    return Seq(
        output.set(a.get() + b.get()),
        # store the result in the sender's local state too
        App.localPut(Txn.sender(), Bytes("result", output.get())),
    )

# Register the `add` method with the router, using the default `MethodConfig`
# (only no-op, non-creation calls allowed).
router.add_method_handler(add)

# Register the `addAndStore` method with the router, using a `MethodConfig` that allows
# no-op and opt in non-creation calls.
router.add_method_handler(
    addAndStore,
    method_config=MethodConfig(no_op=CallConfig.CALL, opt_in=CallConfig.CALL),
)

This example registers two methods with the router, add and addAndStore.

Because the add method does not pass a value for the method_config parameter of 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 OnComplete.NoOp.

On the other hand, the addAndStore method does provide a method_config value. A value of 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 OnComplete.NoOp or OnComplete.OptIn.

The second way to register a method is with the 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 Router.method syntax:

from pyteal import *

my_router = Router(
    name="Calculator",
    bare_calls=BareCallActions(
        # Allow this app to be created with a no-op call
        no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE),
        # Allow standalone user opt in and close out
        opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
        close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL),
    ),
)

# NOTE: the first part of the decorator `@my_router.method` is the router variable's name
@my_router.method
def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr:
    """Adds the two arguments and returns the result.

    If addition will overflow a uint64, this method will fail.
    """
    return output.set(a.get() + b.get())

@my_router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL)
def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr:
    """Adds the two arguments, returns the result, and stores it in the sender's local state.

    If addition will overflow a uint64, this method will fail.

    The sender must be opted into the app. Opt-in can occur during this call.
    """
    return Seq(
        output.set(a.get() + b.get()),
        # store the result in the sender's local state too
        App.localPut(Txn.sender(), Bytes("result", output.get())),
    )

Compiling a Router Program

Now that we know how to add bare app call and method call handlers to a Router, the next step is to compile the Router into TEAL code.

The 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 compileTeal to compile these expressions and create TEAL code.

Note

We recommend enabling the 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 Compiler Optimization page for more information.

In addition to receiving the approval and clear state programs, the Router.compile_program method also returns a Python SDK 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 Router class:

# 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))

This example uses the Router.compile_program method to create the approval program, clear state program, and contract description for the “AlgoBank” contract. The produced algobank.json file is below:

{
    "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": {}
}

Calling an ARC-4 Program

One of the advantages of developing an ABI-compliant PyTeal contract is that there is a standard way for clients to call your contract.

Broadly, there are two categories of clients that may wish to call your contract: off-chain systems and other on-chain contracts. The following sections describe how each of these clients can call ABI methods implemented by your contract.

Off-Chain, from an SDK or goal

Off-chain systems can use the Algorand SDKs or the command-line tool goal to interact with ABI-compliant contracts.

Every SDK contains an 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 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, goal app create (docs) can be used to construct and send an app creation bare app call.

    • For non-creation calls, goal app <action> can be used to construct and send a non-creation bare app call. The <action> 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:
    • goal app method (docs) can be used to construct, send, and read the return value of a method call. This command can be used for application creation as well, if the application allows this to happen in one of its methods.

On-Chain, in an Inner Transaction

Algorand applications can issue inner transactions, which can be used to invoke other applications.

In PyTeal, this can be achieved using the InnerTxnBuilder class and its functions. To invoke an ABI method, PyTeal has 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 last_log() property of either InnerTxn or Gitxn[<index>] to obtain the logged return value. As described in ARC-4, this value will be prefixed with the 4 bytes 151f7c75 (shown in hex), and after this prefix the encoded ABI value will be available. You can use the decode(...) method on an instance of the appropriate ABI type in order to decode and use this value.