Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ See which `algorand-python` stubs are implemented by the `algorand-python-testin
| Application | Emulated |
| subroutine | Emulated |
| Global | Emulated |
| op.Box.\* | Emulated |
| Box | Emulated |
| BoxRef | Emulated |
| BoxMap | Emulated |
| Block | Emulated |
| logicsig | Emulated |
| log | Emulated |
Expand Down
30 changes: 28 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,35 @@ To be documented...

### Boxes

To be documented...
The higher-level Boxes interface, introduced in version 2.1.0, along with all low-level Box 'op' calls, are available.

> NOTE: Higher level Boxes interface introduce in v2.1.0 is not supported yet, however all low level Box 'op' calls are available.
```py
import algopy

# Check and mark the sender's POA claim in the Box by their address
# to prevent duplicates using low-level Box 'op' calls.
_id, has_claimed = algopy.op.Box.get(algopy.Txn.sender.bytes)
assert not has_claimed, "Already claimed POA"
algopy.op.Box.put(algopy.Txn.sender.bytes, algopy.op.itob(minted_asset.id))

# Utilizing the higher-level 'Box' interface for an alternative implementation.
box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
has_claimed = bool(box)
assert not has_claimed, "Already claimed POA"
box.value = minted_asset.id

# Utilizing the higher-level 'BoxRef' interface for an alternative implementation.
box_ref = algopy.BoxRef(key=algopy.Txn.sender.bytes)
has_claimed = bool(box_ref)
assert not has_claimed, "Already claimed POA"
box_ref.put(algopy.op.itob(minted_asset.id))

# Utilizing the higher-level 'BoxMap' interface for an alternative implementation.
box_map = algopy.BoxMap(algopy.Bytes, algopy.UInt64, key_prefix="box_map")
has_claimed = algopy.Txn.sender.bytes in self.box_map
assert not has_claimed, "Already claimed POA"
self.box_map[algopy.Txn.sender.bytes] = minted_asset.id
```

## Smart Signatures

Expand Down
Empty file added examples/box/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions examples/box/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from algopy import ARC4Contract, Box, OnCompleteAction, TransactionType, arc4, op


class BoxContract(ARC4Contract):

def __init__(self) -> None:
self.oca = Box(OnCompleteAction)
self.txn = Box(TransactionType)

@arc4.abimethod()
def store_enums(self) -> None:
self.oca.value = OnCompleteAction.OptIn
self.txn.value = TransactionType.ApplicationCall

@arc4.abimethod()
def read_enums(self) -> tuple[OnCompleteAction, TransactionType]:
assert op.Box.get(b"oca")[0] == op.itob(self.oca.value)
assert op.Box.get(b"txn")[0] == op.itob(self.txn.value)

return self.oca.value, self.txn.value
26 changes: 26 additions & 0 deletions examples/box/test_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from collections.abc import Generator

import pytest
from algopy import op
from algopy_testing import AlgopyTestContext, algopy_testing_context

from .contract import BoxContract


@pytest.fixture()
def context() -> Generator[AlgopyTestContext, None, None]:
with algopy_testing_context() as ctx:
yield ctx


def test_enums(context: AlgopyTestContext) -> None:
# Arrange
contract = BoxContract()

# Act
contract.store_enums()
oca, txn = contract.read_enums()

# Assert
assert context.get_box(b"oca") == op.itob(oca)
assert context.get_box(b"txn") == op.itob(txn)
118 changes: 118 additions & 0 deletions examples/proof_of_attendance/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def __init__(self) -> None:
self.max_attendees = algopy.UInt64(30)
self.asset_url = algopy.String("ipfs://QmW5vERkgeJJtSY1YQdcWU6gsHCZCyLFtM1oT9uyy2WGm8")
self.total_attendees = algopy.UInt64(0)
self.box_map = algopy.BoxMap(algopy.Bytes, algopy.UInt64)

@algopy.arc4.abimethod(create="require")
def init(self, max_attendees: algopy.UInt64) -> None:
Expand All @@ -24,12 +25,70 @@ def confirm_attendance(self) -> None:

algopy.op.Box.put(algopy.Txn.sender.bytes, algopy.op.itob(minted_asset.id))

@algopy.arc4.abimethod()
def confirm_attendance_with_box(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

minted_asset = self._mint_poa(algopy.Txn.sender)
self.total_attendees += 1

box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
has_claimed = bool(box)
assert not has_claimed, "Already claimed POA"

box.value = minted_asset.id

@algopy.arc4.abimethod()
def confirm_attendance_with_box_ref(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

minted_asset = self._mint_poa(algopy.Txn.sender)
self.total_attendees += 1

box_ref = algopy.BoxRef(key=algopy.Txn.sender.bytes)
has_claimed = bool(box_ref)
assert not has_claimed, "Already claimed POA"

box_ref.put(algopy.op.itob(minted_asset.id))

@algopy.arc4.abimethod()
def confirm_attendance_with_box_map(self) -> None:
assert self.total_attendees < self.max_attendees, "Max attendees reached"

minted_asset = self._mint_poa(algopy.Txn.sender)
self.total_attendees += 1

has_claimed = algopy.Txn.sender.bytes in self.box_map
assert not has_claimed, "Already claimed POA"

self.box_map[algopy.Txn.sender.bytes] = minted_asset.id

@algopy.arc4.abimethod(readonly=True)
def get_poa_id(self) -> algopy.UInt64:
poa_id, exists = algopy.op.Box.get(algopy.Txn.sender.bytes)
assert exists, "POA not found"
return algopy.op.btoi(poa_id)

@algopy.arc4.abimethod(readonly=True)
def get_poa_id_with_box(self) -> algopy.UInt64:
box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
poa_id, exists = box.maybe()
assert exists, "POA not found"
return poa_id

@algopy.arc4.abimethod(readonly=True)
def get_poa_id_with_box_ref(self) -> algopy.UInt64:
box_ref = algopy.BoxRef(key=algopy.Txn.sender.bytes)
poa_id, exists = box_ref.maybe()
assert exists, "POA not found"
return algopy.op.btoi(poa_id)

@algopy.arc4.abimethod(readonly=True)
def get_poa_id_with_box_map(self) -> algopy.UInt64:
poa_id, exists = self.box_map.maybe(algopy.Txn.sender.bytes)
assert exists, "POA not found"
return poa_id

@algopy.arc4.abimethod()
def claim_poa(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
poa_id, exists = algopy.op.Box.get(algopy.Txn.sender.bytes)
Expand All @@ -49,6 +108,65 @@ def claim_poa(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
algopy.op.btoi(poa_id),
)

@algopy.arc4.abimethod()
def claim_poa_with_box(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
box = algopy.Box(algopy.UInt64, key=algopy.Txn.sender.bytes)
poa_id, exists = box.maybe()
assert exists, "POA not found, attendance validation failed!"
assert opt_in_txn.xfer_asset.id == poa_id, "POA ID mismatch"
assert opt_in_txn.fee == algopy.UInt64(0), "We got you covered for free!"
assert opt_in_txn.asset_amount == algopy.UInt64(0)
assert (
opt_in_txn.sender == opt_in_txn.asset_receiver == algopy.Txn.sender
), "Opt-in transaction sender and receiver must be the same"
assert (
opt_in_txn.asset_close_to == opt_in_txn.rekey_to == algopy.Global.zero_address
), "Opt-in transaction close to must be zero address"

self._send_poa(
algopy.Txn.sender,
poa_id,
)

@algopy.arc4.abimethod()
def claim_poa_with_box_ref(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
box_ref = algopy.BoxRef(key=algopy.Txn.sender.bytes)
poa_id, exists = box_ref.maybe()
assert exists, "POA not found, attendance validation failed!"
assert opt_in_txn.xfer_asset.id == algopy.op.btoi(poa_id), "POA ID mismatch"
assert opt_in_txn.fee == algopy.UInt64(0), "We got you covered for free!"
assert opt_in_txn.asset_amount == algopy.UInt64(0)
assert (
opt_in_txn.sender == opt_in_txn.asset_receiver == algopy.Txn.sender
), "Opt-in transaction sender and receiver must be the same"
assert (
opt_in_txn.asset_close_to == opt_in_txn.rekey_to == algopy.Global.zero_address
), "Opt-in transaction close to must be zero address"

self._send_poa(
algopy.Txn.sender,
algopy.op.btoi(poa_id),
)

@algopy.arc4.abimethod()
def claim_poa_with_box_map(self, opt_in_txn: algopy.gtxn.AssetTransferTransaction) -> None:
poa_id, exists = self.box_map.maybe(algopy.Txn.sender.bytes)
assert exists, "POA not found, attendance validation failed!"
assert opt_in_txn.xfer_asset.id == poa_id, "POA ID mismatch"
assert opt_in_txn.fee == algopy.UInt64(0), "We got you covered for free!"
assert opt_in_txn.asset_amount == algopy.UInt64(0)
assert (
opt_in_txn.sender == opt_in_txn.asset_receiver == algopy.Txn.sender
), "Opt-in transaction sender and receiver must be the same"
assert (
opt_in_txn.asset_close_to == opt_in_txn.rekey_to == algopy.Global.zero_address
), "Opt-in transaction close to must be zero address"

self._send_poa(
algopy.Txn.sender,
poa_id,
)

@algopy.subroutine
def _mint_poa(self, claimer: algopy.Account) -> algopy.Asset:
algopy.ensure_budget(algopy.UInt64(10000), algopy.OpUpFeeSource.AppAccount)
Expand Down
40 changes: 33 additions & 7 deletions examples/proof_of_attendance/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,46 @@ def test_init(context: AlgopyTestContext) -> None:
assert contract.max_attendees == max_attendees


@pytest.mark.parametrize(
("confirm_attendance", "key_prefix"),
[
("confirm_attendance", b""),
("confirm_attendance_with_box", b""),
("confirm_attendance_with_box_ref", b""),
("confirm_attendance_with_box_map", b"box_map"),
],
)
def test_confirm_attendance(
context: AlgopyTestContext,
confirm_attendance: str,
key_prefix: bytes,
) -> None:
# Arrange
contract = ProofOfAttendance()
contract.max_attendees = context.any_uint64(1, 100)

# Act
contract.confirm_attendance()
confirm = getattr(contract, confirm_attendance)
confirm()

# Assert
assert context.get_box(context.default_creator.bytes) == algopy.op.itob(1)


def test_claim_poa(context: AlgopyTestContext) -> None:
assert context.get_box(key_prefix + context.default_creator.bytes) == algopy.op.itob(1)


@pytest.mark.parametrize(
("claim_poa", "key_prefix"),
[
("claim_poa", b""),
("claim_poa_with_box", b""),
("claim_poa_with_box_ref", b""),
("claim_poa_with_box_map", b"box_map"),
],
)
def test_claim_poa(
context: AlgopyTestContext,
claim_poa: str,
key_prefix: bytes,
) -> None:
# Arrange
contract = ProofOfAttendance()
dummy_poa = context.any_asset()
Expand All @@ -54,10 +79,11 @@ def test_claim_poa(context: AlgopyTestContext) -> None:
fee=algopy.UInt64(0),
asset_amount=algopy.UInt64(0),
)
context.set_box(context.default_creator.bytes, algopy.op.itob(dummy_poa.id))
context.set_box(key_prefix + context.default_creator.bytes, algopy.op.itob(dummy_poa.id))

# Act
contract.claim_poa(opt_in_txn)
claim = getattr(contract, claim_poa)
claim(opt_in_txn)

# Assert
axfer_itxn = context.get_submitted_itxn_group(-1).asset_transfer(0)
Expand Down
6 changes: 6 additions & 0 deletions src/algopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
GTxn,
ITxn,
LogicSig,
StateTotals,
TemplateVar,
Txn,
logicsig,
uenumerate,
urange,
)
from algopy_testing.models.box import Box, BoxMap, BoxRef
from algopy_testing.primitives import BigUInt, Bytes, String, UInt64
from algopy_testing.protocols import BytesBacked
from algopy_testing.state import GlobalState, LocalState
Expand Down Expand Up @@ -55,4 +58,7 @@
"subroutine",
"uenumerate",
"urange",
"Box",
"BoxRef",
"BoxMap",
]
20 changes: 12 additions & 8 deletions src/algopy_testing/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def __init__(
self._scratch_spaces: dict[str, list[algopy.Bytes | algopy.UInt64 | bytes | int]] = {}
self._template_vars: dict[str, Any] = template_vars or {}
self._blocks: dict[int, dict[str, int]] = {}
self._boxes: dict[bytes, algopy.Bytes] = {}
self._boxes: dict[bytes, bytes] = {}
self._lsigs: dict[algopy.LogicSig, Callable[[], algopy.UInt64 | bool]] = {}
self._active_lsig_args: Sequence[algopy.Bytes] = []

Expand Down Expand Up @@ -1047,20 +1047,23 @@ def any_transaction( # type: ignore[misc]

return new_txn

def get_box(self, name: algopy.Bytes | bytes) -> algopy.Bytes:
def does_box_exist(self, name: algopy.Bytes | bytes) -> bool:
"""return true if the box with the given name exists."""
name_bytes = name if isinstance(name, bytes) else name.value
return name_bytes in self._boxes

def get_box(self, name: algopy.Bytes | bytes) -> bytes:
"""Get the content of a box."""
import algopy

name_bytes = name if isinstance(name, bytes) else name.value
return self._boxes.get(name_bytes, algopy.Bytes(b""))
return self._boxes.get(name_bytes, b"")

def set_box(self, name: algopy.Bytes | bytes, content: algopy.Bytes | bytes) -> None:
"""Set the content of a box."""
import algopy

name_bytes = name if isinstance(name, bytes) else name.value
content_bytes = content if isinstance(content, bytes) else content.value
self._boxes[name_bytes] = algopy.Bytes(content_bytes)
self._boxes[name_bytes] = content_bytes

def execute_logicsig(
self, lsig: algopy.LogicSig, lsig_args: Sequence[algopy.Bytes] | None = None
Expand All @@ -1071,12 +1074,14 @@ def execute_logicsig(
self._lsigs[lsig] = lsig.func
return lsig.func()

def clear_box(self, name: algopy.Bytes | bytes) -> None:
def clear_box(self, name: algopy.Bytes | bytes) -> bool:
"""Clear the content of a box."""

name_bytes = name if isinstance(name, bytes) else name.value
if name_bytes in self._boxes:
del self._boxes[name_bytes]
return True
return False

def clear_all_boxes(self) -> None:
"""Clear all boxes."""
Expand Down Expand Up @@ -1170,7 +1175,6 @@ def reset(self) -> None:
self._app_id = iter(range(1, 2**64))


#
_var: ContextVar[AlgopyTestContext] = ContextVar("_var")


Expand Down
Loading