algorand-python-testing 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algopy/__init__.py +42 -0
- algopy/arc4.py +1 -0
- algopy/gtxn.py +1 -0
- algopy/itxn.py +1 -0
- algopy/op.py +1 -0
- algopy/py.typed +0 -0
- algopy_testing/__init__.py +47 -0
- algopy_testing/arc4.py +1222 -0
- algopy_testing/constants.py +17 -0
- algopy_testing/context.py +769 -0
- algopy_testing/decorators/__init__.py +0 -0
- algopy_testing/decorators/abimethod.py +146 -0
- algopy_testing/decorators/subroutine.py +9 -0
- algopy_testing/enums.py +39 -0
- algopy_testing/gtxn.py +239 -0
- algopy_testing/itxn.py +353 -0
- algopy_testing/models/__init__.py +23 -0
- algopy_testing/models/account.py +128 -0
- algopy_testing/models/application.py +72 -0
- algopy_testing/models/asset.py +109 -0
- algopy_testing/models/contract.py +69 -0
- algopy_testing/models/global_values.py +67 -0
- algopy_testing/models/gtxn.py +40 -0
- algopy_testing/models/itxn.py +34 -0
- algopy_testing/models/transactions.py +158 -0
- algopy_testing/models/txn.py +111 -0
- algopy_testing/models/unsigned_builtins.py +15 -0
- algopy_testing/op.py +639 -0
- algopy_testing/primitives/__init__.py +6 -0
- algopy_testing/primitives/biguint.py +147 -0
- algopy_testing/primitives/bytes.py +173 -0
- algopy_testing/primitives/string.py +67 -0
- algopy_testing/primitives/uint64.py +210 -0
- algopy_testing/py.typed +0 -0
- algopy_testing/state/__init__.py +4 -0
- algopy_testing/state/global_state.py +73 -0
- algopy_testing/state/local_state.py +54 -0
- algopy_testing/utils.py +156 -0
- algorand_python_testing-0.1.0.dist-info/METADATA +29 -0
- algorand_python_testing-0.1.0.dist-info/RECORD +41 -0
- algorand_python_testing-0.1.0.dist-info/WHEEL +4 -0
algopy_testing/arc4.py
ADDED
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import decimal
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
from collections.abc import Iterable, Reversible
|
|
8
|
+
|
|
9
|
+
import algosdk
|
|
10
|
+
|
|
11
|
+
from algopy_testing.constants import ARC4_RETURN_PREFIX, BITS_IN_BYTE, UINT64_SIZE, UINT512_SIZE
|
|
12
|
+
from algopy_testing.decorators.abimethod import abimethod
|
|
13
|
+
from algopy_testing.models import Account
|
|
14
|
+
from algopy_testing.utils import (
|
|
15
|
+
as_bytes,
|
|
16
|
+
as_int,
|
|
17
|
+
as_int16,
|
|
18
|
+
as_int64,
|
|
19
|
+
as_int512,
|
|
20
|
+
as_string,
|
|
21
|
+
int_to_bytes,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if typing.TYPE_CHECKING:
|
|
25
|
+
import algopy
|
|
26
|
+
|
|
27
|
+
_ABI_LENGTH_SIZE = 2
|
|
28
|
+
_TBitSize = typing.TypeVar("_TBitSize", bound=int)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _ABIEncoded(typing.Protocol):
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
34
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def bytes(self) -> algopy.Bytes:
|
|
39
|
+
"""Get the underlying Bytes"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
44
|
+
"""Load an ABI type from application logs,
|
|
45
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class String(_ABIEncoded):
|
|
50
|
+
"""An ARC4 sequence of bytes containing a UTF8 string"""
|
|
51
|
+
|
|
52
|
+
_value: bytes
|
|
53
|
+
|
|
54
|
+
def __init__(self, value: algopy.String | str = "", /) -> None:
|
|
55
|
+
import algopy
|
|
56
|
+
|
|
57
|
+
match value:
|
|
58
|
+
case algopy.String():
|
|
59
|
+
bytes_value = as_bytes(value.bytes)
|
|
60
|
+
case str(value):
|
|
61
|
+
bytes_value = value.encode("utf-8")
|
|
62
|
+
case _:
|
|
63
|
+
raise TypeError(
|
|
64
|
+
f"value must be a string or String type, not {type(value).__name__!r}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self._value = len(bytes_value).to_bytes(_ABI_LENGTH_SIZE) + bytes_value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def native(self) -> algopy.String:
|
|
71
|
+
"""Return the String representation of the UTF8 string after ARC4 decoding"""
|
|
72
|
+
import algopy
|
|
73
|
+
|
|
74
|
+
return algopy.String.from_bytes(self._value[_ABI_LENGTH_SIZE:])
|
|
75
|
+
|
|
76
|
+
def __add__(self, other: String | str) -> String:
|
|
77
|
+
return String(self.native + as_string(other))
|
|
78
|
+
|
|
79
|
+
def __radd__(self, other: String | str) -> String:
|
|
80
|
+
return String(as_string(other) + self.native)
|
|
81
|
+
|
|
82
|
+
def __eq__(self, other: String | str) -> bool: # type: ignore[override]
|
|
83
|
+
return self.native == as_string(other)
|
|
84
|
+
|
|
85
|
+
def __bool__(self) -> bool:
|
|
86
|
+
"""Returns `True` if length is not zero"""
|
|
87
|
+
return bool(self.native)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
91
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
92
|
+
result = cls()
|
|
93
|
+
result._value = as_bytes(value)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def bytes(self) -> algopy.Bytes:
|
|
98
|
+
"""Get the underlying Bytes"""
|
|
99
|
+
import algopy
|
|
100
|
+
|
|
101
|
+
return algopy.Bytes(self._value)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
105
|
+
"""Load an ABI type from application logs,
|
|
106
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
107
|
+
import algopy
|
|
108
|
+
|
|
109
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
110
|
+
return cls.from_bytes(log[4:])
|
|
111
|
+
raise ValueError("ABI return prefix not found")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# https://stackoverflow.com/a/75395800
|
|
115
|
+
class _UIntNMeta(type(_ABIEncoded), typing.Generic[_TBitSize]): # type: ignore[misc]
|
|
116
|
+
__concrete__: typing.ClassVar[dict[type, type]] = {}
|
|
117
|
+
|
|
118
|
+
def __getitem__(cls, key_t: type[_TBitSize]) -> type:
|
|
119
|
+
cache = cls.__concrete__
|
|
120
|
+
if c := cache.get(key_t, None):
|
|
121
|
+
return c
|
|
122
|
+
cache[key_t] = c = types.new_class(
|
|
123
|
+
f"{cls.__name__}[{key_t.__name__}]", (cls,), {}, lambda ns: ns.update(_t=key_t)
|
|
124
|
+
)
|
|
125
|
+
return c
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _UIntN(_ABIEncoded, typing.Generic[_TBitSize], metaclass=_UIntNMeta):
|
|
129
|
+
_t: type[_TBitSize]
|
|
130
|
+
_bit_size: int
|
|
131
|
+
_max_bits_len: int
|
|
132
|
+
_max_bytes_len: int
|
|
133
|
+
_max_int: int
|
|
134
|
+
_value: bytes # underlying 'bytes' value representing the UIntN
|
|
135
|
+
|
|
136
|
+
def __init__(self, value: algopy.BigUInt | algopy.UInt64 | int = 0, /) -> None:
|
|
137
|
+
self._bit_size = as_int(typing.get_args(self._t)[0], max=self._max_bits_len)
|
|
138
|
+
self._max_int = 2**self._bit_size - 1
|
|
139
|
+
self._max_bytes_len = self._bit_size // BITS_IN_BYTE
|
|
140
|
+
|
|
141
|
+
value = as_int(value, max=self._max_int)
|
|
142
|
+
bytes_value = int_to_bytes(value, self._max_bytes_len)
|
|
143
|
+
self._value = as_bytes(bytes_value, max_size=self._max_bytes_len)
|
|
144
|
+
|
|
145
|
+
# ~~~ https://docs.python.org/3/reference/datamodel.html#basic-customization ~~~
|
|
146
|
+
# TODO: mypy suggests due to Liskov below should be other: object
|
|
147
|
+
# need to consider ramifications here, ignoring it for now
|
|
148
|
+
def __eq__( # type: ignore[override]
|
|
149
|
+
self,
|
|
150
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
151
|
+
) -> bool:
|
|
152
|
+
raise NotImplementedError
|
|
153
|
+
|
|
154
|
+
def __ne__( # type: ignore[override]
|
|
155
|
+
self,
|
|
156
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
157
|
+
) -> bool:
|
|
158
|
+
raise NotImplementedError
|
|
159
|
+
|
|
160
|
+
def __le__(
|
|
161
|
+
self,
|
|
162
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
163
|
+
) -> bool:
|
|
164
|
+
raise NotImplementedError
|
|
165
|
+
|
|
166
|
+
def __lt__(
|
|
167
|
+
self,
|
|
168
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
169
|
+
) -> bool:
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
|
|
172
|
+
def __ge__(
|
|
173
|
+
self,
|
|
174
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
175
|
+
) -> bool:
|
|
176
|
+
raise NotImplementedError
|
|
177
|
+
|
|
178
|
+
def __gt__(
|
|
179
|
+
self,
|
|
180
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
181
|
+
) -> bool:
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
def __bool__(self) -> bool:
|
|
185
|
+
"""Returns `True` if not equal to zero"""
|
|
186
|
+
raise NotImplementedError
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
190
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
191
|
+
|
|
192
|
+
value = as_bytes(value)
|
|
193
|
+
result = cls()
|
|
194
|
+
result._value = value
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def bytes(self) -> algopy.Bytes:
|
|
199
|
+
"""Get the underlying Bytes"""
|
|
200
|
+
import algopy
|
|
201
|
+
|
|
202
|
+
return algopy.Bytes(self._value)
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
206
|
+
"""Load an ABI type from application logs,
|
|
207
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
208
|
+
import algopy
|
|
209
|
+
|
|
210
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
211
|
+
return cls.from_bytes(log[4:])
|
|
212
|
+
raise ValueError("ABI return prefix not found")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class UIntN(_UIntN[_TBitSize], typing.Generic[_TBitSize]):
|
|
216
|
+
"""An ARC4 UInt consisting of the number of bits specified.
|
|
217
|
+
|
|
218
|
+
Max Size: 64 bits"""
|
|
219
|
+
|
|
220
|
+
_max_bits_len = UINT64_SIZE
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def native(self) -> algopy.UInt64:
|
|
224
|
+
"""Return the UInt64 representation of the value after ARC4 decoding"""
|
|
225
|
+
import algopy
|
|
226
|
+
|
|
227
|
+
return algopy.UInt64(int.from_bytes(self._value))
|
|
228
|
+
|
|
229
|
+
def __eq__(self, other: object) -> bool:
|
|
230
|
+
return as_int64(self.native) == as_int(other, max=None)
|
|
231
|
+
|
|
232
|
+
def __ne__(self, other: object) -> bool:
|
|
233
|
+
return as_int64(self.native) != as_int(other, max=None)
|
|
234
|
+
|
|
235
|
+
def __le__(self, other: object) -> bool:
|
|
236
|
+
return as_int64(self.native) <= as_int(other, max=None)
|
|
237
|
+
|
|
238
|
+
def __lt__(self, other: object) -> bool:
|
|
239
|
+
return as_int64(self.native) < as_int(other, max=None)
|
|
240
|
+
|
|
241
|
+
def __ge__(self, other: object) -> bool:
|
|
242
|
+
return as_int64(self.native) >= as_int(other, max=None)
|
|
243
|
+
|
|
244
|
+
def __gt__(self, other: object) -> bool:
|
|
245
|
+
return as_int64(self.native) > as_int(other, max=None)
|
|
246
|
+
|
|
247
|
+
def __bool__(self) -> bool:
|
|
248
|
+
return bool(self.native)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class BigUIntN(_UIntN[_TBitSize], typing.Generic[_TBitSize]):
|
|
252
|
+
"""An ARC4 UInt consisting of the number of bits specified.
|
|
253
|
+
|
|
254
|
+
Max size: 512 bits"""
|
|
255
|
+
|
|
256
|
+
_max_bits_len = UINT512_SIZE
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def native(self) -> algopy.BigUInt:
|
|
260
|
+
"""Return the UInt64 representation of the value after ARC4 decoding"""
|
|
261
|
+
import algopy
|
|
262
|
+
|
|
263
|
+
return algopy.BigUInt.from_bytes(self._value)
|
|
264
|
+
|
|
265
|
+
def __eq__(self, other: object) -> bool:
|
|
266
|
+
return as_int512(self.native) == as_int(other, max=None)
|
|
267
|
+
|
|
268
|
+
def __ne__(self, other: object) -> bool:
|
|
269
|
+
return as_int512(self.native) != as_int(other, max=None)
|
|
270
|
+
|
|
271
|
+
def __le__(self, other: object) -> bool:
|
|
272
|
+
return as_int512(self.native) <= as_int(other, max=None)
|
|
273
|
+
|
|
274
|
+
def __lt__(self, other: object) -> bool:
|
|
275
|
+
return as_int512(self.native) < as_int(other, max=None)
|
|
276
|
+
|
|
277
|
+
def __ge__(self, other: object) -> bool:
|
|
278
|
+
return as_int512(self.native) >= as_int(other, max=None)
|
|
279
|
+
|
|
280
|
+
def __gt__(self, other: object) -> bool:
|
|
281
|
+
return as_int512(self.native) > as_int(other, max=None)
|
|
282
|
+
|
|
283
|
+
def __bool__(self) -> bool:
|
|
284
|
+
return bool(self.native)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
_TDecimalPlaces = typing.TypeVar("_TDecimalPlaces", bound=int)
|
|
288
|
+
_MAX_M_SIZE = 160
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class _UFixedNxMMeta(type(_ABIEncoded), typing.Generic[_TBitSize, _TDecimalPlaces]): # type: ignore[misc]
|
|
292
|
+
__concrete__: typing.ClassVar[dict[tuple[type, type], type]] = {}
|
|
293
|
+
|
|
294
|
+
def __getitem__(cls, key_t: tuple[type[_TBitSize], type[_TDecimalPlaces]]) -> type:
|
|
295
|
+
cache = cls.__concrete__
|
|
296
|
+
if c := cache.get(key_t, None):
|
|
297
|
+
return c
|
|
298
|
+
|
|
299
|
+
size_t, decimal_t = key_t
|
|
300
|
+
cache[key_t] = c = types.new_class(
|
|
301
|
+
f"{cls.__name__}[{size_t.__name__}, {decimal_t.__name__}]",
|
|
302
|
+
(cls,),
|
|
303
|
+
{},
|
|
304
|
+
lambda ns: ns.update(_size_t=size_t, _decimal_t=decimal_t),
|
|
305
|
+
)
|
|
306
|
+
return c
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class _UFixedNxM(
|
|
310
|
+
_ABIEncoded, typing.Generic[_TBitSize, _TDecimalPlaces], metaclass=_UFixedNxMMeta
|
|
311
|
+
):
|
|
312
|
+
_size_t: type[_TBitSize]
|
|
313
|
+
_decimal_t: type[_TDecimalPlaces]
|
|
314
|
+
_n: int
|
|
315
|
+
_m: int
|
|
316
|
+
_max_bits_len: int
|
|
317
|
+
_max_bytes_len: int
|
|
318
|
+
_value: bytes # underlying 'bytes' value representing the UFixedNxM
|
|
319
|
+
|
|
320
|
+
def __init__(self, value: str = "0.0", /):
|
|
321
|
+
"""
|
|
322
|
+
Construct an instance of UFixedNxM where value (v) is determined from the original
|
|
323
|
+
decimal value (d) by the formula v = round(d * (10^M))
|
|
324
|
+
"""
|
|
325
|
+
self._n = as_int(typing.get_args(self._size_t)[0], max=self._max_bits_len)
|
|
326
|
+
self._m = as_int(typing.get_args(self._decimal_t)[0], max=_MAX_M_SIZE)
|
|
327
|
+
self._max_int = 2**self._n - 1
|
|
328
|
+
self._max_bytes_len = self._n // BITS_IN_BYTE
|
|
329
|
+
|
|
330
|
+
value = as_string(value)
|
|
331
|
+
with decimal.localcontext(
|
|
332
|
+
decimal.Context(
|
|
333
|
+
prec=160,
|
|
334
|
+
traps=[
|
|
335
|
+
decimal.Rounded,
|
|
336
|
+
decimal.InvalidOperation,
|
|
337
|
+
decimal.Overflow,
|
|
338
|
+
decimal.DivisionByZero,
|
|
339
|
+
],
|
|
340
|
+
)
|
|
341
|
+
):
|
|
342
|
+
try:
|
|
343
|
+
d = decimal.Decimal(value)
|
|
344
|
+
except ArithmeticError as ex:
|
|
345
|
+
raise ValueError(f"Invalid decimal literal: {value}") from ex
|
|
346
|
+
if d < 0:
|
|
347
|
+
raise ValueError("Negative numbers not allowed")
|
|
348
|
+
try:
|
|
349
|
+
q = d.quantize(decimal.Decimal(f"1e-{self._m}"))
|
|
350
|
+
except ArithmeticError as ex:
|
|
351
|
+
raise ValueError(f"Too many decimals, expected max of {self._m}") from ex
|
|
352
|
+
|
|
353
|
+
int_value = round(q * (10**self._m))
|
|
354
|
+
int_value = as_int(int_value, max=self._max_int)
|
|
355
|
+
bytes_value = int_to_bytes(int_value, self._max_bytes_len)
|
|
356
|
+
self._value = as_bytes(bytes_value, max_size=self._max_bytes_len)
|
|
357
|
+
|
|
358
|
+
def __bool__(self) -> bool:
|
|
359
|
+
"""Returns `True` if not equal to zero"""
|
|
360
|
+
return bool(int.from_bytes(self._value))
|
|
361
|
+
|
|
362
|
+
@classmethod
|
|
363
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
364
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
365
|
+
value = as_bytes(value)
|
|
366
|
+
result = cls()
|
|
367
|
+
result._value = value
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def bytes(self) -> algopy.Bytes:
|
|
372
|
+
"""Get the underlying Bytes"""
|
|
373
|
+
import algopy
|
|
374
|
+
|
|
375
|
+
return algopy.Bytes(self._value)
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
379
|
+
"""Load an ABI type from application logs,
|
|
380
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
381
|
+
import algopy
|
|
382
|
+
|
|
383
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
384
|
+
return cls.from_bytes(log[4:])
|
|
385
|
+
raise ValueError("ABI return prefix not found")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class UFixedNxM(
|
|
389
|
+
_UFixedNxM[_TBitSize, _TDecimalPlaces], typing.Generic[_TBitSize, _TDecimalPlaces]
|
|
390
|
+
):
|
|
391
|
+
"""An ARC4 UFixed representing a decimal with the number of bits and precision specified.
|
|
392
|
+
|
|
393
|
+
Max size: 64 bits"""
|
|
394
|
+
|
|
395
|
+
_max_bits_len: int = UINT64_SIZE
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class BigUFixedNxM(
|
|
399
|
+
_UFixedNxM[_TBitSize, _TDecimalPlaces], typing.Generic[_TBitSize, _TDecimalPlaces]
|
|
400
|
+
):
|
|
401
|
+
"""An ARC4 UFixed representing a decimal with the number of bits and precision specified.
|
|
402
|
+
|
|
403
|
+
Max size: 512 bits"""
|
|
404
|
+
|
|
405
|
+
_max_bits_len: int = UINT512_SIZE
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class Byte(UIntN[typing.Literal[8]]):
|
|
409
|
+
"""An ARC4 alias for a UInt8"""
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
UInt8: typing.TypeAlias = UIntN[typing.Literal[8]]
|
|
413
|
+
"""An ARC4 UInt8"""
|
|
414
|
+
|
|
415
|
+
UInt16: typing.TypeAlias = UIntN[typing.Literal[16]]
|
|
416
|
+
"""An ARC4 UInt16"""
|
|
417
|
+
|
|
418
|
+
UInt32: typing.TypeAlias = UIntN[typing.Literal[32]]
|
|
419
|
+
"""An ARC4 UInt32"""
|
|
420
|
+
|
|
421
|
+
UInt64: typing.TypeAlias = UIntN[typing.Literal[64]]
|
|
422
|
+
"""An ARC4 UInt64"""
|
|
423
|
+
|
|
424
|
+
UInt128: typing.TypeAlias = BigUIntN[typing.Literal[128]]
|
|
425
|
+
"""An ARC4 UInt128"""
|
|
426
|
+
|
|
427
|
+
UInt256: typing.TypeAlias = BigUIntN[typing.Literal[256]]
|
|
428
|
+
"""An ARC4 UInt256"""
|
|
429
|
+
|
|
430
|
+
UInt512: typing.TypeAlias = BigUIntN[typing.Literal[512]]
|
|
431
|
+
"""An ARC4 UInt512"""
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class Bool(_ABIEncoded):
|
|
435
|
+
"""An ARC4 encoded bool"""
|
|
436
|
+
|
|
437
|
+
_value: bytes
|
|
438
|
+
|
|
439
|
+
# True value is encoded as having a 1 on the most significant bit (0x80 = 128)
|
|
440
|
+
_true_int_value = 128
|
|
441
|
+
_false_int_value = 0
|
|
442
|
+
|
|
443
|
+
def __init__(self, value: bool = False, /) -> None: # noqa: FBT001, FBT002
|
|
444
|
+
self._value = int_to_bytes(self._true_int_value if value else self._false_int_value, 1)
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def native(self) -> bool:
|
|
448
|
+
"""Return the bool representation of the value after ARC4 decoding"""
|
|
449
|
+
int_value = int.from_bytes(self._value)
|
|
450
|
+
return int_value == self._true_int_value
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
454
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
455
|
+
result = cls()
|
|
456
|
+
result._value = as_bytes(value)
|
|
457
|
+
return result
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def bytes(self) -> algopy.Bytes:
|
|
461
|
+
"""Get the underlying Bytes"""
|
|
462
|
+
import algopy
|
|
463
|
+
|
|
464
|
+
return algopy.Bytes(self._value)
|
|
465
|
+
|
|
466
|
+
@classmethod
|
|
467
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
468
|
+
"""Load an ABI type from application logs,
|
|
469
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
470
|
+
import algopy
|
|
471
|
+
|
|
472
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
473
|
+
return cls.from_bytes(log[4:])
|
|
474
|
+
raise ValueError("ABI return prefix not found")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
_TArrayItem = typing.TypeVar("_TArrayItem")
|
|
478
|
+
_TArrayLength = typing.TypeVar("_TArrayLength", bound=int)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class _StaticArrayMeta(type(_ABIEncoded), typing.Generic[_TArrayItem, _TArrayLength]): # type: ignore # noqa: PGH003
|
|
482
|
+
__concrete__: typing.ClassVar[dict[tuple[type, type], type]] = {}
|
|
483
|
+
|
|
484
|
+
def __getitem__(cls, key_t: tuple[type[_TArrayItem], type[_TArrayLength]]) -> type:
|
|
485
|
+
cache = cls.__concrete__
|
|
486
|
+
if c := cache.get(key_t, None):
|
|
487
|
+
return c
|
|
488
|
+
|
|
489
|
+
cache[key_t] = c = types.new_class(
|
|
490
|
+
f"{cls.__name__}[{','.join([k.__name__ for k in key_t])}]",
|
|
491
|
+
(cls,),
|
|
492
|
+
{},
|
|
493
|
+
lambda ns: ns.update(
|
|
494
|
+
_child_types=[_TypeInfo(key_t[0])]
|
|
495
|
+
* (typing.get_args(key_t[1])[0] if len(typing.get_args(key_t[1])) > 0 else 0),
|
|
496
|
+
_array_item_t=key_t[0],
|
|
497
|
+
),
|
|
498
|
+
)
|
|
499
|
+
return c
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class _TypeInfo:
|
|
503
|
+
value: type
|
|
504
|
+
child_types: list[_TypeInfo] | None
|
|
505
|
+
|
|
506
|
+
def __init__(self, value: type, child_types: list[_TypeInfo] | None = None):
|
|
507
|
+
self.value = value
|
|
508
|
+
self.child_types = child_types or (
|
|
509
|
+
value._child_types if hasattr(value, "_child_types") else child_types
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class StaticArray(
|
|
514
|
+
_ABIEncoded,
|
|
515
|
+
typing.Generic[_TArrayItem, _TArrayLength],
|
|
516
|
+
Reversible[_TArrayItem],
|
|
517
|
+
metaclass=_StaticArrayMeta,
|
|
518
|
+
):
|
|
519
|
+
"""A fixed length ARC4 Array of the specified type and length"""
|
|
520
|
+
|
|
521
|
+
_array_item_t: type[_TArrayItem]
|
|
522
|
+
_child_types: list[_TypeInfo]
|
|
523
|
+
|
|
524
|
+
_value: bytes
|
|
525
|
+
|
|
526
|
+
def __init__(self, *items: _TArrayItem):
|
|
527
|
+
self._value = _encode(items)
|
|
528
|
+
if items:
|
|
529
|
+
self._array_item_t = type(items[0])
|
|
530
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
531
|
+
|
|
532
|
+
# ensure these two variables are set as instance variables instead of class variables
|
|
533
|
+
# to avoid sharing state between instances created by copy operation
|
|
534
|
+
if hasattr(self, "_array_item_t"):
|
|
535
|
+
self._array_item_t = self._array_item_t
|
|
536
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
537
|
+
|
|
538
|
+
def _get_type_info(self, item: _TArrayItem) -> _TypeInfo:
|
|
539
|
+
return _TypeInfo(
|
|
540
|
+
self._array_item_t,
|
|
541
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def __iter__(self) -> typing.Iterator[_TArrayItem]:
|
|
545
|
+
# """Returns an iterator for the items in the array"""
|
|
546
|
+
return iter(self._list())
|
|
547
|
+
|
|
548
|
+
def __reversed__(self) -> typing.Iterator[_TArrayItem]:
|
|
549
|
+
# """Returns an iterator for the items in the array, in reverse order"""
|
|
550
|
+
return reversed(self._list())
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def length(self) -> algopy.UInt64:
|
|
554
|
+
# """Returns the current length of the array"""
|
|
555
|
+
import algopy
|
|
556
|
+
|
|
557
|
+
return algopy.UInt64(len(self._list()))
|
|
558
|
+
|
|
559
|
+
@typing.overload
|
|
560
|
+
def __getitem__(self, value: algopy.UInt64 | int, /) -> _TArrayItem: ...
|
|
561
|
+
@typing.overload
|
|
562
|
+
def __getitem__(self, value: slice, /) -> list[_TArrayItem]: ...
|
|
563
|
+
def __getitem__(self, index: algopy.UInt64 | int | slice) -> _TArrayItem | list[_TArrayItem]:
|
|
564
|
+
if isinstance(index, slice):
|
|
565
|
+
return self._list()[index]
|
|
566
|
+
return self._list()[index]
|
|
567
|
+
|
|
568
|
+
def append(self, item: _TArrayItem, /) -> None:
|
|
569
|
+
"""Append items to this array"""
|
|
570
|
+
if not issubclass(type(item), self._array_item_t):
|
|
571
|
+
expected_type = self._array_item_t.__name__
|
|
572
|
+
actual_type = type(item).__name__
|
|
573
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
574
|
+
x = self._list()
|
|
575
|
+
x.append(item)
|
|
576
|
+
self._child_types.append(self._get_type_info(item))
|
|
577
|
+
self._value = _encode(x)
|
|
578
|
+
|
|
579
|
+
def extend(self, other: Iterable[_TArrayItem], /) -> None:
|
|
580
|
+
"""Extend this array with the contents of another array"""
|
|
581
|
+
if any(not issubclass(type(x), self._array_item_t) for x in other):
|
|
582
|
+
raise TypeError(f"items must be of type {self._array_item_t.__name__!r}")
|
|
583
|
+
x = self._list()
|
|
584
|
+
x.extend(other)
|
|
585
|
+
self._child_types.extend([self._get_type_info(x) for x in other])
|
|
586
|
+
self._value = _encode(x)
|
|
587
|
+
|
|
588
|
+
def __setitem__(self, index: algopy.UInt64 | int, value: _TArrayItem) -> _TArrayItem:
|
|
589
|
+
if not issubclass(type(value), self._array_item_t):
|
|
590
|
+
expected_type = self._array_item_t.__name__
|
|
591
|
+
actual_type = type(value).__name__
|
|
592
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
593
|
+
x = self._list()
|
|
594
|
+
x[index] = value
|
|
595
|
+
self._value = _encode(x)
|
|
596
|
+
return value
|
|
597
|
+
|
|
598
|
+
def copy(self) -> typing.Self:
|
|
599
|
+
# """Create a copy of this array"""
|
|
600
|
+
return copy.deepcopy(self)
|
|
601
|
+
|
|
602
|
+
def _list(self) -> list[_TArrayItem]:
|
|
603
|
+
return _decode(self._value, self._child_types)
|
|
604
|
+
|
|
605
|
+
@classmethod
|
|
606
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
607
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
608
|
+
value = as_bytes(value)
|
|
609
|
+
result = cls()
|
|
610
|
+
result._value = value
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def bytes(self) -> algopy.Bytes:
|
|
615
|
+
"""Get the underlying Bytes"""
|
|
616
|
+
import algopy
|
|
617
|
+
|
|
618
|
+
return algopy.Bytes(self._value)
|
|
619
|
+
|
|
620
|
+
@classmethod
|
|
621
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
622
|
+
"""Load an ABI type from application logs,
|
|
623
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
624
|
+
import algopy
|
|
625
|
+
|
|
626
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
627
|
+
return cls.from_bytes(log[4:])
|
|
628
|
+
raise ValueError("ABI return prefix not found")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class Address(StaticArray[Byte, typing.Literal[32]]):
|
|
632
|
+
"""An alias for an array containing 32 bytes representing an Algorand address"""
|
|
633
|
+
|
|
634
|
+
def __init__(self, value: Account | str | algopy.Bytes = algosdk.constants.ZERO_ADDRESS):
|
|
635
|
+
"""
|
|
636
|
+
If `value` is a string, it should be a 58 character base32 string,
|
|
637
|
+
ie a base32 string-encoded 32 bytes public key + 4 bytes checksum.
|
|
638
|
+
If `value` is a Bytes, it's length checked to be 32 bytes - to avoid this
|
|
639
|
+
check, use `Address.from_bytes(...)` instead.
|
|
640
|
+
Defaults to the zero-address.
|
|
641
|
+
"""
|
|
642
|
+
if isinstance(value, str):
|
|
643
|
+
try:
|
|
644
|
+
bytes_value = algosdk.encoding.decode_address(value)
|
|
645
|
+
except Exception as e:
|
|
646
|
+
raise ValueError(f"cannot encode the following address: {value!r}") from e
|
|
647
|
+
else:
|
|
648
|
+
bytes_value = (
|
|
649
|
+
value.bytes.value if isinstance(value, Account) else as_bytes(value, max_size=32)
|
|
650
|
+
)
|
|
651
|
+
if len(bytes_value) != 32:
|
|
652
|
+
raise ValueError(f"expected 32 bytes, got: {len(bytes_value)}")
|
|
653
|
+
self._value = bytes_value
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def native(self) -> Account:
|
|
657
|
+
# """Return the Account representation of the address after ARC4 decoding"""
|
|
658
|
+
return Account(self.bytes)
|
|
659
|
+
|
|
660
|
+
def __bool__(self) -> bool:
|
|
661
|
+
# """Returns `True` if not equal to the zero address"""
|
|
662
|
+
zero_bytes = algosdk.encoding.decode_address(algosdk.constants.ZERO_ADDRESS)
|
|
663
|
+
return self.bytes != zero_bytes if isinstance(zero_bytes, bytes) else False
|
|
664
|
+
|
|
665
|
+
def __eq__(self, other: Address | Account | str) -> bool: # type: ignore[override]
|
|
666
|
+
"""Address equality is determined by the address of another
|
|
667
|
+
`arc4.Address`, `Account` or `str`"""
|
|
668
|
+
if isinstance(other, Address | Account):
|
|
669
|
+
return self.bytes == other.bytes
|
|
670
|
+
other_bytes = algosdk.encoding.decode_address(other)
|
|
671
|
+
return self.bytes == other_bytes if isinstance(other_bytes, bytes) else False
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class _DynamicArrayMeta(type(_ABIEncoded), typing.Generic[_TArrayItem, _TArrayLength]): # type: ignore # noqa: PGH003
|
|
675
|
+
__concrete__: typing.ClassVar[dict[type, type]] = {}
|
|
676
|
+
|
|
677
|
+
def __getitem__(cls, key_t: type[_TArrayItem]) -> type:
|
|
678
|
+
cache = cls.__concrete__
|
|
679
|
+
if c := cache.get(key_t, None):
|
|
680
|
+
return c
|
|
681
|
+
|
|
682
|
+
cache[key_t] = c = types.new_class(
|
|
683
|
+
f"{cls.__name__}[{key_t.__name__}]",
|
|
684
|
+
(cls,),
|
|
685
|
+
{},
|
|
686
|
+
lambda ns: ns.update(
|
|
687
|
+
_child_types=[],
|
|
688
|
+
_array_item_t=key_t,
|
|
689
|
+
),
|
|
690
|
+
)
|
|
691
|
+
return c
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class DynamicArray(
|
|
695
|
+
_ABIEncoded,
|
|
696
|
+
typing.Generic[_TArrayItem],
|
|
697
|
+
Reversible[_TArrayItem],
|
|
698
|
+
metaclass=_DynamicArrayMeta,
|
|
699
|
+
):
|
|
700
|
+
"""A dynamically sized ARC4 Array of the specified type"""
|
|
701
|
+
|
|
702
|
+
_array_item_t: type[_TArrayItem]
|
|
703
|
+
_child_types: list[_TypeInfo]
|
|
704
|
+
|
|
705
|
+
_value: bytes
|
|
706
|
+
|
|
707
|
+
def __init__(self, *items: _TArrayItem):
|
|
708
|
+
self._value = self._encode_with_length(items)
|
|
709
|
+
if items:
|
|
710
|
+
self._array_item_t = type(items[0])
|
|
711
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
712
|
+
|
|
713
|
+
# ensure these two variables are set as instance variables instead of class variables
|
|
714
|
+
# to avoid sharing state between instances created by copy operation
|
|
715
|
+
if hasattr(self, "_array_item_t"):
|
|
716
|
+
self._array_item_t = self._array_item_t
|
|
717
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
718
|
+
|
|
719
|
+
def _get_type_info(self, item: _TArrayItem) -> _TypeInfo:
|
|
720
|
+
return _TypeInfo(
|
|
721
|
+
self._array_item_t,
|
|
722
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def __iter__(self) -> typing.Iterator[_TArrayItem]:
|
|
726
|
+
"""Returns an iterator for the items in the array"""
|
|
727
|
+
return iter(self._list())
|
|
728
|
+
|
|
729
|
+
def __reversed__(self) -> typing.Iterator[_TArrayItem]:
|
|
730
|
+
"""Returns an iterator for the items in the array, in reverse order"""
|
|
731
|
+
return reversed(self._list())
|
|
732
|
+
|
|
733
|
+
@property
|
|
734
|
+
def length(self) -> algopy.UInt64:
|
|
735
|
+
"""Returns the current length of the array"""
|
|
736
|
+
import algopy
|
|
737
|
+
|
|
738
|
+
return algopy.UInt64(len(self._list()))
|
|
739
|
+
|
|
740
|
+
@typing.overload
|
|
741
|
+
def __getitem__(self, value: algopy.UInt64 | int, /) -> _TArrayItem: ...
|
|
742
|
+
@typing.overload
|
|
743
|
+
def __getitem__(self, value: slice, /) -> list[_TArrayItem]: ...
|
|
744
|
+
def __getitem__(self, index: algopy.UInt64 | int | slice) -> _TArrayItem | list[_TArrayItem]:
|
|
745
|
+
if isinstance(index, slice):
|
|
746
|
+
return self._list()[index]
|
|
747
|
+
return self._list()[index]
|
|
748
|
+
|
|
749
|
+
def append(self, item: _TArrayItem, /) -> None:
|
|
750
|
+
"""Append items to this array"""
|
|
751
|
+
if not issubclass(type(item), self._array_item_t):
|
|
752
|
+
expected_type = self._array_item_t.__name__
|
|
753
|
+
actual_type = type(item).__name__
|
|
754
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
755
|
+
x = self._list()
|
|
756
|
+
x.append(item)
|
|
757
|
+
self._child_types.append(self._get_type_info(item))
|
|
758
|
+
self._value = self._encode_with_length(x)
|
|
759
|
+
|
|
760
|
+
def extend(self, other: Iterable[_TArrayItem], /) -> None:
|
|
761
|
+
"""Extend this array with the contents of another array"""
|
|
762
|
+
if any(not issubclass(type(x), self._array_item_t) for x in other):
|
|
763
|
+
raise TypeError(f"items must be of type {self._array_item_t.__name__!r}")
|
|
764
|
+
x = self._list()
|
|
765
|
+
x.extend(other)
|
|
766
|
+
self._child_types.extend([self._get_type_info(x) for x in other])
|
|
767
|
+
self._value = self._encode_with_length(x)
|
|
768
|
+
|
|
769
|
+
def __setitem__(self, index: algopy.UInt64 | int, value: _TArrayItem) -> _TArrayItem:
|
|
770
|
+
if not issubclass(type(value), self._array_item_t):
|
|
771
|
+
expected_type = self._array_item_t.__name__
|
|
772
|
+
actual_type = type(value).__name__
|
|
773
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
774
|
+
x = self._list()
|
|
775
|
+
x[index] = value
|
|
776
|
+
self._value = self._encode_with_length(x)
|
|
777
|
+
return value
|
|
778
|
+
|
|
779
|
+
def __add__(self, other: Iterable[_TArrayItem]) -> DynamicArray[_TArrayItem]:
|
|
780
|
+
self.extend(other)
|
|
781
|
+
return self
|
|
782
|
+
|
|
783
|
+
def pop(self) -> _TArrayItem:
|
|
784
|
+
"""Remove and return the last item in the array"""
|
|
785
|
+
x = self._list()
|
|
786
|
+
item = x.pop()
|
|
787
|
+
self._child_types.pop()
|
|
788
|
+
self._value = self._encode_with_length(x)
|
|
789
|
+
return item
|
|
790
|
+
|
|
791
|
+
def copy(self) -> typing.Self:
|
|
792
|
+
"""Create a copy of this array"""
|
|
793
|
+
return copy.deepcopy(self)
|
|
794
|
+
|
|
795
|
+
def __bool__(self) -> bool:
|
|
796
|
+
"""Returns `True` if not an empty array"""
|
|
797
|
+
return bool(self._list())
|
|
798
|
+
|
|
799
|
+
def _list(self) -> list[_TArrayItem]:
|
|
800
|
+
length = int.from_bytes(self._value[:_ABI_LENGTH_SIZE])
|
|
801
|
+
self._child_types = self._child_types or []
|
|
802
|
+
if hasattr(self, "_array_item_t"):
|
|
803
|
+
self._child_types += [_TypeInfo(self._array_item_t)] * (
|
|
804
|
+
length - len(self._child_types)
|
|
805
|
+
)
|
|
806
|
+
return _decode(self._value[_ABI_LENGTH_SIZE:], self._child_types)
|
|
807
|
+
|
|
808
|
+
def _encode_with_length(self, items: list[_TArrayItem] | tuple[_TArrayItem, ...]) -> bytes:
|
|
809
|
+
return len(items).to_bytes(_ABI_LENGTH_SIZE) + _encode(items)
|
|
810
|
+
|
|
811
|
+
@classmethod
|
|
812
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
813
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
814
|
+
value = as_bytes(value)
|
|
815
|
+
result = cls()
|
|
816
|
+
result._value = value
|
|
817
|
+
return result
|
|
818
|
+
|
|
819
|
+
@property
|
|
820
|
+
def bytes(self) -> algopy.Bytes:
|
|
821
|
+
"""Get the underlying Bytes"""
|
|
822
|
+
import algopy
|
|
823
|
+
|
|
824
|
+
return algopy.Bytes(self._value)
|
|
825
|
+
|
|
826
|
+
@classmethod
|
|
827
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
828
|
+
"""Load an ABI type from application logs,
|
|
829
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
830
|
+
import algopy
|
|
831
|
+
|
|
832
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
833
|
+
return cls.from_bytes(log[4:])
|
|
834
|
+
raise ValueError("ABI return prefix not found")
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class DynamicBytes(DynamicArray[Byte]):
|
|
838
|
+
"""A variable sized array of bytes"""
|
|
839
|
+
|
|
840
|
+
@typing.overload
|
|
841
|
+
def __init__(self, *values: Byte | UInt8 | int): ...
|
|
842
|
+
|
|
843
|
+
@typing.overload
|
|
844
|
+
def __init__(self, value: algopy.Bytes | bytes, /): ...
|
|
845
|
+
|
|
846
|
+
def __init__(
|
|
847
|
+
self,
|
|
848
|
+
*value: algopy.Bytes | bytes | Byte | UInt8 | int,
|
|
849
|
+
):
|
|
850
|
+
import algopy
|
|
851
|
+
|
|
852
|
+
items = []
|
|
853
|
+
for x in value:
|
|
854
|
+
if isinstance(x, int):
|
|
855
|
+
items.append(Byte(x))
|
|
856
|
+
elif isinstance(x, Byte):
|
|
857
|
+
items.append(x)
|
|
858
|
+
elif isinstance(x, UInt8): # type: ignore[misc]
|
|
859
|
+
items.append(typing.cast(Byte, x))
|
|
860
|
+
elif isinstance(x, algopy.Bytes):
|
|
861
|
+
items.extend([Byte(int.from_bytes(i.value)) for i in x])
|
|
862
|
+
elif isinstance(x, bytes):
|
|
863
|
+
items.extend([Byte(int.from_bytes(i.value)) for i in algopy.Bytes(x)])
|
|
864
|
+
|
|
865
|
+
super().__init__(*items)
|
|
866
|
+
|
|
867
|
+
@property
|
|
868
|
+
def native(self) -> algopy.Bytes:
|
|
869
|
+
"""Return the Bytes representation of the address after ARC4 decoding"""
|
|
870
|
+
return self.bytes
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
_TTuple = typing.TypeVarTuple("_TTuple")
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
class _TupleMeta(type(_ABIEncoded), typing.Generic[typing.Unpack[_TTuple]]): # type: ignore # noqa: PGH003
|
|
877
|
+
__concrete__: typing.ClassVar[dict[tuple, type]] = {} # type: ignore[type-arg]
|
|
878
|
+
|
|
879
|
+
def __getitem__(cls, key_t: tuple) -> type: # type: ignore[type-arg]
|
|
880
|
+
cache = cls.__concrete__
|
|
881
|
+
if c := cache.get(key_t, None):
|
|
882
|
+
return c
|
|
883
|
+
|
|
884
|
+
cache[key_t] = c = types.new_class(
|
|
885
|
+
f"{cls.__name__}[{key_t}]",
|
|
886
|
+
(cls,),
|
|
887
|
+
{},
|
|
888
|
+
lambda ns: ns.update(
|
|
889
|
+
_child_types=[_TypeInfo(typing.cast(type, item)) for item in key_t],
|
|
890
|
+
),
|
|
891
|
+
)
|
|
892
|
+
return c
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class Tuple(
|
|
896
|
+
_ABIEncoded,
|
|
897
|
+
tuple[*_TTuple],
|
|
898
|
+
typing.Generic[typing.Unpack[_TTuple]],
|
|
899
|
+
metaclass=_TupleMeta,
|
|
900
|
+
):
|
|
901
|
+
"""An ARC4 ABI tuple, containing other ARC4 ABI types"""
|
|
902
|
+
|
|
903
|
+
__slots__ = ()
|
|
904
|
+
|
|
905
|
+
_child_types: list[_TypeInfo]
|
|
906
|
+
_value: bytes
|
|
907
|
+
|
|
908
|
+
def __init__(self, items: tuple[typing.Unpack[_TTuple]] = (), /): # type: ignore[assignment]
|
|
909
|
+
"""Construct an ARC4 tuple from a python tuple"""
|
|
910
|
+
self._value = _encode(items)
|
|
911
|
+
if items:
|
|
912
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
913
|
+
|
|
914
|
+
# ensure the variable is set as instance variables instead of class variables
|
|
915
|
+
# to avoid sharing state between instances created by copy operation
|
|
916
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
917
|
+
|
|
918
|
+
def _get_type_info(self, item: typing.Any) -> _TypeInfo:
|
|
919
|
+
return _TypeInfo(
|
|
920
|
+
type(item),
|
|
921
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def __len__(self) -> int:
|
|
925
|
+
return len(self.native)
|
|
926
|
+
|
|
927
|
+
@typing.overload
|
|
928
|
+
def __getitem__(self, value: typing.SupportsIndex, /) -> object: ...
|
|
929
|
+
@typing.overload
|
|
930
|
+
def __getitem__(self, value: slice, /) -> tuple[object, ...]: ...
|
|
931
|
+
|
|
932
|
+
def __getitem__(self, index: typing.SupportsIndex | slice) -> tuple[object, ...] | object:
|
|
933
|
+
return self.native[index]
|
|
934
|
+
|
|
935
|
+
def __iter__(self) -> typing.Iterator[object]:
|
|
936
|
+
return iter(self.native)
|
|
937
|
+
|
|
938
|
+
@property
|
|
939
|
+
def native(self) -> tuple[typing.Unpack[_TTuple]]:
|
|
940
|
+
"""Return the Bytes representation of the address after ARC4 decoding"""
|
|
941
|
+
return typing.cast(
|
|
942
|
+
tuple[typing.Unpack[_TTuple]], tuple(_decode(self._value, self._child_types))
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
@classmethod
|
|
946
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
947
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
948
|
+
value = as_bytes(value)
|
|
949
|
+
result = cls()
|
|
950
|
+
result._value = value
|
|
951
|
+
return result
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def bytes(self) -> algopy.Bytes:
|
|
955
|
+
"""Get the underlying Bytes"""
|
|
956
|
+
import algopy
|
|
957
|
+
|
|
958
|
+
return algopy.Bytes(self._value)
|
|
959
|
+
|
|
960
|
+
@classmethod
|
|
961
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
962
|
+
"""Load an ABI type from application logs,
|
|
963
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
964
|
+
import algopy
|
|
965
|
+
|
|
966
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
967
|
+
return cls.from_bytes(log[4:])
|
|
968
|
+
raise ValueError("ABI return prefix not found")
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _is_arc4_dynamic(value: object) -> bool:
|
|
972
|
+
if isinstance(value, DynamicArray):
|
|
973
|
+
return True
|
|
974
|
+
elif isinstance(value, StaticArray | Tuple):
|
|
975
|
+
return any(_is_arc4_dynamic(v) for v in value)
|
|
976
|
+
return not isinstance(value, BigUFixedNxM | BigUIntN | UFixedNxM | UIntN | Bool)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _is_arc4_dynamic_type(value: _TypeInfo) -> bool:
|
|
980
|
+
if issubclass(value.value, DynamicArray):
|
|
981
|
+
return True
|
|
982
|
+
elif value.child_types:
|
|
983
|
+
return any(_is_arc4_dynamic_type(v) for v in value.child_types)
|
|
984
|
+
return not issubclass(value.value, BigUFixedNxM | BigUIntN | UFixedNxM | UIntN | Bool)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _find_bool(
|
|
988
|
+
values: (
|
|
989
|
+
StaticArray[typing.Any, typing.Any]
|
|
990
|
+
| DynamicArray[typing.Any]
|
|
991
|
+
| Tuple[typing.Any]
|
|
992
|
+
| tuple[typing.Any, ...]
|
|
993
|
+
| list[typing.Any]
|
|
994
|
+
),
|
|
995
|
+
index: int,
|
|
996
|
+
delta: int,
|
|
997
|
+
) -> int:
|
|
998
|
+
"""
|
|
999
|
+
Helper function to find consecutive booleans from current index in a tuple.
|
|
1000
|
+
"""
|
|
1001
|
+
until = 0
|
|
1002
|
+
values_length = len(values) if isinstance(values, tuple | list) else values.length.value
|
|
1003
|
+
while True:
|
|
1004
|
+
curr = index + delta * until
|
|
1005
|
+
if isinstance(values[curr], Bool):
|
|
1006
|
+
if curr != values_length - 1 and delta > 0 or curr > 0 and delta < 0:
|
|
1007
|
+
until += 1
|
|
1008
|
+
else:
|
|
1009
|
+
break
|
|
1010
|
+
else:
|
|
1011
|
+
until -= 1
|
|
1012
|
+
break
|
|
1013
|
+
return until
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _find_bool_types(values: typing.Sequence[_TypeInfo], index: int, delta: int) -> int:
|
|
1017
|
+
"""
|
|
1018
|
+
Helper function to find consecutive booleans from current index in a tuple.
|
|
1019
|
+
"""
|
|
1020
|
+
until = 0
|
|
1021
|
+
values_length = len(values)
|
|
1022
|
+
while True:
|
|
1023
|
+
curr = index + delta * until
|
|
1024
|
+
if issubclass(values[curr].value, Bool):
|
|
1025
|
+
if curr != values_length - 1 and delta > 0 or curr > 0 and delta < 0:
|
|
1026
|
+
until += 1
|
|
1027
|
+
else:
|
|
1028
|
+
break
|
|
1029
|
+
else:
|
|
1030
|
+
until -= 1
|
|
1031
|
+
break
|
|
1032
|
+
return until
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _compress_multiple_bool(value_list: list[Bool]) -> int:
|
|
1036
|
+
"""
|
|
1037
|
+
Compress consecutive boolean values into a byte for a Tuple/Array.
|
|
1038
|
+
"""
|
|
1039
|
+
result = 0
|
|
1040
|
+
if len(value_list) > 8:
|
|
1041
|
+
raise ValueError("length of list should not be greater than 8")
|
|
1042
|
+
for i, value in enumerate(value_list):
|
|
1043
|
+
assert isinstance(value, Bool)
|
|
1044
|
+
bool_val = value.native
|
|
1045
|
+
if bool_val:
|
|
1046
|
+
result |= 1 << (7 - i)
|
|
1047
|
+
return result
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _get_max_bytes_len(type_info: _TypeInfo) -> int:
|
|
1051
|
+
size = 0
|
|
1052
|
+
if issubclass(type_info.value, DynamicArray):
|
|
1053
|
+
size += _ABI_LENGTH_SIZE
|
|
1054
|
+
elif type_info.child_types:
|
|
1055
|
+
i = 0
|
|
1056
|
+
child_types = type_info.child_types or []
|
|
1057
|
+
while i < len(child_types):
|
|
1058
|
+
if issubclass(child_types[i].value, Bool):
|
|
1059
|
+
after = _find_bool_types(child_types, i, 1)
|
|
1060
|
+
i += after
|
|
1061
|
+
bool_num = after + 1
|
|
1062
|
+
size += bool_num // 8
|
|
1063
|
+
if bool_num % 8 != 0:
|
|
1064
|
+
size += 1
|
|
1065
|
+
else:
|
|
1066
|
+
child_byte_size = _get_max_bytes_len(child_types[i])
|
|
1067
|
+
size += child_byte_size
|
|
1068
|
+
i += 1
|
|
1069
|
+
|
|
1070
|
+
else:
|
|
1071
|
+
value = type_info.value()
|
|
1072
|
+
if hasattr(value, "_max_bytes_len"):
|
|
1073
|
+
size = typing.cast(int, value._max_bytes_len)
|
|
1074
|
+
|
|
1075
|
+
return size
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _encode(
|
|
1079
|
+
values: (
|
|
1080
|
+
StaticArray[typing.Any, typing.Any]
|
|
1081
|
+
| DynamicArray[typing.Any]
|
|
1082
|
+
| Tuple[typing.Any]
|
|
1083
|
+
| tuple[typing.Any, ...]
|
|
1084
|
+
| list[typing.Any]
|
|
1085
|
+
),
|
|
1086
|
+
) -> bytes:
|
|
1087
|
+
heads = []
|
|
1088
|
+
tails = []
|
|
1089
|
+
is_dynamic_index = []
|
|
1090
|
+
i = 0
|
|
1091
|
+
values_length = len(values) if isinstance(values, tuple | list) else values.length.value
|
|
1092
|
+
values_length_bytes = (
|
|
1093
|
+
int_to_bytes(values_length, _ABI_LENGTH_SIZE) if isinstance(values, DynamicArray) else b""
|
|
1094
|
+
)
|
|
1095
|
+
while i < values_length:
|
|
1096
|
+
value = values[i]
|
|
1097
|
+
is_dynamic_index.append(_is_arc4_dynamic(value))
|
|
1098
|
+
if is_dynamic_index[-1]:
|
|
1099
|
+
heads.append(b"\x00\x00")
|
|
1100
|
+
tail_encoding = value.bytes.value if isinstance(value, String) else _encode(value)
|
|
1101
|
+
tails.append(tail_encoding)
|
|
1102
|
+
else:
|
|
1103
|
+
if isinstance(value, Bool):
|
|
1104
|
+
before = _find_bool(values, i, -1)
|
|
1105
|
+
after = _find_bool(values, i, 1)
|
|
1106
|
+
|
|
1107
|
+
# Pack bytes to heads and tails
|
|
1108
|
+
if before % 8 != 0:
|
|
1109
|
+
raise ValueError(
|
|
1110
|
+
"expected before index should have number of bool mod 8 equal 0"
|
|
1111
|
+
)
|
|
1112
|
+
after = min(7, after)
|
|
1113
|
+
consecutive_bool_list = typing.cast(list[Bool], values[i : i + after + 1])
|
|
1114
|
+
compressed_int = _compress_multiple_bool(consecutive_bool_list)
|
|
1115
|
+
heads.append(bytes([compressed_int]))
|
|
1116
|
+
i += after
|
|
1117
|
+
else:
|
|
1118
|
+
heads.append(value.bytes.value)
|
|
1119
|
+
tails.append(b"")
|
|
1120
|
+
i += 1
|
|
1121
|
+
|
|
1122
|
+
# Adjust heads for dynamic types
|
|
1123
|
+
head_length = 0
|
|
1124
|
+
for head_element in heads:
|
|
1125
|
+
# If the element is not a placeholder, append the length of the element
|
|
1126
|
+
head_length += len(head_element)
|
|
1127
|
+
|
|
1128
|
+
# Correctly encode dynamic types and replace placeholder
|
|
1129
|
+
tail_curr_length = 0
|
|
1130
|
+
for i in range(len(heads)):
|
|
1131
|
+
if is_dynamic_index[i]:
|
|
1132
|
+
head_value = as_int16(head_length + tail_curr_length)
|
|
1133
|
+
heads[i] = int_to_bytes(head_value, _ABI_LENGTH_SIZE)
|
|
1134
|
+
|
|
1135
|
+
tail_curr_length += len(tails[i])
|
|
1136
|
+
|
|
1137
|
+
# Concatenate bytes
|
|
1138
|
+
return values_length_bytes + b"".join(heads) + b"".join(tails)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _decode( # noqa: PLR0912, C901
|
|
1142
|
+
value: bytes, child_types: typing.Sequence[_TypeInfo]
|
|
1143
|
+
) -> list[typing.Any]:
|
|
1144
|
+
dynamic_segments: list[list[int]] = [] # Store the start and end of a dynamic element
|
|
1145
|
+
value_partitions: list[bytes | None] = []
|
|
1146
|
+
i = 0
|
|
1147
|
+
array_index = 0
|
|
1148
|
+
|
|
1149
|
+
while i < len(child_types):
|
|
1150
|
+
child_type = child_types[i]
|
|
1151
|
+
if _is_arc4_dynamic_type(child_type):
|
|
1152
|
+
# Decode the size of the dynamic element
|
|
1153
|
+
dynamic_index = int.from_bytes(value[array_index : array_index + _ABI_LENGTH_SIZE])
|
|
1154
|
+
if len(dynamic_segments) > 0:
|
|
1155
|
+
dynamic_segments[-1][1] = dynamic_index
|
|
1156
|
+
|
|
1157
|
+
# Since we do not know where the current dynamic element ends,
|
|
1158
|
+
# put a placeholder and update later
|
|
1159
|
+
dynamic_segments.append([dynamic_index, -1])
|
|
1160
|
+
value_partitions.append(None)
|
|
1161
|
+
array_index += _ABI_LENGTH_SIZE
|
|
1162
|
+
elif issubclass(child_type.value, Bool):
|
|
1163
|
+
before = _find_bool_types(child_types, i, -1)
|
|
1164
|
+
after = _find_bool_types(child_types, i, 1)
|
|
1165
|
+
|
|
1166
|
+
if before % 8 != 0:
|
|
1167
|
+
raise ValueError("expected before index should have number of bool mod 8 equal 0")
|
|
1168
|
+
after = min(7, after)
|
|
1169
|
+
bits = int.from_bytes(value[array_index : array_index + 1])
|
|
1170
|
+
# Parse bool values into multiple byte strings
|
|
1171
|
+
for bool_i in range(after + 1):
|
|
1172
|
+
mask = 128 >> bool_i
|
|
1173
|
+
if mask & bits:
|
|
1174
|
+
value_partitions.append(b"\x80")
|
|
1175
|
+
else:
|
|
1176
|
+
value_partitions.append(b"\x00")
|
|
1177
|
+
i += after
|
|
1178
|
+
array_index += 1
|
|
1179
|
+
else:
|
|
1180
|
+
curr_len = _get_max_bytes_len(child_type)
|
|
1181
|
+
value_partitions.append(value[array_index : array_index + curr_len])
|
|
1182
|
+
array_index += curr_len
|
|
1183
|
+
|
|
1184
|
+
if array_index >= len(value) and i != len(child_types) - 1:
|
|
1185
|
+
raise ValueError(f"input string is not long enough to be decoded: {value!r}")
|
|
1186
|
+
|
|
1187
|
+
i += 1
|
|
1188
|
+
|
|
1189
|
+
if len(dynamic_segments) > 0:
|
|
1190
|
+
dynamic_segments[len(dynamic_segments) - 1][1] = len(value)
|
|
1191
|
+
array_index = len(value)
|
|
1192
|
+
if array_index < len(value):
|
|
1193
|
+
raise ValueError(f"input string was not fully consumed: {value!r}")
|
|
1194
|
+
|
|
1195
|
+
# Check dynamic element partitions
|
|
1196
|
+
segment_index = 0
|
|
1197
|
+
for i, child_type in enumerate(child_types):
|
|
1198
|
+
if _is_arc4_dynamic_type(child_type):
|
|
1199
|
+
segment_start, segment_end = dynamic_segments[segment_index]
|
|
1200
|
+
value_partitions[i] = value[segment_start:segment_end]
|
|
1201
|
+
segment_index += 1
|
|
1202
|
+
|
|
1203
|
+
# Decode individual tuple elements
|
|
1204
|
+
values = []
|
|
1205
|
+
for i, child_type in enumerate(child_types):
|
|
1206
|
+
val = child_type.value.from_bytes(value_partitions[i]) # type: ignore[attr-defined]
|
|
1207
|
+
val._child_types = child_type.child_types
|
|
1208
|
+
values.append(val)
|
|
1209
|
+
return values
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
__all__ = [
|
|
1213
|
+
"Bool",
|
|
1214
|
+
"UInt8",
|
|
1215
|
+
"UInt16",
|
|
1216
|
+
"UInt32",
|
|
1217
|
+
"UInt64",
|
|
1218
|
+
"UInt128",
|
|
1219
|
+
"UInt256",
|
|
1220
|
+
"UInt512",
|
|
1221
|
+
"abimethod",
|
|
1222
|
+
]
|