Skip to content

Commit

Permalink
Box support for Application calls and Atomic Transaction Composer (#338)
Browse files Browse the repository at this point in the history
* starting to add box support

* Add boxes to atc

* Format files with black

* Add boxes docstring

* Add boxes support for appl creation

* Update cucumber steps for appl txn encoding for boxes

* Formatting

* Point testing branch to box-reference (WIP)

* Sort imports on relevant files and format

* Add translation for foreign apps in box refs and some local unit tests

* Add some invalid cases for box translation

* Check for None when iterating box refs

* Add self app id references and tests

* Minor changes for box support

* Split boxref to separate file

* Change box name type to bytes

* Change docs references from box string to bytes

* Refactor cucumber steps

* Refactoring code and adding docstrings

* Add another comment

* Change test steps to encode box args like app args

* Add safety checks for box references

* Add some detailed errors and refactor

* Format unit test

* Fix foreign index error and revise undictify method

* Finish merging cucumber steps

* Formatting

* Fix box tests again

* Encoded as bytes unit test (#344)

* Unit test for encoding.py's `encode_as_bytes()`

* Accept AttributeError for foreign apps array if it is referencing its own app id

* Add unit test for empty foreign app array

* Change unit test to pass in None foreign array

* Change undictify method for boxes

* Formatting

* Change test branch

* Change type hints for atc boxes

* Check for int type for box reference id

* Change type ignore annotation

Co-authored-by: Ben Guidarelli <[email protected]>
Co-authored-by: Zeph Grunschlag <[email protected]>
  • Loading branch information
3 people authored Jun 15, 2022
1 parent faa13be commit d056766
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 59 deletions.
8 changes: 6 additions & 2 deletions algosdk/atomic_transaction_composer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from abc import ABC, abstractmethod
import base64
import copy
from abc import ABC, abstractmethod
from enum import IntEnum
from typing import Any, List, Optional, TypeVar, Union
from typing import Any, List, Optional, Tuple, TypeVar, Union

from algosdk import abi, error
from algosdk.abi.address_type import AddressType
Expand Down Expand Up @@ -173,6 +173,7 @@ def add_method_call(
note: bytes = None,
lease: bytes = None,
rekey_to: str = None,
boxes: List[Tuple[int, bytes]] = None,
) -> "AtomicTransactionComposer":
"""
Add a smart contract method call to this atomic group.
Expand Down Expand Up @@ -210,6 +211,7 @@ def add_method_call(
with the same sender and lease can be confirmed in this
transaction's valid rounds
rekey_to (str, optional): additionally rekey the sender to this address
boxes (list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access
"""
if self.status != AtomicTransactionComposerStatus.BUILDING:
Expand Down Expand Up @@ -259,6 +261,7 @@ def add_method_call(
accounts = accounts[:] if accounts else []
foreign_apps = foreign_apps[:] if foreign_apps else []
foreign_assets = foreign_assets[:] if foreign_assets else []
boxes = boxes[:] if boxes else []

app_args = []
raw_values = []
Expand Down Expand Up @@ -350,6 +353,7 @@ def add_method_call(
lease=lease,
rekey_to=rekey_to,
extra_pages=extra_pages,
boxes=boxes,
)
txn_with_signer = TransactionWithSigner(method_txn, signer)
txn_list.append(txn_with_signer)
Expand Down
94 changes: 94 additions & 0 deletions algosdk/box_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from collections import OrderedDict
from typing import List, Tuple, Union

from algosdk import encoding, error


class BoxReference:
"""
Represents a box reference with a foreign app index and the box name.
Args:
app_index (int): index of the application in the foreign app array
name (bytes): key for the box in bytes
"""

def __init__(self, app_index: int, name: bytes):
if app_index < 0:
raise ValueError(
f"Box app index must be a non-negative integer: {app_index}"
)
self.app_index = app_index
self.name = name

@staticmethod
def translate_box_reference(
ref: Tuple[int, Union[bytes, bytearray, str, int]],
foreign_apps: List[int],
this_app_id: int,
) -> "BoxReference":
# Try checking reference id and name type.
ref_id, ref_name = ref[0], encoding.encode_as_bytes(ref[1])
if not isinstance(ref_id, int):
raise TypeError("Box reference ID must be an int")

index = 0
try:
# Foreign apps start from index 1; index 0 is its own app ID.
index = foreign_apps.index(ref_id) + 1
except (ValueError, AttributeError):
# Check if the app referenced is itself after checking the
# foreign apps array (in case its own app id is in its own
# foreign apps array).
if ref_id != 0 and ref_id != this_app_id:
raise error.InvalidForeignIndexError(
f"Box ref with appId {ref_id} not in foreign-apps"
)
return BoxReference(index, ref_name)

@staticmethod
def translate_box_references(
references: List[Tuple[int, Union[bytes, bytearray, str, int]]],
foreign_apps: List[int],
this_app_id: int,
) -> List["BoxReference"]:
"""
Translates a list of tuples with app IDs and names into an array of
BoxReferences with foreign indices.
Args:
references (list[(int, bytes)]): list of tuples specifying app id
and key for boxes the app may access
foreign_apps (list[int]): list of other applications in appl call
this_app_id (int): app ID of the box being references
"""
if not references:
return []

return [
BoxReference.translate_box_reference(
ref, foreign_apps, this_app_id
)
for ref in references
]

def dictify(self):
d = dict()
if self.app_index:
d["i"] = self.app_index
if self.name:
d["n"] = self.name
od = OrderedDict(sorted(d.items()))
return od

@staticmethod
def undictify(d):
return BoxReference(
d["i"] if "i" in d else None,
d["n"] if "n" in d else None,
)

def __eq__(self, other):
if not isinstance(other, BoxReference):
return False
return self.app_index == other.app_index and self.name == other.name
21 changes: 19 additions & 2 deletions algosdk/encoding.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import base64
import msgpack
from collections import OrderedDict
from typing import Union

import msgpack
from Cryptodome.Hash import SHA512
from . import transaction, error, auction, constants, future

from algosdk import auction, constants, error, future, transaction


def msgpack_encode(obj):
Expand Down Expand Up @@ -235,3 +238,17 @@ def checksum(data):
chksum = SHA512.new(truncate="256")
chksum.update(data)
return chksum.digest()


def encode_as_bytes(
e: Union[bytes, bytearray, str, int]
) -> Union[bytes, bytearray]:
"""Confirm or coerce element to bytes."""
if isinstance(e, (bytes, bytearray)):
return e
if isinstance(e, str):
return e.encode()
if isinstance(e, int):
# Uses 8 bytes, big endian to match TEAL's btoi
return e.to_bytes(8, "big") # raises for negative or too big
raise TypeError("{} is not bytes, bytearray, str, or int".format(e))
5 changes: 5 additions & 0 deletions algosdk/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,8 @@ def __init__(self, msg):
class AtomicTransactionComposerError(Exception):
def __init__(self, msg):
super().__init__(msg)


class InvalidForeignIndexError(Exception):
def __init__(self, msg):
super().__init__(msg)
Loading

0 comments on commit d056766

Please sign in to comment.