Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ABI Type encoding support #238

Merged
merged 27 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
21f01cf
Add initial support for ABI types
algochoi Sep 24, 2021
b7089eb
Finish up abi types and values
algochoi Sep 28, 2021
5e4bd2b
Add doc strings
algochoi Sep 28, 2021
5428035
Merge branch 'develop' of https://github.com/algorand/py-algorand-sdk…
algochoi Sep 28, 2021
f2ee5d3
Refactor and finish encoding functions
algochoi Oct 1, 2021
9aa7984
More cleanup
algochoi Oct 1, 2021
867d285
Fix minor bugs and add number decoding
algochoi Oct 4, 2021
a8f0a9e
Bump version to 1.8.0 and add changelog
bricerisingalgorand Oct 4, 2021
23d0d3b
Add change to changelog
bricerisingalgorand Oct 4, 2021
7b62bab
Merge branch 'release/v1.8.0'
bricerisingalgorand Oct 4, 2021
77c111b
Add decoding for arrays and dynamic types
algochoi Oct 5, 2021
053d263
Merge branch 'master' of https://github.com/algorand/py-algorand-sdk …
algochoi Oct 5, 2021
0577857
Add more unit tests
algochoi Oct 5, 2021
75e901a
Minor change from PR comment
algochoi Oct 5, 2021
1e338f8
Another small PR comment change
algochoi Oct 5, 2021
8f622a8
Split up files and remove circular imports
algochoi Oct 6, 2021
e4fc085
Address PR comments
algochoi Oct 6, 2021
70ac9f5
Have arrays accept bytes and add docstrings
algochoi Oct 7, 2021
7191b12
Minor clean up
algochoi Oct 7, 2021
821cc81
Fix error messages for negative uint
algochoi Oct 7, 2021
6aa4c2a
Remove unnecessary imports
algochoi Oct 7, 2021
0c6a635
Refactor out abi_types since it is implicit by the class
algochoi Oct 7, 2021
7fc99ab
Tuples don't need static lengths...
algochoi Oct 8, 2021
d6015ee
Address PR comments 1
algochoi Oct 8, 2021
b9f7ff4
Fix head encoding placeholders
algochoi Oct 8, 2021
fadabcb
Fix the tuple docstring
algochoi Oct 8, 2021
d52ebb6
Formatting fixes
algochoi Oct 8, 2021
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.8.0
### Added

- Add wait_for_confirmation() to AlgodClient (#214)
- Support AVM 1.0 (#236)
- Support for < python 3.7 (#221)

### Bug Fixes
- Fix JSON decoding in AlgodHTTPError (#223)

## 1.7.0
### Added

Expand Down
1 change: 1 addition & 0 deletions algosdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import abi
from . import account
from . import algod
from . import auction
Expand Down
12 changes: 12 additions & 0 deletions algosdk/abi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .util import type_from_string
from .uint_type import UintType
from .ufixed_type import UfixedType
from .bool_type import BoolType
from .byte_type import ByteType
from .address_type import AddressType
from .string_type import StringType
from .array_dynamic_type import ArrayDynamicType
from .array_static_type import ArrayStaticType
from .tuple_type import TupleType

name = "abi"
88 changes: 88 additions & 0 deletions algosdk/abi/address_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from .base_type import Type
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error

from algosdk import encoding


class AddressType(Type):
"""
Represents an Address ABI Type for encoding.
"""

def __init__(self) -> None:
super().__init__()

def __eq__(self, other) -> bool:
if not isinstance(other, AddressType):
return False
return True

def __str__(self):
return "address"

def byte_len(self):
return 32

def is_dynamic(self):
return False

def _to_tuple_type(self):
child_type_array = list()
for _ in range(self.byte_len()):
child_type_array.append(ByteType())
return TupleType(child_type_array)

def encode(self, value):
"""
Encode an address string or a 32-byte public key into a Address ABI bytestring.

Args:
value (str | bytes): value to be encoded. It can be either a base32
address string or a 32-byte public key.

Returns:
bytes: encoded bytes of the address
"""
# Check that the value is an address in string or the public key in bytes
if isinstance(value, str):
try:
value = encoding.decode_address(value)
except Exception as e:
raise error.ABIEncodingError(
"cannot encode the following address: {}".format(value)
) from e
elif (
not (isinstance(value, bytes) or isinstance(value, bytearray))
or len(value) != 32
):
raise error.ABIEncodingError(
"cannot encode the following public key: {}".format(value)
)
return bytes(value)

def decode(self, bytestring):
"""
Decodes a bytestring to a base32 encoded address string.

Args:
bytestring (bytes | bytearray): bytestring to be decoded

Returns:
str: base32 encoded address from the encoded bytestring
"""
if (
not (
isinstance(bytestring, bytearray)
or isinstance(bytestring, bytes)
)
or len(bytestring) != 32
):
raise error.ABIEncodingError(
"address string must be in bytes and correspond to a byte[32]: {}".format(
bytestring
)
)
# Return the base32 encoded address string
return encoding.encode_address(bytestring)
98 changes: 98 additions & 0 deletions algosdk/abi/array_dynamic_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from .base_type import ABI_LENGTH_SIZE, Type
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error


class ArrayDynamicType(Type):
"""
Represents a ArrayDynamic ABI Type for encoding.

Args:
child_type (Type): the type of the dynamic array.

Attributes:
child_type (Type)
"""

def __init__(self, arg_type) -> None:
super().__init__()
self.child_type = arg_type

def __eq__(self, other) -> bool:
if not isinstance(other, ArrayDynamicType):
return False
return self.child_type == other.child_type

def __str__(self):
return "{}[]".format(self.child_type)

def byte_len(self):
raise error.ABITypeError(
"cannot get length of a dynamic type: {}".format(self)
)

def is_dynamic(self):
return True

def _to_tuple_type(self, length):
child_type_array = [self.child_type] * length
return TupleType(child_type_array)

def encode(self, value_array):
"""
Encodes a list of values into a ArrayDynamic ABI bytestring.

Args:
value_array (list | bytes | bytearray): list of values to be encoded.
If the child types are ByteType, then bytes or bytearray can be
passed in to be encoded as well.

Returns:
bytes: encoded bytes of the dynamic array
"""
if (
isinstance(value_array, bytes)
or isinstance(value_array, bytearray)
) and not isinstance(self.child_type, ByteType):
raise error.ABIEncodingError(
"cannot pass in bytes when the type of the array is not ByteType: {}".format(
value_array
)
)
converted_tuple = self._to_tuple_type(len(value_array))
length_to_encode = len(converted_tuple.child_types).to_bytes(
2, byteorder="big"
)
encoded = converted_tuple.encode(value_array)
return bytes(length_to_encode) + encoded

def decode(self, array_bytes):
"""
Decodes a bytestring to a dynamic list.

Args:
array_bytes (bytes | bytearray): bytestring to be decoded

Returns:
list: values from the encoded bytestring
"""
if not (
isinstance(array_bytes, bytearray)
or isinstance(array_bytes, bytes)
):
raise error.ABIEncodingError(
"value to be decoded must be in bytes: {}".format(array_bytes)
)
if len(array_bytes) < ABI_LENGTH_SIZE:
raise error.ABIEncodingError(
"dynamic array is too short to be decoded: {}".format(
len(array_bytes)
)
)

byte_length = int.from_bytes(
array_bytes[:ABI_LENGTH_SIZE], byteorder="big"
)
converted_tuple = self._to_tuple_type(byte_length)
return converted_tuple.decode(array_bytes[ABI_LENGTH_SIZE:])
108 changes: 108 additions & 0 deletions algosdk/abi/array_static_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import math

from .base_type import Type
from .bool_type import BoolType
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error


class ArrayStaticType(Type):
"""
Represents a ArrayStatic ABI Type for encoding.

Args:
child_type (Type): the type of the child_types array.
static_length (int): length of the static array.

Attributes:
child_type (Type)
static_length (int)
"""

def __init__(self, arg_type, array_len) -> None:
if array_len < 1:
raise error.ABITypeError(
"static array length must be a positive integer: {}".format(
len(array_len)
)
)
super().__init__()
self.child_type = arg_type
self.static_length = array_len

def __eq__(self, other) -> bool:
if not isinstance(other, ArrayStaticType):
return False
return (
self.child_type == other.child_type
and self.static_length == other.static_length
)

def __str__(self):
return "{}[{}]".format(self.child_type, self.static_length)

def byte_len(self):
if isinstance(self.child_type, BoolType):
# 8 Boolean values can be encoded into 1 byte
return math.ceil(self.static_length / 8)
element_byte_length = self.child_type.byte_len()
return self.static_length * element_byte_length

def is_dynamic(self):
return self.child_type.is_dynamic()

def _to_tuple_type(self):
child_type_array = [self.child_type] * self.static_length
return TupleType(child_type_array)

def encode(self, value_array):
"""
Encodes a list of values into a ArrayStatic ABI bytestring.

Args:
value_array (list | bytes | bytearray): list of values to be encoded.
The number of elements must match the predefined length of array.
If the child types are ByteType, then bytes or bytearray can be
passed in to be encoded as well.

Returns:
bytes: encoded bytes of the static array
"""
if len(value_array) != self.static_length:
raise error.ABIEncodingError(
"value array length does not match static array length: {}".format(
len(value_array)
)
)
if (
isinstance(value_array, bytes)
or isinstance(value_array, bytearray)
) and not isinstance(self.child_type, ByteType):
raise error.ABIEncodingError(
"cannot pass in bytes when the type of the array is not ByteType: {}".format(
value_array
)
)
converted_tuple = self._to_tuple_type()
return converted_tuple.encode(value_array)

def decode(self, array_bytes):
"""
Decodes a bytestring to a static list.

Args:
array_bytes (bytes | bytearray): bytestring to be decoded

Returns:
list: values from the encoded bytestring
"""
if not (
isinstance(array_bytes, bytearray)
or isinstance(array_bytes, bytes)
):
raise error.ABIEncodingError(
"value to be decoded must be in bytes: {}".format(array_bytes)
)
converted_tuple = self._to_tuple_type()
return converted_tuple.decode(array_bytes)
52 changes: 52 additions & 0 deletions algosdk/abi/base_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from abc import ABC, abstractmethod
from enum import IntEnum

# Globals
ABI_LENGTH_SIZE = 2 # We use 2 bytes to encode the length of a dynamic element


class Type(ABC):
"""
Represents an ABI Type for encoding.
"""

def __init__(
self,
) -> None:
pass

@abstractmethod
def __str__(self):
pass

@abstractmethod
def __eq__(self, other) -> bool:
pass

@abstractmethod
def is_dynamic(self):
"""
Return whether the ABI type is dynamic.
"""
pass

@abstractmethod
def byte_len(self):
"""
Return the length in bytes of the ABI type.
"""
pass

@abstractmethod
def encode(self, value):
"""
Serialize the ABI value into a byte string using ABI encoding rules.
"""
pass

@abstractmethod
def decode(self, value_string):
"""
Deserialize the ABI type and value from a byte string using ABI encoding rules.
"""
pass
Loading