Skip to content

Commit

Permalink
Merge pull request #3 from algorandfoundation/feat/unit-testing-boxes
Browse files Browse the repository at this point in the history
feat: stub implementation of Box, BoxRef and BoxMap
  • Loading branch information
boblat authored Jul 24, 2024
2 parents 44000c9 + 3b95140 commit 46f7493
Show file tree
Hide file tree
Showing 19 changed files with 1,788 additions and 131 deletions.
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

0 comments on commit 46f7493

Please sign in to comment.