Skip to content

Commit

Permalink
feat: Box storage api
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel authored and achidlow committed Jun 25, 2024
1 parent 49d366e commit c41ce5e
Show file tree
Hide file tree
Showing 71 changed files with 11,945 additions and 710 deletions.
4 changes: 4 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
display: none;
}
}

.py.class {
margin-top: 3rem;
}
91 changes: 88 additions & 3 deletions docs/lg-storage.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Storing data on-chain

Algorand smart contracts have [three different types of on-chain storage](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/state/)
they can utilise: [Global storage](#global-storage), [Local storage](#local-storage) and [Scratch storage](#scratch-storage).
they can utilise: [Global storage](#global-storage), [Local storage](#local-storage), [Box Storage](#box-storage), and [Scratch storage](#scratch-storage).

The life-cycle of a smart contract matches the semantics of Python classes when you consider
deploying a smart contract as "instantiating" the class. Any calls to that smart contract are made
Expand Down Expand Up @@ -88,9 +88,94 @@ any [generated typed clients](https://github.com/algorandfoundation/algokit-cli/

## Box storage

There isn't currently first-class support via `self.` for box storage, but this feature is coming soon.
We provide 3 different types for accessing box storage: [Box](./api-algopy.md#algopy.Box), [BoxMap](./api-algopy.md#algopy.BoxMap), and [BoxBlob](./api-algopy.md#algopy.BoxBlob). We also expose raw operations via the [AVM ops](./lg-ops.md) module.

In the meantime, you can use the box storage [AVM ops](./lg-ops.md) to interact with box storage.
Before using box storage, be sure to familiarise yourself with the [requirements and restrictions](https://developer.algorand.org/articles/smart-contract-storage-boxes/) of the underlying API.

The `Box` type provides an abstraction over storing a single value in a single box. A box can be declared against `self`
in an `__init__` method (in which case the key must be a compile time constant); or as a local variable within any
subroutine. `Box` proxy instances can be passed around like any other value.

Once declared, you can interact with the box via its instance methods.


```python
import typing as t
from algopy import Box, arc4, Contract, op


class MyContract(Contract):
def __init__(self) -> None:
self.box_a = Box(arc4.StaticArray[arc4.UInt32, t.Literal[20]], key=b"a")

def approval_program(self) -> bool:
box_b = Box(arc4.String, key=b"b")
box_b.value = arc4.String("Hello")
# Check if the box exists
if self.box_a:
# Reassign the value
self.box_a.value[2] = arc4.UInt32(40)
else:
# Assign a new value
self.box_a.value = arc4.StaticArray[arc4.UInt32, t.Literal[20]].from_bytes(op.bzero(20 * 4))
# Read a value
return self.box_a.value[4] == arc4.UInt32(2)
```

`BoxMap` is similar to the `Box` type, but allows for grouping a set of boxes with a common key and content type. A `key_prefix` can optionally be provided.
The key can be a `Bytes` value, or anything that can be converted to `Bytes`. The final box name is the combination of `key_prefix + key`.

```python
from algopy import BoxMap, Contract, Account, Txn, String

class MyContract(Contract):
def __init__(self) -> None:
self.my_map = BoxMap(Account, String, key_prefix=b"a_")

def approval_program(self) -> bool:
# Check if the box exists
if Txn.sender in self.my_map:
# Reassign the value
self.my_map[Txn.sender] = String(" World")
else:
# Assign a new value
self.my_map[Txn.sender] = String("Hello")
# Read a value
return self.my_map[Txn.sender] == String("Hello World")
```

`BoxBlob` is a specialised type for interacting with boxes which contain binary data. In addition to being able to set and read the box value, there are operations for extracting and replacing just a portion of the box data which
is useful for minimizing the amount of reads and writes required, but also allows you to interact with byte arrays which are longer than the AVM can support (currently 4096).

```python
from algopy import BoxBlob, Contract, Global, Txn



class MyContract(Contract):
def approval_program(self) -> bool:
my_blob = BoxBlob(key=b"blob")

sender_bytes = Txn.sender.bytes
app_address = Global.current_application_address.bytes
assert my_blob.create(8000)
my_blob.replace(0, sender_bytes)
my_blob.splice(0, 0, app_address)
first_64 = my_blob.extract(0, 32 * 2)
assert first_64 == app_address + sender_bytes
assert my_blob.delete()
value, exists = my_blob.maybe()
assert not exists
assert my_blob.get(default=sender_bytes) == sender_bytes
my_blob.create(sender_bytes + app_address)
assert my_blob, "Blob exists"
assert my_blob.length == 64
return True
```



If none of these abstractions suit your needs, you can use the box storage [AVM ops](./lg-ops.md) to interact with box storage. These ops match closely to the opcodes available on the AVM.

For example:

Expand Down
93 changes: 93 additions & 0 deletions examples/box_storage/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import typing

from algopy import Box, BoxBlob, BoxMap, Bytes, Global, String, Txn, UInt64, arc4

StaticInts: typing.TypeAlias = arc4.StaticArray[arc4.UInt8, typing.Literal[4]]


class BoxContract(arc4.ARC4Contract):
def __init__(self) -> None:
self.box_a = Box(UInt64, key=b"BOX_A")
self.box_b = Box(Bytes, key=b"b")
self.box_c = Box(arc4.String, key=b"BOX_C")
self.box_map = BoxMap(UInt64, String)

@arc4.abimethod
def set_boxes(self, a: UInt64, b: Bytes, c: arc4.String) -> None:
self.box_a.value = a
self.box_b.value = b
self.box_c.value = c

self.box_a.value += 3

@arc4.abimethod
def read_boxes(self) -> tuple[UInt64, Bytes, arc4.String]:
return self.box_a.value, self.box_b.value, self.box_c.value

@arc4.abimethod
def boxes_exist(self) -> tuple[bool, bool, bool]:
return bool(self.box_a), bool(self.box_b), bool(self.box_c)

@arc4.abimethod
def slice_box(self) -> None:
box_0 = Box(Bytes, key=b"0")
box_0.value = Bytes(b"Testing testing 123")
assert box_0.value[0:7] == b"Testing"

self.box_c.value = arc4.String("Hello")
assert self.box_c.value.bytes[2:10] == b"Hello"

@arc4.abimethod
def arc4_box(self) -> None:
box_d = Box(StaticInts, key=b"d")
box_d.value = StaticInts(arc4.UInt8(0), arc4.UInt8(1), arc4.UInt8(2), arc4.UInt8(3))

assert box_d.value[0] == 0
assert box_d.value[1] == 1
assert box_d.value[2] == 2
assert box_d.value[3] == 3

@arc4.abimethod
def box_blob(self) -> None:
box_blob = BoxBlob(key=b"blob")
sender_bytes = Txn.sender.bytes
app_address = Global.current_application_address.bytes
assert box_blob.create(size=8000)
box_blob.replace(0, sender_bytes)
box_blob.splice(0, 0, app_address)
first_64 = box_blob.extract(0, 32 * 2)
assert first_64 == app_address + sender_bytes
assert box_blob.delete()

value, exists = box_blob.maybe()
assert not exists
assert box_blob.get(default=sender_bytes) == sender_bytes
box_blob.create(sender_bytes + app_address)
assert box_blob, "Blob exists"
assert box_blob.length == 64

@arc4.abimethod
def box_map_test(self) -> None:
key_0 = UInt64(0)
key_1 = UInt64(1)
value = String("Hmmmmm")
self.box_map[key_0] = value
assert self.box_map[key_0].bytes.length == value.bytes.length
assert self.box_map.length(key_0) == value.bytes.length

assert self.box_map.get(key_1, default=String("default")) == String("default")
value, exists = self.box_map.maybe(key_1)
assert not exists
assert key_0 in self.box_map

@arc4.abimethod
def box_map_set(self, key: UInt64, value: String) -> None:
self.box_map[key] = value

@arc4.abimethod
def box_map_get(self, key: UInt64) -> String:
return self.box_map[key]

@arc4.abimethod
def box_map_exists(self, key: UInt64) -> bool:
return key in self.box_map
Loading

0 comments on commit c41ce5e

Please sign in to comment.