Skip to content

Commit e444328

Browse files
ABI Type encoding support (#238)
* Add initial support for ABI types * Finish up abi types and values * Add doc strings * Refactor and finish encoding functions * More cleanup * Fix minor bugs and add number decoding * Bump version to 1.8.0 and add changelog * Add change to changelog * Add decoding for arrays and dynamic types * Add more unit tests * Minor change from PR comment * Another small PR comment change * Split up files and remove circular imports * Address PR comments * Have arrays accept bytes and add docstrings * Minor clean up * Fix error messages for negative uint * Remove unnecessary imports * Refactor out abi_types since it is implicit by the class * Tuples don't need static lengths... * Address PR comments 1 * Fix head encoding placeholders * Fix the tuple docstring * Formatting fixes Co-authored-by: Brice Rising <brice@algorand.com>
1 parent 14b4121 commit e444328

15 files changed

+1751
-0
lines changed

algosdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import abi
12
from . import account
23
from . import algod
34
from . import auction

algosdk/abi/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from .util import type_from_string
2+
from .uint_type import UintType
3+
from .ufixed_type import UfixedType
4+
from .bool_type import BoolType
5+
from .byte_type import ByteType
6+
from .address_type import AddressType
7+
from .string_type import StringType
8+
from .array_dynamic_type import ArrayDynamicType
9+
from .array_static_type import ArrayStaticType
10+
from .tuple_type import TupleType
11+
12+
name = "abi"

algosdk/abi/address_type.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from .base_type import Type
2+
from .byte_type import ByteType
3+
from .tuple_type import TupleType
4+
from .. import error
5+
6+
from algosdk import encoding
7+
8+
9+
class AddressType(Type):
10+
"""
11+
Represents an Address ABI Type for encoding.
12+
"""
13+
14+
def __init__(self) -> None:
15+
super().__init__()
16+
17+
def __eq__(self, other) -> bool:
18+
if not isinstance(other, AddressType):
19+
return False
20+
return True
21+
22+
def __str__(self):
23+
return "address"
24+
25+
def byte_len(self):
26+
return 32
27+
28+
def is_dynamic(self):
29+
return False
30+
31+
def _to_tuple_type(self):
32+
child_type_array = list()
33+
for _ in range(self.byte_len()):
34+
child_type_array.append(ByteType())
35+
return TupleType(child_type_array)
36+
37+
def encode(self, value):
38+
"""
39+
Encode an address string or a 32-byte public key into a Address ABI bytestring.
40+
41+
Args:
42+
value (str | bytes): value to be encoded. It can be either a base32
43+
address string or a 32-byte public key.
44+
45+
Returns:
46+
bytes: encoded bytes of the address
47+
"""
48+
# Check that the value is an address in string or the public key in bytes
49+
if isinstance(value, str):
50+
try:
51+
value = encoding.decode_address(value)
52+
except Exception as e:
53+
raise error.ABIEncodingError(
54+
"cannot encode the following address: {}".format(value)
55+
) from e
56+
elif (
57+
not (isinstance(value, bytes) or isinstance(value, bytearray))
58+
or len(value) != 32
59+
):
60+
raise error.ABIEncodingError(
61+
"cannot encode the following public key: {}".format(value)
62+
)
63+
return bytes(value)
64+
65+
def decode(self, bytestring):
66+
"""
67+
Decodes a bytestring to a base32 encoded address string.
68+
69+
Args:
70+
bytestring (bytes | bytearray): bytestring to be decoded
71+
72+
Returns:
73+
str: base32 encoded address from the encoded bytestring
74+
"""
75+
if (
76+
not (
77+
isinstance(bytestring, bytearray)
78+
or isinstance(bytestring, bytes)
79+
)
80+
or len(bytestring) != 32
81+
):
82+
raise error.ABIEncodingError(
83+
"address string must be in bytes and correspond to a byte[32]: {}".format(
84+
bytestring
85+
)
86+
)
87+
# Return the base32 encoded address string
88+
return encoding.encode_address(bytestring)

algosdk/abi/array_dynamic_type.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from .base_type import ABI_LENGTH_SIZE, Type
2+
from .byte_type import ByteType
3+
from .tuple_type import TupleType
4+
from .. import error
5+
6+
7+
class ArrayDynamicType(Type):
8+
"""
9+
Represents a ArrayDynamic ABI Type for encoding.
10+
11+
Args:
12+
child_type (Type): the type of the dynamic array.
13+
14+
Attributes:
15+
child_type (Type)
16+
"""
17+
18+
def __init__(self, arg_type) -> None:
19+
super().__init__()
20+
self.child_type = arg_type
21+
22+
def __eq__(self, other) -> bool:
23+
if not isinstance(other, ArrayDynamicType):
24+
return False
25+
return self.child_type == other.child_type
26+
27+
def __str__(self):
28+
return "{}[]".format(self.child_type)
29+
30+
def byte_len(self):
31+
raise error.ABITypeError(
32+
"cannot get length of a dynamic type: {}".format(self)
33+
)
34+
35+
def is_dynamic(self):
36+
return True
37+
38+
def _to_tuple_type(self, length):
39+
child_type_array = [self.child_type] * length
40+
return TupleType(child_type_array)
41+
42+
def encode(self, value_array):
43+
"""
44+
Encodes a list of values into a ArrayDynamic ABI bytestring.
45+
46+
Args:
47+
value_array (list | bytes | bytearray): list of values to be encoded.
48+
If the child types are ByteType, then bytes or bytearray can be
49+
passed in to be encoded as well.
50+
51+
Returns:
52+
bytes: encoded bytes of the dynamic array
53+
"""
54+
if (
55+
isinstance(value_array, bytes)
56+
or isinstance(value_array, bytearray)
57+
) and not isinstance(self.child_type, ByteType):
58+
raise error.ABIEncodingError(
59+
"cannot pass in bytes when the type of the array is not ByteType: {}".format(
60+
value_array
61+
)
62+
)
63+
converted_tuple = self._to_tuple_type(len(value_array))
64+
length_to_encode = len(converted_tuple.child_types).to_bytes(
65+
2, byteorder="big"
66+
)
67+
encoded = converted_tuple.encode(value_array)
68+
return bytes(length_to_encode) + encoded
69+
70+
def decode(self, array_bytes):
71+
"""
72+
Decodes a bytestring to a dynamic list.
73+
74+
Args:
75+
array_bytes (bytes | bytearray): bytestring to be decoded
76+
77+
Returns:
78+
list: values from the encoded bytestring
79+
"""
80+
if not (
81+
isinstance(array_bytes, bytearray)
82+
or isinstance(array_bytes, bytes)
83+
):
84+
raise error.ABIEncodingError(
85+
"value to be decoded must be in bytes: {}".format(array_bytes)
86+
)
87+
if len(array_bytes) < ABI_LENGTH_SIZE:
88+
raise error.ABIEncodingError(
89+
"dynamic array is too short to be decoded: {}".format(
90+
len(array_bytes)
91+
)
92+
)
93+
94+
byte_length = int.from_bytes(
95+
array_bytes[:ABI_LENGTH_SIZE], byteorder="big"
96+
)
97+
converted_tuple = self._to_tuple_type(byte_length)
98+
return converted_tuple.decode(array_bytes[ABI_LENGTH_SIZE:])

algosdk/abi/array_static_type.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import math
2+
3+
from .base_type import Type
4+
from .bool_type import BoolType
5+
from .byte_type import ByteType
6+
from .tuple_type import TupleType
7+
from .. import error
8+
9+
10+
class ArrayStaticType(Type):
11+
"""
12+
Represents a ArrayStatic ABI Type for encoding.
13+
14+
Args:
15+
child_type (Type): the type of the child_types array.
16+
static_length (int): length of the static array.
17+
18+
Attributes:
19+
child_type (Type)
20+
static_length (int)
21+
"""
22+
23+
def __init__(self, arg_type, array_len) -> None:
24+
if array_len < 1:
25+
raise error.ABITypeError(
26+
"static array length must be a positive integer: {}".format(
27+
len(array_len)
28+
)
29+
)
30+
super().__init__()
31+
self.child_type = arg_type
32+
self.static_length = array_len
33+
34+
def __eq__(self, other) -> bool:
35+
if not isinstance(other, ArrayStaticType):
36+
return False
37+
return (
38+
self.child_type == other.child_type
39+
and self.static_length == other.static_length
40+
)
41+
42+
def __str__(self):
43+
return "{}[{}]".format(self.child_type, self.static_length)
44+
45+
def byte_len(self):
46+
if isinstance(self.child_type, BoolType):
47+
# 8 Boolean values can be encoded into 1 byte
48+
return math.ceil(self.static_length / 8)
49+
element_byte_length = self.child_type.byte_len()
50+
return self.static_length * element_byte_length
51+
52+
def is_dynamic(self):
53+
return self.child_type.is_dynamic()
54+
55+
def _to_tuple_type(self):
56+
child_type_array = [self.child_type] * self.static_length
57+
return TupleType(child_type_array)
58+
59+
def encode(self, value_array):
60+
"""
61+
Encodes a list of values into a ArrayStatic ABI bytestring.
62+
63+
Args:
64+
value_array (list | bytes | bytearray): list of values to be encoded.
65+
The number of elements must match the predefined length of array.
66+
If the child types are ByteType, then bytes or bytearray can be
67+
passed in to be encoded as well.
68+
69+
Returns:
70+
bytes: encoded bytes of the static array
71+
"""
72+
if len(value_array) != self.static_length:
73+
raise error.ABIEncodingError(
74+
"value array length does not match static array length: {}".format(
75+
len(value_array)
76+
)
77+
)
78+
if (
79+
isinstance(value_array, bytes)
80+
or isinstance(value_array, bytearray)
81+
) and not isinstance(self.child_type, ByteType):
82+
raise error.ABIEncodingError(
83+
"cannot pass in bytes when the type of the array is not ByteType: {}".format(
84+
value_array
85+
)
86+
)
87+
converted_tuple = self._to_tuple_type()
88+
return converted_tuple.encode(value_array)
89+
90+
def decode(self, array_bytes):
91+
"""
92+
Decodes a bytestring to a static list.
93+
94+
Args:
95+
array_bytes (bytes | bytearray): bytestring to be decoded
96+
97+
Returns:
98+
list: values from the encoded bytestring
99+
"""
100+
if not (
101+
isinstance(array_bytes, bytearray)
102+
or isinstance(array_bytes, bytes)
103+
):
104+
raise error.ABIEncodingError(
105+
"value to be decoded must be in bytes: {}".format(array_bytes)
106+
)
107+
converted_tuple = self._to_tuple_type()
108+
return converted_tuple.decode(array_bytes)

algosdk/abi/base_type.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from abc import ABC, abstractmethod
2+
from enum import IntEnum
3+
4+
# Globals
5+
ABI_LENGTH_SIZE = 2 # We use 2 bytes to encode the length of a dynamic element
6+
7+
8+
class Type(ABC):
9+
"""
10+
Represents an ABI Type for encoding.
11+
"""
12+
13+
def __init__(
14+
self,
15+
) -> None:
16+
pass
17+
18+
@abstractmethod
19+
def __str__(self):
20+
pass
21+
22+
@abstractmethod
23+
def __eq__(self, other) -> bool:
24+
pass
25+
26+
@abstractmethod
27+
def is_dynamic(self):
28+
"""
29+
Return whether the ABI type is dynamic.
30+
"""
31+
pass
32+
33+
@abstractmethod
34+
def byte_len(self):
35+
"""
36+
Return the length in bytes of the ABI type.
37+
"""
38+
pass
39+
40+
@abstractmethod
41+
def encode(self, value):
42+
"""
43+
Serialize the ABI value into a byte string using ABI encoding rules.
44+
"""
45+
pass
46+
47+
@abstractmethod
48+
def decode(self, value_string):
49+
"""
50+
Deserialize the ABI type and value from a byte string using ABI encoding rules.
51+
"""
52+
pass

0 commit comments

Comments
 (0)