Boxes

Applications that need to maintain a large amount of state can use Box data storage.

See the Parameters Table for protocol level limits on Boxes.

Full Example

While PyTeal provides the basic tools for working with boxes, Beaker provides a few handy abstractions for working with them.

Note

Beaker provides helpful abstractions, but these are NOT required to be used. The standard PyTeal Box expressions can be used to interact with boxes outside the helpers provided by Beaker.

BoxMapping

A BoxMapping provides a way to store data with a given key.

Warning

Care should be taken to ensure if multiple BoxMapping types are used, there is no overlap with keys. If there may be overlap, a prefix argument MUST be set in order to provide a unique namespace.

class beaker.lib.storage.BoxMapping[source]

Mapping provides an abstraction to store some typed data in a box keyed with a typed key

class Element[source]

Container type for a specific box key and type

__init__(key: Expr, value_type: type[pyteal.abi.BaseType])[source]
delete() Expr[source]

delete the box at this key

exists() Expr[source]

check to see if a box with this key exists.

get() Expr[source]

get the bytes from this box.

set(val: pyteal.abi.BaseType | pyteal.Expr) Expr[source]

overwrites the contents of the box with the provided value.

Parameters

val – An instance of the type or an Expr that evaluates to bytes

store_into(val: BaseType) Expr[source]

decode the bytes from this box into an abi type.

Parameters

val – An instance of the type to be populated with the bytes from the box

__init__(key_type: type[pyteal.abi.BaseType], value_type: type[pyteal.abi.BaseType], prefix: pyteal.Expr | None = None)[source]

Initialize a Mapping object with details about storage

Parameters
  • key_type – The type that will be used for the key. This type MUST encode to a byte string of < 64 bytes or it will fail at runtime.

  • value_type – The type to be stored in the box.

  • prefix (Optional) – An optional argument to prefix the key, providing a name space in order to avoid collisions with other mappings using the same keys

BoxList

A BoxList provides a way to store some number of some _static_ abi type.

Note

Since the BoxList uses the size of the element to compute the offset into the box, the data type MUST be static.

class beaker.lib.storage.BoxList[source]

List stores a list of static types in a box, named as the class attribute unless an overriding name is provided

class Element[source]
__init__(name: Expr, element_size: Expr, idx: Expr)[source]
get() Expr[source]

get the bytes for this element in the list

has_return() bool[source]

Check if this expression always returns from the current subroutine or program.

set(val: BaseType) Expr[source]

set the bytes for this element in the list

Parameters

index (The value to write into the list at the given) –

store_into(val: BaseType) Expr[source]

decode the bytes from this list element into the instance of the type provided

Parameters

val – An instance of the type to decode into

type_of() TealType[source]

Get the return type of this expression.

__init__(value_type: type[pyteal.abi.BaseType], elements: int, name: str | None = None)[source]
create() Expr[source]

creates a box with the given name and with a size that will allow storage of the number of the element specified.

Full Example

import typing

import pyteal as pt

import beaker
from beaker.consts import (
    ASSET_MIN_BALANCE,
    BOX_BYTE_MIN_BALANCE,
    BOX_FLAT_MIN_BALANCE,
    FALSE,
)
from beaker.lib.storage import BoxList, BoxMapping


# NamedTuple we'll store in a box per member
class MembershipRecord(pt.abi.NamedTuple):
    role: pt.abi.Field[pt.abi.Uint8]
    voted: pt.abi.Field[pt.abi.Bool]


# Custom type alias
Affirmation = pt.abi.StaticBytes[typing.Literal[64]]


class MembershipClubState:
    membership_token = beaker.GlobalStateValue(
        pt.TealType.uint64,
        static=True,
        descr="The asset that represents membership of this club",
    )

    # A Listing is a simple list, initialized with some _static_ data type and a length
    affirmations = BoxList(Affirmation, 10)

    def __init__(self, *, max_members: int, record_type: type[pt.abi.BaseType]):
        self.record_type = record_type
        # A Mapping will create a new box for every unique key,
        # taking a data type for key and value
        # Only static types can provide information about the max
        # size (and thus min balance required) - dynamic types will fail at abi.size_of
        self.membership_records = BoxMapping(pt.abi.Address, record_type)

        # Math for determining min balance based on expected size of boxes
        self.max_members = pt.Int(max_members)
        self.minimum_balance = pt.Int(
            ASSET_MIN_BALANCE  # Cover min bal for member token
            + (
                BOX_FLAT_MIN_BALANCE
                + (pt.abi.size_of(record_type) * BOX_BYTE_MIN_BALANCE)
                + (pt.abi.size_of(pt.abi.Address) * BOX_BYTE_MIN_BALANCE)
            )
            * max_members  # cover min bal for member record boxes we might create
            + (
                BOX_FLAT_MIN_BALANCE
                + (self.affirmations.box_size.value * BOX_BYTE_MIN_BALANCE)
            )  # cover min bal for affirmation box
        )


app = beaker.Application(
    "MembershipClub",
    state=MembershipClubState(max_members=1000, record_type=MembershipRecord),
    build_options=beaker.BuildOptions(scratch_slots=False),
)


@app.external(authorize=beaker.Authorize.only_creator())
def bootstrap(
    seed: pt.abi.PaymentTransaction,
    token_name: pt.abi.String,
    *,
    output: pt.abi.Uint64,
) -> pt.Expr:
    """create membership token and receive initial seed payment"""
    return pt.Seq(
        pt.Assert(
            seed.get().receiver() == pt.Global.current_application_address(),
            comment="payment must be to app address",
        ),
        pt.Assert(
            seed.get().amount() >= app.state.minimum_balance,
            comment=f"payment must be for >= {app.state.minimum_balance.value}",
        ),
        pt.Pop(app.state.affirmations.create()),
        pt.InnerTxnBuilder.Execute(
            {
                pt.TxnField.type_enum: pt.TxnType.AssetConfig,
                pt.TxnField.config_asset_name: token_name.get(),
                pt.TxnField.config_asset_total: app.state.max_members,
                pt.TxnField.config_asset_default_frozen: pt.Int(1),
                pt.TxnField.config_asset_manager: pt.Global.current_application_address(),
                pt.TxnField.config_asset_clawback: pt.Global.current_application_address(),
                pt.TxnField.config_asset_freeze: pt.Global.current_application_address(),
                pt.TxnField.config_asset_reserve: pt.Global.current_application_address(),
                pt.TxnField.fee: pt.Int(0),
            }
        ),
        app.state.membership_token.set(pt.InnerTxn.created_asset_id()),
        output.set(app.state.membership_token),
    )


@app.external(authorize=beaker.Authorize.only_creator())
def remove_member(member: pt.abi.Address) -> pt.Expr:
    return pt.Pop(app.state.membership_records[member].delete())


@app.external(authorize=beaker.Authorize.only_creator())
def add_member(
    new_member: pt.abi.Account,
    membership_token: pt.abi.Asset = app.state.membership_token,  # type: ignore[assignment]
) -> pt.Expr:
    return pt.Seq(
        (role := pt.abi.Uint8()).set(pt.Int(0)),
        (voted := pt.abi.Bool()).set(FALSE),
        (mr := MembershipRecord()).set(role, voted),
        app.state.membership_records[new_member.address()].set(mr),
        pt.InnerTxnBuilder.Execute(
            {
                pt.TxnField.type_enum: pt.TxnType.AssetTransfer,
                pt.TxnField.xfer_asset: app.state.membership_token,
                pt.TxnField.asset_amount: pt.Int(1),
                pt.TxnField.asset_receiver: new_member.address(),
                pt.TxnField.fee: pt.Int(0),
                pt.TxnField.asset_sender: pt.Global.current_application_address(),
            }
        ),
    )


@app.external(authorize=beaker.Authorize.only_creator())
def update_role(member: pt.abi.Account, new_role: pt.abi.Uint8) -> pt.Expr:
    return pt.Seq(
        (mr := MembershipRecord()).decode(
            app.state.membership_records[member.address()].get()
        ),
        # retain their voted status
        (voted := pt.abi.Bool()).set(mr.voted),
        mr.set(new_role, voted),
        app.state.membership_records[member.address()].set(mr),
    )


@app.external
def get_membership_record(
    member: pt.abi.Address, *, output: MembershipRecord
) -> pt.Expr:
    return app.state.membership_records[member].store_into(output)


@app.external(authorize=beaker.Authorize.holds_token(app.state.membership_token))
def set_affirmation(
    idx: pt.abi.Uint16,
    affirmation: Affirmation,
    membership_token: pt.abi.Asset = app.state.membership_token,  # type: ignore[assignment]
) -> pt.Expr:
    return app.state.affirmations[idx.get()].set(affirmation)


@app.external(authorize=beaker.Authorize.holds_token(app.state.membership_token))
def get_affirmation(
    membership_token: pt.abi.Asset = app.state.membership_token,  # type: ignore[assignment]
    *,
    output: Affirmation,
) -> pt.Expr:
    return output.set(
        app.state.affirmations[pt.Global.round() % app.state.affirmations.elements]
    )