Usage

Tutorial

Note

This tutorial assumes you’ve already installed the sandbox and have it running.

Lets write a simple calculator app. Full source here.

First, create a class to represent our application as a subclass of the beaker Application.

from beaker import Application

class Calculator(Application):
    pass

This is a full application, though it doesn’t do much.

Instantiate it and take a look at some of the resulting fields.


if __name__ == "__main__":
    import json

Nice! This is already enough to provide the TEAL programs and ABI specification.

Note

The Application.dump method can be used to write the approval.teal, clear.teal, contract.json, and application.json to the local file system.

Lets add some methods to be handled by an incoming ApplicationCallTransaction. We can do this by tagging a PyTeal ABI method with with the external decorator.

class Calculator(Application):
    @external
    def add(self, a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64):
        """Add a and b, return the result"""
        return output.set(a.get() + b.get())

    @external
    def mul(self, a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64):
        """Multiply a and b, return the result"""
        return output.set(a.get() * b.get())

    @external
    def sub(self, a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64):
        """Subtract b from a, return the result"""
        return output.set(a.get() - b.get())

    @external
    def div(self, a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64):
        """Divide a by b, return the result"""
        return output.set(a.get() / b.get())

The @external decorator adds an ABI method to our application and includes it in the routing logic for handling an ABI call.

The python method must return an Expr of some kind, invoked when the external is called.

Note

self may be omitted if the method does not need to access any instance variables. Class variables or methods may be accessed through the class name like MySickApp.do_thing(data)

Lets now deploy and call our contract using an ApplicationClient.

    # Here we use `sandbox` but beaker.client.api_providers can also be used
    # with something like ``AlgoNode(Network.TestNet).algod()``
    algod_client = sandbox.get_algod_client()

    acct = sandbox.get_accounts().pop()

    # Create an Application client containing both an algod client and app
    app_client = ApplicationClient(
        client=algod_client, app=Calculator(), signer=acct.signer
    )

    # Create the application on chain, set the app id for the app client
    app_id, app_addr, txid = app_client.create()
    print(f"Created App with id: {app_id} and address addr: {app_addr} in tx: {txid}")

Thats it!

To summarize, we:

  • Wrote an application using Beaker and PyTeal

    By subclassing Application and adding an external method

  • Compiled it to TEAL

    Done automatically by the Application class, and PyTeal’s Router.compile

  • Assembled the TEAL to binary

    Done automatically by the ApplicationClient by sending the TEAL to the algod compile endpoint

  • Deployed the application on-chain

    Done by invoking the app_client.create, which submits an ApplicationCallTransaction including our binary.

    Note

    Once created, subsequent calls to the app_client are directed to the app_id. The constructor may also be passed an app_id directly if one is already deployed.

  • Called the method we defined

    Using app_client.call, passing the method defined in our class and args the method specified (by name).

    Note

    The args passed must match the type of the method (i.e. don’t pass a string when it wants an int).

    The result contains the parsed return_value which is a python native type that mirrors the return type of the ABI method.

Decorators

Above, we used the decorator @external to mark a method as being exposed in the ABI and available to be called from off-chain.

The @external decorator can take parameters to change how it may be called or what accounts may call it, see examples here.

Other decorators include @internal which marks the method as being callable only from inside the application or with one of the OnComplete handlers (e.g. create, opt_in, etc…)

State Management

Beaker provides a way to define state values as class variables and use them throughout our program. This is a convenient way to encapsulate functionality associated with some state values.

Note

Throughout the examples, we tend to mark State Values as Final[...], this is solely for good practice and has no effect on the output of the program.

Lets write a new app with Application State (or Global State in Algorand parlance) to our Application.

class CounterApp(Application):

    counter: Final[ApplicationStateValue] = ApplicationStateValue(
        stack_type=TealType.uint64,
        descr="A counter for showing how to use application state",
    )

    @create
    def create(self):
        return self.initialize_application_state()

    @external(authorize=Authorize.only(Global.creator_address()))
    def increment(self, *, output: abi.Uint64):
        """increment the counter"""
        return Seq(
            self.counter.set(self.counter + Int(1)),
            output.set(self.counter),
        )

    @external(authorize=Authorize.only(Global.creator_address()))
    def decrement(self, *, output: abi.Uint64):
        """decrement the counter"""
        return Seq(
            self.counter.set(self.counter - Int(1)),
            output.set(self.counter),
        )

We’ve added an ApplicationStateValue attribute to our class with several configuration options and we can reference it by name throughout our application.

Note

The base Application class has several externals pre-defined, including create which performs ApplicationState initialization for us, setting the keys to default values.

You may also define state values for applications, called AccountState (or Local storage) and even allow for reserved state keys.

For more example usage see the example here.

Inheritance

What about extending our Application with some other functionality?

from beaker.decorators import external

if __name__ == "__main__":
    from op_up import OpUp
else:
    from .op_up import OpUp


class ExpensiveApp(OpUp):
    """Do expensive work to demonstrate inheriting from OpUp"""

    @external
    def hash_it(
        self,
        input: abi.String,
        iters: abi.Uint64,
        opup_app: abi.Application = OpUp.opup_app_id,
        *,
        output: abi.StaticBytes[Literal[32]],
    ):
        return Seq(
            Assert(opup_app.application_id() == self.opup_app_id),
            self.call_opup(Int(255)),
            (current := ScratchVar()).store(input.get()),
            For(
                (i := ScratchVar()).store(Int(0)),

Here we subclassed the OpUp contract which provides functionality to create a new Application on chain and store its app id for subsequent calls to increase budget.

We inherit the methods and class variables that OpUp defined, allowing us to encapsulate and compose behavior.

Also note that the opup_app argument specifies a default value. This is a bit of magic that serves only to produce a hint for the caller in the resulting Application Spec.

Parameter Default Values

In the OpUp example above, the argument opup_app should be the id of the application that we use to increase our budget via inner app calls. This value should not change frequently, if at all, but is still required to be passed by the caller so we may use it in our logic, namely to execute an application call against it.

By specifying the default value of the argument in the method signature, we can communicate to the caller, through the hints of the Application Spec, what the value should be.

Options for default arguments are:

  • A constant: one of bytes | int | str | Bytes | Int

  • State Values: one of ApplicationStateValue | AccountStateValue

  • A read-only ABI method: a method defined to produce some more complex value than a state value or constant would be able to produce.

The result of specifying the default value here is that we can call the method, omitting the opup_app argument:

result = app_client.call(app.hash_it, input="hashme", iters=10)

When invoked, the ApplicationClient consults the method definition to check that all the expected arguments are passed. If it finds that an argument is not passed, it will check the hints for a default argument for the method that may be used directly (constant) or resolved (need to look it up on chain or call method). Upon finding a resolvable it will look up the state value, call the method. The resulting value is passed in for argument to the application call.

Precompiles

Often an app developer needs to have the assembled binary of a program available at contract runtime. One way to get this binary is to use the Precompile feature.

In the OpUp example above, the ExpensiveApp inherits from the OpUp app. The OpUp app contains an AppPrecompile for the target app we’d like to call when we make opup requests.

class TargetApp(Application):
    """Simple app that allows the creator to call `opup` in order to increase its opcode budget"""

    @external(authorize=Authorize.only(Global.creator_address()))
    def opup(self):
        return Approve()


class OpUp(Application):
    """OpUp creates a "target" application to make opup calls against in order to increase our opcode budget."""

    #: The app to be created to receiver opup requests
    target: AppPrecompile = AppPrecompile(TargetApp())

The inclusion of a Precompile prevents building the TEAL until its been fully compiled to assembled binary but we can still reference it in our Application.

The OpUp Application defines a method to create the target application by referencing the AppPrecompile's approval/clear binary attribute which contains the assembled binary for the target Application as a Bytes Expression.

    @internal(TealType.none)
    def create_opup(self):
        """internal method to create the target application"""
        return Seq(
            InnerTxnBuilder.Begin(),
            InnerTxnBuilder.SetFields(
                {
                    TxnField.type_enum: TxnType.ApplicationCall,
                    TxnField.approval_program: self.target.approval.binary,
                    TxnField.clear_state_program: self.target.clear.binary,
                    TxnField.fee: Int(0),
                }
            ),
            InnerTxnBuilder.Submit(),
            self.opup_app_id.set(InnerTxn.created_application_id()),
        )

Another situations where a Precompile is useful is when validating the logic of a LogicSignature.

In the offload_compute example, we check to make sure that the hash of the signer is the LogicSignature we’re expecting so that we’re sure it is doing “the right thing”.

    @external
    def check_eth_sig(
        self, hash: HashValue, signature: Signature, *, output: abi.String
    ):
        return Seq(
            # The precompiled lsig should have its address and binary available
            # here so we can use it to make sure we've been called
            # with the correct lsig
            Assert(Txn.sender() == self.verifier.logic.hash()),
            output.set("lsig validated"),
        )

Additionally, if the LogicSignature has one or more TemplateVariables specified, the template_hash function may be used by passing arguments that should be populated into the templated LogicSignature.

    @external
    def check(self, signer_address: abi.Address, msg: abi.String, sig: Signature):
        return Assert(
            Txn.sender() == self.sig_checker.logic.template_hash(signer_address.get())
        )