Usage

Tutorial

Note

This tutorial assumes you’ve already got a local algod node running from either Algokit or the sandbox.

Let’s write a simple calculator app. Full source here.

First, create an instance of the Application class to represent our application.

calculator_app = Application("Calculator")

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

Build it and take a look at the resulting application spec.

    calc_app_spec = calculator_app.build()
    print(calc_app_spec.approval_program)
    print(calc_app_spec.clear_program)
    print(calc_app_spec.to_json())

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

Note

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

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

@calculator_app.external
def add(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
    """Add a and b, return the result"""
    return output.set(a.get() + b.get())


@calculator_app.external
def mul(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
    """Multiply a and b, return the result"""
    return output.set(a.get() * b.get())


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


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

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

Note

The python method must return a PyTeal Expr, to be invoked when the external method is called.

Let’s now deploy and call our contract using an ApplicationClient.

def demo() -> None:
    # Here we use `localnet` but beaker.client.api_providers can also be used
    # with something like ``AlgoNode(Network.TestNet).algod()``
    algod_client = localnet.get_algod_client()

    acct = localnet.get_accounts().pop()

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

    # Create the application on chain, implicitly sets 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}")

    result = app_client.call(add, a=2, b=2)
    print(f"add result: {result.return_value}")

That’s it!

To summarize, we:

  • Wrote an application using Beaker and PyTeal

    By instantiating a new Application and adding external methods.

  • Built the smart contract, converting it to TEAL

    Done by calling the build method on by the Application instance.

  • Assembled the TEAL to binary

    Done automatically by the ApplicationClient during initialization 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 compiled programs.

    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 as keyword arguments.

    Note

    The args passed must match the types of those specified in 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 reflects the return type of the ABI method.

Application structure

The recommended way to structure a beaker application is to have one smart contract per file. This then makes it simpler for any other code to build or interact with the app by importing the module.

For example with a smart contract in a file named my_smart_contract.py

class MyState:
    global_state_value = beaker.GlobalStateValue(TealType.uint64)
    local_state_value = beaker.LocalStateValue(TealType.bytes)

state = MyState()
app = beaker.Application("MySmartContract", state=state)

@app.external
def my_method(self, x: pyteal.abi.Uint64, *, output: pyteal.abi.Uint64) -> pyteal.Expr
    ...

You can then import that smart contract as a module, and interact with it’s components.

import my_smart_contract

my_smart_contract.app # reference the app

my_smart_contract.my_method # reference a method

my_smart_contract.my_method.get_method_signature() # get ABI signature of my_method

my_smart_contract.app.state # to reference the state, OR

my_smart_contract.state # if the app state was assigned to a global variable

Decorators

Above, we used the decorator @app.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.

It is also possible to use one of the OnComplete decorators (e.g. update, delete, opt_in, etc…) to specify a single OnComplete type the contract expects.

State Management

Beaker provides a way to define state using a class which encapsulates the state configuration. This provides a convenient way to organize and reference state values.

Let’s write a new app, adding Global State to our Application.


class CounterState:
    counter = GlobalStateValue(
        stack_type=pt.TealType.uint64,
        descr="A counter for showing how to use application state",
    )


counter_app = Application("CounterApp", state=CounterState())


@counter_app.external(authorize=Authorize.only_creator())
def increment(*, output: pt.abi.Uint64) -> pt.Expr:
    """increment the counter"""
    return pt.Seq(
        counter_app.state.counter.set(counter_app.state.counter + pt.Int(1)),
        output.set(counter_app.state.counter),
    )


@counter_app.external(authorize=Authorize.only_creator())
def decrement(*, output: pt.abi.Uint64) -> pt.Expr:
    """decrement the counter"""
    return pt.Seq(
        counter_app.state.counter.set(counter_app.state.counter - pt.Int(1)),
        output.set(counter_app.state.counter),
    )

We’ve created a class to hold our State and added a GlobalStateValue attribute to it with configuration options for how it should be treated. We can reference it by name throughout our application.

You may also define state values for applications, called LocalState (or Local storage) and even allow for reserved state keys when you’re not sure what the keys will be.

For more example usage see the example here.

Code Reuse

What about extending our Application with some functionality that already exists? For example, what if we wanted to provide the correct interface for a specific ARC?

We can use the blueprint pattern to include things like method handlers to our application!

# A blueprint that adds a method named `add` to the external
# methods of the Application passed
def add_blueprint(app: Application) -> None:
    @app.external
    def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr:
        return output.set(a.get() + b.get())

Here we add a method handler for an ABI method to our application in two ways.

If no arguments are needed for the blueprint method, we can pass it to apply and the blueprint method will be called with our Application as the argument.

app = Application("BlueprintExampleNoArgs").apply(add_blueprint)

If the blueprint requires some arguments to customize it’s behavior, we can pass the additional arguments the blueprint expects:

# A blueprint that adds a method named `addN` to the external
# methods of the Application passed
def add_n_blueprint(app: Application, n: int) -> None:
    @app.external
    def add_n(a: abi.Uint64, *, output: abi.Uint64) -> Expr:
        return output.set(a.get() + Int(n))


app = Application("BlueprintExampleWithArgs").apply(add_n_blueprint, n=1)

Parameter Default Values

In the OpUp example, there is a method handler, hash_it, which specifies the argument opup_app should be the ID of the application that we use to increase our budget via inner app calls.

Note

The default argument shown below is _not_ a valid type, it’s only used as a hint to the compiler. If you’re using mypy or similar, a type ignore directive should be used to stop it from complaining.

@app.external
def hash_it(
    input: pt.abi.String,
    iters: pt.abi.Uint64,
    opup_app: pt.abi.Application = app.state.opup_app_id,  # type: ignore[assignment]
    *,
    output: pt.abi.StaticBytes[Literal[32]],
) -> pt.Expr:
    return pt.Seq(
        pt.Assert(opup_app.application_id() == app.state.opup_app_id),
        Repeat(255, call_opup()),
        (current := pt.ScratchVar()).store(input.get()),
        pt.For(
            (i := pt.ScratchVar()).store(pt.Int(0)),
            i.load() < iters.get(),
            i.store(i.load() + pt.Int(1)),
        ).Do(current.store(pt.Sha256(current.load()))),

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.

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

Options for default arguments are:

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

  • State Values: one of GlobalStateValue | LocalStateValue

  • 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 without specifying the opup_app argument:

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

When invoked, the ApplicationClient consults the Application Specification to check that all the expected arguments are passed.

If it finds that an argument the method expects is not passed, it will check the hints for a default value for the argument of the method that may be used directly (constant) or resolved (need to look it up on chain or call method).

Upon finding a default value that needs to be resolved, it will look up the state value or call the method. The resulting value is passed in for argument to the application call.

Precompiles

Often an app developer needs to have the fully 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, the ExpensiveApp needs to create an application to use as the target app for op budget increase calls.

    @pt.Subroutine(pt.TealType.none)
    def create_opup() -> pt.Expr:
        """internal method to create the target application"""
        #: The app to be created to receiver opup requests
        target = beaker.precompiled(target_app)

        return pt.Seq(
            pt.InnerTxnBuilder.Begin(),
            pt.InnerTxnBuilder.SetFields(
                {
                    **target.get_create_config(),
                    pt.TxnField.fee: pt.Int(0),
                }
            ),
            pt.InnerTxnBuilder.Submit(),
            app.state.opup_app_id.set(pt.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 address of the signer is the LogicSignature we’re expecting so that we’re sure it is doing “the right thing”.

@app.external
def check_eth_sig(
    hash: pt.abi.StaticBytes[Literal[32]],
    signature: pt.abi.StaticBytes[Literal[65]],
    *,
    output: pt.abi.String
) -> pt.Expr:
    # The lsig that will be responsible for validating the
    # incoming signature against the incoming hash
    # When passed to Precompile, it flags the init of the Application
    # to prevent building approval/clear programs until the precompile is
    # compiled so we have access to compiled information (its address for instance)
    verifier = beaker.precompiled(verify_lsig)

    return pt.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
        pt.Assert(pt.Txn.sender() == verifier.address()),
        output.set("lsig validated"),
    )

Additionally, if the LogicSignature needs one or more TemplateVariables the LogicSignatureTemplate is used and functions similarly, by passing the named template arguments to the call to get the address.

@app.external
def check(
    signer_address: pt.abi.Address, msg: pt.abi.String, sig: Signature
) -> pt.Expr:
    sig_checker_pc = beaker.precompiled(lsig)
    # The lsig will take care of verifying the signature
    # all we need to do is check that its been used to sign this transaction
    return pt.Assert(
        pt.Txn.sender() == sig_checker_pc.address(user_addr=signer_address.get())
    )