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.
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 == "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 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 can be a nightmare of trying to figure out an error message like
assert failed: pc=XXX
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.
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 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
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
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.
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.
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
clear program, an implicitly empty
state. The methods defined are provided in an ABI
contract to export for other clients.
@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 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
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
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
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
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.
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