Skip to content

Commit d6d2cf4

Browse files
authored
Merge pull request #7 from barnjamin/abi-examples
Abi examples
2 parents 54408ee + 73c4f82 commit d6d2cf4

File tree

17 files changed

+532
-1
lines changed

17 files changed

+532
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ABI Examples
2+
------------
3+
4+
Demonstrates some usage of the PyTeal ABI types.
5+
6+
ABI Types are defined in the [ARC-4 spec](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md) and are very useful for a standard encoding of complex types. They allow for easier interoperability with other contracts that use the same encoding.
7+
8+
9+
See https://pyteal.readthedocs.io/en/stable/abi.html for more information on the PyTeal types.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import beaker
2+
import pyteal as pt
3+
4+
5+
from named_tuple_blueprint import named_tuple_blueprint
6+
from array_blueprint import array_blueprint
7+
8+
app = beaker.Application("ABIExample")
9+
app.apply(named_tuple_blueprint)
10+
app.apply(array_blueprint)
11+
12+
13+
@app.external
14+
def echo(v: pt.abi.String, *, output: pt.abi.String) -> pt.Expr:
15+
"""echos the string back unchanged"""
16+
return output.set(v)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from typing import Literal
2+
3+
import beaker
4+
import pyteal as pt
5+
6+
7+
# util method for converting an Int to u16 bytes
8+
def to_u16(i: pt.Expr) -> pt.Expr:
9+
return pt.Suffix(pt.Itob(i), pt.Int(6))
10+
11+
12+
def array_blueprint(app: beaker.Application) -> None:
13+
@app.external
14+
def sum_dynamic_array(
15+
v: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64
16+
) -> pt.Expr:
17+
"""sums the array of ints"""
18+
return pt.Seq(
19+
# Use a scratch var to store the running sum
20+
(running_sum := pt.ScratchVar(pt.TealType.uint64)).store(pt.Int(0)),
21+
# Iterate over the elements of the array
22+
pt.For(
23+
(i := pt.ScratchVar()).store(pt.Int(0)),
24+
i.load() < v.length(),
25+
i.store(i.load() + pt.Int(1)),
26+
).Do(
27+
# Access the element with square bracket annotation
28+
# and call `use` on it to use the value since its a
29+
# computed type like tuple elements
30+
v[i.load()].use(
31+
lambda val: running_sum.store(running_sum.load() + val.get())
32+
)
33+
),
34+
# Set the value we're returning
35+
output.set(running_sum.load()),
36+
)
37+
38+
# While its not advisable to make heavy use dynamic ABI
39+
# types within the logic of the contract due to the inefficient
40+
# access to elements, below are some examples of how you
41+
# might construct a larger array from 2 smaller ones
42+
@app.external
43+
def concat_static_arrays(
44+
a: pt.abi.StaticArray[pt.abi.Uint64, Literal[3]],
45+
b: pt.abi.StaticArray[pt.abi.Uint64, Literal[3]],
46+
*,
47+
output: pt.abi.StaticArray[pt.abi.Uint64, Literal[6]],
48+
) -> pt.Expr:
49+
# Static arrays are easy to concat since there is no
50+
# length prefix or offsets to track. The typing of the
51+
# value includes the length explicitly.
52+
return output.decode(pt.Concat(a.encode(), b.encode()))
53+
54+
@app.external
55+
def concat_dynamic_arrays(
56+
a: pt.abi.DynamicArray[pt.abi.Uint64],
57+
b: pt.abi.DynamicArray[pt.abi.Uint64],
58+
*,
59+
output: pt.abi.DynamicArray[pt.abi.Uint64],
60+
) -> pt.Expr:
61+
"""demonstrate how two dynamic arrays of static elements could be concat'd"""
62+
# A Dynamic array of static types is encoded as:
63+
# [uint16 length, element 0, element 1]
64+
# so to concat them, we must remove the 2 byte length prefix
65+
# from each, and prepend the new length (of elements!) as 2 byte integer
66+
return output.decode(
67+
pt.Concat(
68+
pt.Suffix(pt.Itob(a.length() + b.length()), pt.Int(6)),
69+
pt.Suffix(a.encode(), pt.Int(2)),
70+
pt.Suffix(b.encode(), pt.Int(2)),
71+
)
72+
)
73+
74+
@app.external
75+
def concat_dynamic_string_arrays(
76+
a: pt.abi.DynamicArray[pt.abi.String],
77+
b: pt.abi.DynamicArray[pt.abi.String],
78+
*,
79+
output: pt.abi.DynamicArray[pt.abi.String],
80+
) -> pt.Expr:
81+
"""demonstrate how two dynamic arrays of dynamic elements could be concat'd"""
82+
# NOTE: this is not efficient (clearly), static types should
83+
# always be preferred if possible. Otherwise use some encoding
84+
# other than the abi encoding, which is more for serializing/deserializing data
85+
86+
# A Dynamic array of dynamic types is encoded as:
87+
# [uint16 length, uint16 pos elem 0, uint16 pos elem 1, elem 0, elem 1]
88+
# so to concat them, we must remove the 2 byte length prefix
89+
# from each, and prepend the new length (of elements!) as 2 byte integer
90+
return pt.Seq(
91+
# Make a couple bufs for the header (offsets) and elements
92+
(_head_buf := pt.ScratchVar()).store(
93+
pt.Suffix(pt.Itob(a.length() + b.length()), pt.Int(6))
94+
),
95+
# Take the element contents of the 2 arrays
96+
(_tail_buf := pt.ScratchVar()).store(
97+
pt.Concat(
98+
# strip length and positions, now its [elem0, elem1, elem2]
99+
pt.Suffix(a.encode(), pt.Int(2) + (pt.Int(2) * a.length())),
100+
pt.Suffix(b.encode(), pt.Int(2) + (pt.Int(2) * b.length())),
101+
)
102+
),
103+
# Create the offset value we'll use for the position header
104+
# we know the first string will start at 2 * combined length
105+
(offset := pt.ScratchVar()).store(((a.length() + b.length()) * pt.Int(2))),
106+
# We'll track the current string we're working on here
107+
(curr_str_len := pt.ScratchVar()).store(pt.Int(0)),
108+
(cursor := pt.ScratchVar()).store(pt.Int(0)),
109+
pt.While(
110+
(cursor.load() + curr_str_len.load()) <= pt.Len(_tail_buf.load())
111+
).Do(
112+
# Add the offset for this string to the head buf
113+
_head_buf.store(pt.Concat(_head_buf.load(), to_u16(offset.load()))),
114+
# Get the length of the current string + 2 bytes for uint16 len
115+
curr_str_len.store(
116+
pt.ExtractUint16(_tail_buf.load(), cursor.load()) + pt.Int(2)
117+
),
118+
# update our cursor to point to the next str element
119+
cursor.store(cursor.load() + curr_str_len.load()),
120+
# update our offset similarly
121+
offset.store(offset.load() + curr_str_len.load()),
122+
),
123+
output.decode(pt.Concat(_head_buf.load(), _tail_buf.load())),
124+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Build the sample contract in this directory using Beaker and output to ./artifacts
2+
from pathlib import Path
3+
4+
import application
5+
6+
7+
def build() -> Path:
8+
"""Build the beaker app, export it to disk, and return the Path to the app spec file"""
9+
app_spec = application.app.build()
10+
output_dir = Path(__file__).parent / "artifacts"
11+
print(f"Dumping {app_spec.contract.name} to {output_dir}")
12+
app_spec.export(output_dir)
13+
return output_dir / "application.json"
14+
15+
16+
if __name__ == "__main__":
17+
build()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import application
2+
from beaker import client, sandbox
3+
4+
5+
def main() -> None:
6+
acct = sandbox.get_accounts().pop()
7+
algod_client = sandbox.get_algod_client()
8+
app_client = client.ApplicationClient(
9+
algod_client, application.app, signer=acct.signer
10+
)
11+
app_id, app_address, _ = app_client.create()
12+
print(f"Deployed Application ID: {app_id} Address: {app_address}")
13+
14+
result = app_client.call("sum_tuple_elements_with_use", t=(123, 456))
15+
print(f"{result.method.name} returned {result.return_value}")
16+
17+
result = app_client.call("sum_tuple_elements_with_store_into", t=(123, 456))
18+
print(f"{result.method.name} returned {result.return_value}")
19+
20+
result = app_client.call("sum_dynamic_array", v=[1, 2, 3, 4, 5, 6])
21+
print(f"{result.method.name} returned {result.return_value}")
22+
23+
result = app_client.call("concat_dynamic_arrays", a=[1, 2, 3], b=[4, 5, 6])
24+
print(f"{result.method.name} returned {result.return_value}")
25+
26+
result = app_client.call("concat_static_arrays", a=[1, 2, 3], b=[4, 5, 6])
27+
print(f"{result.method.name} returned {result.return_value}")
28+
29+
result = app_client.call(
30+
"concat_dynamic_string_arrays", a=["a", "b", "c"], b=["d", "e", "f"]
31+
)
32+
print(f"{result.method.name} returned {result.return_value}")
33+
34+
35+
if __name__ == "__main__":
36+
main()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import beaker
2+
import pyteal as pt
3+
4+
5+
class StructyTuple(pt.abi.NamedTuple):
6+
# Note that the type is wrapped in a `pt.abi.Field`
7+
a: pt.abi.Field[pt.abi.Uint64]
8+
b: pt.abi.Field[pt.abi.Uint64]
9+
10+
11+
def named_tuple_blueprint(app: beaker.Application) -> None:
12+
@app.external
13+
def sum_tuple_elements_with_use(
14+
t: StructyTuple, *, output: pt.abi.Uint64
15+
) -> pt.Expr:
16+
"""sum the elements of the tuple with `use` and lambda"""
17+
return pt.Seq(
18+
(running_sum := pt.ScratchVar(pt.TealType.uint64)).store(pt.Int(0)),
19+
# we can pass a lambda into the `use` method to access the value
20+
# as the abi type it was declared as
21+
t.a.use(lambda a: running_sum.store(running_sum.load() + a.get())),
22+
t.b.use(lambda b: running_sum.store(running_sum.load() + b.get())),
23+
output.set(running_sum.load()),
24+
)
25+
26+
@app.external
27+
def sum_tuple_elements_with_store_into(
28+
t: StructyTuple, *, output: pt.abi.Uint64
29+
) -> pt.Expr:
30+
"""sum the elements of the tuple with `.set` on matching abi type"""
31+
return pt.Seq(
32+
# we can pass the tuple element into a `set` method for the same type
33+
# under the covers this calls the `store_into` method on the element
34+
# with the abi type as the argument
35+
(a := pt.abi.Uint64()).set(t.a),
36+
(b := pt.abi.Uint64()).set(t.b),
37+
output.set(a.get() + b.get()),
38+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Blueprints
2+
----------
3+
4+
Blueprints are a great way to reuse existing code.
5+
6+
See https://algorand-devrel.github.io/beaker/html/usage.html#code-reuse for more

template_content/playground/blueprint/demo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from algokit_utils import LogicError
21
import application
32
from beaker import client, sandbox
43

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Boxes
2+
-----
3+
4+
Demonstrates some of the uses of the Layer 1 Storage primitive, Boxes.
5+
6+
PyTeal provides the basics for working with Boxes, see more here: https://pyteal.readthedocs.io/en/stable/state.html#box-storage
7+
8+
Beaker provides two higher level constructs for working with Boxes, see more here: https://algorand-devrel.github.io/beaker/html/boxes.html
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import beaker
2+
import pyteal as pt
3+
from beaker import consts
4+
from beaker.lib import storage
5+
6+
MAX_MEMBERS = 10
7+
8+
9+
class BoxExampleState:
10+
# Declares a BoxMapping, where each key is an address and each value is a uint64
11+
# every new key creates a new box.
12+
balances = storage.BoxMapping(pt.abi.Address, pt.abi.Uint64)
13+
14+
# Declares a BoxList, where each element is a 32 byte array and
15+
# creates a box with space for MAX_MEMBERS elements
16+
members = storage.BoxList(pt.abi.Address, MAX_MEMBERS)
17+
18+
# Add a global state value to track the total count of elements in the box
19+
member_count = beaker.GlobalStateValue(pt.TealType.uint64)
20+
21+
22+
app = beaker.Application("BoxExample", state=BoxExampleState())
23+
24+
25+
@app.external
26+
def bootstrap() -> pt.Expr:
27+
return pt.Seq(
28+
app.initialize_global_state(),
29+
# create returns a bool value indicating if the box was created
30+
# we just pop it here to discard it
31+
pt.Pop(app.state.members.create()),
32+
)
33+
34+
35+
@app.external
36+
def set_balance(addr: pt.abi.Address, amount: pt.abi.Uint64) -> pt.Expr:
37+
"""Sets the balance of an address"""
38+
return app.state.balances[addr].set(amount)
39+
40+
41+
@app.external(read_only=True)
42+
def read_balance(addr: pt.abi.Address, *, output: pt.abi.Uint64) -> pt.Expr:
43+
return output.decode(app.state.balances[addr].get())
44+
45+
46+
@app.external
47+
def add_member(addr: pt.abi.Address) -> pt.Expr:
48+
"""Adds a new member to the list"""
49+
return pt.Seq(
50+
pt.Assert(app.state.member_count < pt.Int(MAX_MEMBERS), comment="List is full"),
51+
app.state.members[app.state.member_count].set(addr),
52+
# Write a zero balance to the balance box
53+
# for this address
54+
app.state.balances[addr].set(pt.Itob(pt.Int(0))),
55+
app.state.member_count.increment(),
56+
)
57+
58+
59+
@app.external
60+
def fill_box(
61+
box_name: pt.abi.String, box_data: pt.abi.DynamicArray[pt.abi.String]
62+
) -> pt.Expr:
63+
return pt.BoxPut(box_name.get(), box_data.encode())
64+
65+
66+
def compute_min_balance(members: int):
67+
"""Compute the min balance for the app to hold the boxes we need"""
68+
return (
69+
consts.ASSET_MIN_BALANCE # Cover min bal for member token
70+
+ (
71+
consts.BOX_FLAT_MIN_BALANCE
72+
+ (pt.abi.size_of(pt.abi.Uint64) * consts.BOX_BYTE_MIN_BALANCE)
73+
)
74+
* members # cover min bal for balance boxes we might create
75+
+ (
76+
consts.BOX_FLAT_MIN_BALANCE
77+
+ (members * pt.abi.size_of(pt.abi.Address) * consts.BOX_BYTE_MIN_BALANCE)
78+
) # cover min bal for member list box
79+
)

0 commit comments

Comments
 (0)