Hello Beaker
Hello Developer
If you’ve written and deployed a Smart Contract for Algorand using PyTeal before today, you’ve probably chewed a lot of glass to do so, well done!
Take heart; the Algorand team has been working hard to improve the development experience.
Before we dive into the improvements, let’s review some common things folks struggle with while developing.
Code Organization
When you start writing a contract, how do you structure the program logic? How do you handle inputs and outputs from the contract?
A very common pattern for writing the approval program in PyTeal is something like:
def approval():
#...
return Cond(
# Infer that this is create
[Txn.application_id() == Int(0), do_create_method()],
# Check on complete manually
[Txn.on_complete() == OnComplete.UpdateApplication, do_update()],
# Use some const that you have to somehow communicate to the caller
# to route to the right method, then figure out how to parse the rest
# of the app args
[Txn.application_args[0] == "do_the_thing", do_the_thing()],
#...
)
# ...
approval_teal = compileTeal(approval(), mode=Mode.Application, version=6)
This works, but is difficult to understand for a newcomer.
Interacting with the Application
To deploy an application on-chain, you submit an app create transaction. In this transaction, you specify the compiled TEAL programs, the application schema (“How many global uints do I need again?”), and extra program pages.
Calling an on-chain application involves crafting app call transactions with the appropriate routing and data arguments if not using the ABI.
Even when using the ABI, calling methods involves importing the contract description JSON and constructing an AtomicTransactionComposer, passing arguments as a list with no context about what they should be.
Managing State
Managing the application state schema is often done manually with constants for the keys and remembering what the type associated should be.
Creating the application requires you to know the number and type of each state value which you have no easy way to get automatically.
Debugging
Debugging can be a nightmare of trying to figure out an error message like assert failed: pc=XXX
Testing
Testing contracts can be difficult and little guidance is provided. Often it requires rebuilding a lot of the front end infrastructure to test different inputs/outputs.
The devs did something
Now, let’s see how things have changed.
ABI
The ABI provides standards for encoding types, describing methods, and internally routing method calls to the appropriate logic.
With the ABI, we now have a standard way to both organize code and interact with the application.
More details on the ABI are available here
Atomic Transaction Composer
Using the Atomic Transaction Composer and the the ABI spec for your contract, you can easily compose atomic group transactions and have the arguments encoded and return values decoded for you!
Pyteal ABI
PyTeal now handles encoding/decoding of data types in a contract. The PyTeal Router
class even provides a way to handle method routing logic, passing decoded types directly to a method, and provides the ABI contract spec.
For example, if you want a method that adds 1 to a uint8, you can write it thusly:
@router.method
def increment_my_number(input: abi.Uint8, *, output: abi.Uint8):
return output.set(input.get() + Int(1))
So. Much. Nicer.
For more background see the blog post here
For detailed docs on PyTeal ABI see docs here
Source Maps
Mapping a pc
returned from an error message has been made much easier. You can now compile TEAL with the sourcemap
flag enabled. The resulting map comes back according to this spec and can be decoded with any of the SDKs using the new SourceMap
object.
This means you can associate a pc
directly to the source TEAL line, with all the familiar names and formatting you’re used to looking at.
Hello Beaker
Today we are sharing Beaker, a Smart Contract development framework meant to further improve the development experience.
Beaker takes advantage of the above improvements, allowing us to provide much more structure to applications.
Heads up though, it is still experimental.
Full Docs are here.
Let’s see how Beaker solves our problems.
Code Organization
Beaker provides a standard way to organize code by using a class to encapsulate functionality.
from beaker import Application, external
class MyApp(Application):
@external
def add(self, a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64):
return output.set(a.get() + b.get())
This is a full application! It’s got an approval program
, clear program
, an implicitly empty state
. The methods defined are provided in an ABI contract
to export for other clients.
The @external
decorator on the method exposes our defined method to callers and provides routing based on its method signature
. The resulting method signature of this method is add(uint64,uint64)uint64
.
The add
method is a (mostly) valid PyTeal ABI Method. The exception that makes it mostly valid here, is that Beaker lets you pass self
, allowing references to instance vars.
There is much more you can do including; access control, changing which OnComplete
types may be used to call it, or marking it as a read-only
method.
For more information, see the Decorator docs
Interacting with the application
Beaker provides an ApplicationClient
to deal with common needs like creating/opting-in to/calling methods.
It uses your Application
definition to provide context like the schema or the arguments required for the methods being called.
from beaker import sandbox, client
# get the first acct in the sandbox
acct = sandbox.get_accts().pop()
# create an app client
app_client = client.ApplicationClient(
client=sandbox.get_algod_client(),
app=MyApp(),
signer=acct.signer
)
# deploy the app on-chain
app_client.create()
# call the method
result = app_client.call(MyApp.add, a=32, b=10)
print(result.return_value) # 42
# now go outside and touch some grass cuz you're done
For more see ApplicationClient docs
Managing state
Beaker allows you to declare typed state values as class variables.
from beaker import Application, ApplicationStateValue, external
class CounterApp(Application):
counter = ApplicationStateValue(TealType.uint64)
@external
def incr_counter(self, incr_amt: abi.Uint64):
self.counter.set(self.counter + incr_amt.get())
We can even inspect our application to see what its schema requirements are!
app = CounterApp()
print(app.app_state.schema())
For more see State docs
Debugging
Beaker improves the pc=xxx
error message using the source map endpoint during compilation and mapping the pc back to the source teal. The resulting LogicException
allows you to see the exact source Teal line number with all the useful names of subroutines and any comments in the source teal.
Below is the result of a simple print of a LogicException
telling me exactly where my program failed. More importantly it provides the context from the source TEAL showing me why it failed.
Txn WWVF5P2BXRNQDFFSGAGMCXJNDMZ224RJUGSMVPJVTBCVHEZMOMNA had error 'assert failed pc=883' at PC 883 and Source Line 579:
store 50
store 49
store 48
store 47
// correct asset a
load 50
txnas Assets
bytec_0 // "a"
app_global_get
==
assert <-- Error
// correct asset b
load 51
txnas Assets
bytec_1 // "b"
app_global_get
==
assert
// correct pool token
load 49
We’re also working on getting this mapping all the way back to the source PyTeal with this issue
Testing
Initially Beaker provides helpers for:
-
Retrieving and comparing account balances. Useful for ensuring the correct amounts of algos or tokens were transferred to or from relevant accounts.
-
Unit testing functionality by passing inputs and comparing to expected outputs. Useful for testing small, self contained behaviors.
For more information, see Testing docs.
More
There is a lot more not covered here, and a lot still to be done. Beaker needs your help to get better!
See the docs at https://beaker.algo.xyz
The code at https://github.com/algorand-devrel/beaker. Please feel free to file issues or PRs!
And for any questions, ping @barnji
in the #beaker
channel on the Algorand discord