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 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 some of the resulting fields.

    calc = calculator_app.build()
    print(calc.approval_program)
    print(calc.clear_program)
    print(json.dumps(calc.contract.dictify()))

Nice! 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.

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.

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


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


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


@calculator_app.external
def div(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> 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.

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

Lets now deploy and call our contract using an ApplicationClient.

def demo() -> None:
    # 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_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}")

Thats 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 (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.

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

State Management

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

Lets write a new app, adding Global State to our Application.

class CounterState:
    counter = GlobalStateValue(
        stack_type=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: abi.Uint64) -> Expr:
    """increment the counter"""
    return Seq(
        counter_app.state.counter.set(counter_app.state.counter + Int(1)),
        output.set(counter_app.state.counter),
    )


@counter_app.external(authorize=Authorize.only_creator())
def decrement(*, output: abi.Uint64) -> Expr:
    """decrement the counter"""
    return Seq(
        counter_app.state.counter.set(counter_app.state.counter - 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 a blueprint 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())


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

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

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 its behavior, we can pass the additional arguments the blueprint expects:

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

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, 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, 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()),
        )

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

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