algorand-python-testing 0.0.0b1__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 +58 -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 +55 -0
- algopy_testing/arc4.py +1533 -0
- algopy_testing/constants.py +22 -0
- algopy_testing/context.py +1194 -0
- algopy_testing/decorators/__init__.py +0 -0
- algopy_testing/decorators/abimethod.py +204 -0
- algopy_testing/decorators/baremethod.py +83 -0
- algopy_testing/decorators/subroutine.py +9 -0
- algopy_testing/enums.py +42 -0
- algopy_testing/gtxn.py +261 -0
- algopy_testing/itxn.py +665 -0
- algopy_testing/models/__init__.py +31 -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/block.py +34 -0
- algopy_testing/models/box.py +158 -0
- algopy_testing/models/contract.py +82 -0
- algopy_testing/models/gitxn.py +42 -0
- algopy_testing/models/global_values.py +72 -0
- algopy_testing/models/gtxn.py +56 -0
- algopy_testing/models/itxn.py +85 -0
- algopy_testing/models/logicsig.py +44 -0
- algopy_testing/models/template_variable.py +23 -0
- algopy_testing/models/transactions.py +158 -0
- algopy_testing/models/txn.py +113 -0
- algopy_testing/models/unsigned_builtins.py +36 -0
- algopy_testing/op.py +1098 -0
- algopy_testing/primitives/__init__.py +6 -0
- algopy_testing/primitives/biguint.py +148 -0
- algopy_testing/primitives/bytes.py +174 -0
- algopy_testing/primitives/string.py +68 -0
- algopy_testing/primitives/uint64.py +213 -0
- algopy_testing/protocols.py +18 -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/utilities/__init__.py +3 -0
- algopy_testing/utilities/budget.py +23 -0
- algopy_testing/utilities/log.py +55 -0
- algopy_testing/utils.py +249 -0
- algorand_python_testing-0.0.0b1.dist-info/METADATA +81 -0
- algorand_python_testing-0.0.0b1.dist-info/RECORD +52 -0
- algorand_python_testing-0.0.0b1.dist-info/WHEEL +4 -0
- algorand_python_testing-0.0.0b1.dist-info/licenses/LICENSE +14 -0
algopy_testing/arc4.py
ADDED
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import decimal
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
from collections import ChainMap
|
|
8
|
+
from collections.abc import Iterable, Reversible
|
|
9
|
+
|
|
10
|
+
import algosdk
|
|
11
|
+
from Cryptodome.Hash import SHA512
|
|
12
|
+
|
|
13
|
+
from algopy_testing.constants import (
|
|
14
|
+
ARC4_RETURN_PREFIX,
|
|
15
|
+
BITS_IN_BYTE,
|
|
16
|
+
MAX_UINT64,
|
|
17
|
+
UINT64_SIZE,
|
|
18
|
+
UINT512_SIZE,
|
|
19
|
+
)
|
|
20
|
+
from algopy_testing.decorators.abimethod import abimethod
|
|
21
|
+
from algopy_testing.decorators.baremethod import baremethod
|
|
22
|
+
from algopy_testing.models import Account
|
|
23
|
+
from algopy_testing.protocols import BytesBacked
|
|
24
|
+
from algopy_testing.utils import (
|
|
25
|
+
abi_type_name_for_arg,
|
|
26
|
+
as_bytes,
|
|
27
|
+
as_int,
|
|
28
|
+
as_int16,
|
|
29
|
+
as_int64,
|
|
30
|
+
as_int512,
|
|
31
|
+
as_string,
|
|
32
|
+
int_to_bytes,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if typing.TYPE_CHECKING:
|
|
36
|
+
import algopy
|
|
37
|
+
|
|
38
|
+
_ABI_LENGTH_SIZE = 2
|
|
39
|
+
_TBitSize = typing.TypeVar("_TBitSize", bound=int)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ABIEncoded(BytesBacked, typing.Protocol):
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
45
|
+
"""Load an ABI type from application logs,
|
|
46
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def arc4_signature(signature: str, /) -> algopy.Bytes:
|
|
51
|
+
"""Convert a signature to ARC4 bytes"""
|
|
52
|
+
import algopy
|
|
53
|
+
|
|
54
|
+
hashed_signature = SHA512.new(truncate="256")
|
|
55
|
+
hashed_signature.update(signature.encode("utf-8"))
|
|
56
|
+
return_value = hashed_signature.digest()[:4]
|
|
57
|
+
return algopy.Bytes(return_value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class String(_ABIEncoded):
|
|
61
|
+
"""An ARC4 sequence of bytes containing a UTF8 string"""
|
|
62
|
+
|
|
63
|
+
_value: bytes
|
|
64
|
+
|
|
65
|
+
def __init__(self, value: algopy.String | str = "", /) -> None:
|
|
66
|
+
import algopy
|
|
67
|
+
|
|
68
|
+
match value:
|
|
69
|
+
case algopy.String():
|
|
70
|
+
bytes_value = as_bytes(value.bytes)
|
|
71
|
+
case str(value):
|
|
72
|
+
bytes_value = value.encode("utf-8")
|
|
73
|
+
case _:
|
|
74
|
+
raise TypeError(
|
|
75
|
+
f"value must be a string or String type, not {type(value).__name__!r}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self._value = len(bytes_value).to_bytes(_ABI_LENGTH_SIZE) + bytes_value
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def native(self) -> algopy.String:
|
|
82
|
+
"""Return the String representation of the UTF8 string after ARC4 decoding"""
|
|
83
|
+
import algopy
|
|
84
|
+
|
|
85
|
+
return algopy.String.from_bytes(self._value[_ABI_LENGTH_SIZE:])
|
|
86
|
+
|
|
87
|
+
def __add__(self, other: String | str) -> String:
|
|
88
|
+
return String(self.native + as_string(other))
|
|
89
|
+
|
|
90
|
+
def __radd__(self, other: String | str) -> String:
|
|
91
|
+
return String(as_string(other) + self.native)
|
|
92
|
+
|
|
93
|
+
def __eq__(self, other: String | str) -> bool: # type: ignore[override]
|
|
94
|
+
return self.native == as_string(other)
|
|
95
|
+
|
|
96
|
+
def __bool__(self) -> bool:
|
|
97
|
+
"""Returns `True` if length is not zero"""
|
|
98
|
+
return bool(self.native)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
102
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
103
|
+
result = cls()
|
|
104
|
+
result._value = as_bytes(value)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def bytes(self) -> algopy.Bytes:
|
|
109
|
+
"""Get the underlying Bytes"""
|
|
110
|
+
import algopy
|
|
111
|
+
|
|
112
|
+
return algopy.Bytes(self._value)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
116
|
+
"""Load an ABI type from application logs,
|
|
117
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
118
|
+
import algopy
|
|
119
|
+
|
|
120
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
121
|
+
return cls.from_bytes(log[4:])
|
|
122
|
+
raise ValueError("ABI return prefix not found")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# https://stackoverflow.com/a/75395800
|
|
126
|
+
class _UIntNMeta(type(_ABIEncoded), typing.Generic[_TBitSize]): # type: ignore[misc]
|
|
127
|
+
__concrete__: typing.ClassVar[dict[type, type]] = {}
|
|
128
|
+
|
|
129
|
+
def __getitem__(cls, key_t: type[_TBitSize]) -> type:
|
|
130
|
+
cache = cls.__concrete__
|
|
131
|
+
if c := cache.get(key_t, None):
|
|
132
|
+
return c
|
|
133
|
+
cache[key_t] = c = types.new_class(
|
|
134
|
+
f"{cls.__name__}[{key_t.__name__}]", (cls,), {}, lambda ns: ns.update(_t=key_t)
|
|
135
|
+
)
|
|
136
|
+
return c
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _UIntN(_ABIEncoded, typing.Generic[_TBitSize], metaclass=_UIntNMeta):
|
|
140
|
+
_t: type[_TBitSize]
|
|
141
|
+
_bit_size: int
|
|
142
|
+
_max_bits_len: int
|
|
143
|
+
_max_bytes_len: int
|
|
144
|
+
_max_int: int
|
|
145
|
+
_value: bytes # underlying 'bytes' value representing the UIntN
|
|
146
|
+
|
|
147
|
+
def __init__(self, value: algopy.BigUInt | algopy.UInt64 | int = 0, /) -> None:
|
|
148
|
+
self._bit_size = as_int(typing.get_args(self._t)[0], max=self._max_bits_len)
|
|
149
|
+
self._max_int = 2**self._bit_size - 1
|
|
150
|
+
self._max_bytes_len = self._bit_size // BITS_IN_BYTE
|
|
151
|
+
|
|
152
|
+
value = as_int(value, max=self._max_int)
|
|
153
|
+
bytes_value = int_to_bytes(value, self._max_bytes_len)
|
|
154
|
+
self._value = as_bytes(bytes_value, max_size=self._max_bytes_len)
|
|
155
|
+
|
|
156
|
+
# ~~~ https://docs.python.org/3/reference/datamodel.html#basic-customization ~~~
|
|
157
|
+
# TODO: mypy suggests due to Liskov below should be other: object
|
|
158
|
+
# need to consider ramifications here, ignoring it for now
|
|
159
|
+
def __eq__( # type: ignore[override]
|
|
160
|
+
self,
|
|
161
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
162
|
+
) -> bool:
|
|
163
|
+
raise NotImplementedError
|
|
164
|
+
|
|
165
|
+
def __ne__( # type: ignore[override]
|
|
166
|
+
self,
|
|
167
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
168
|
+
) -> bool:
|
|
169
|
+
raise NotImplementedError
|
|
170
|
+
|
|
171
|
+
def __le__(
|
|
172
|
+
self,
|
|
173
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
174
|
+
) -> bool:
|
|
175
|
+
raise NotImplementedError
|
|
176
|
+
|
|
177
|
+
def __lt__(
|
|
178
|
+
self,
|
|
179
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
180
|
+
) -> bool:
|
|
181
|
+
raise NotImplementedError
|
|
182
|
+
|
|
183
|
+
def __ge__(
|
|
184
|
+
self,
|
|
185
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
186
|
+
) -> bool:
|
|
187
|
+
raise NotImplementedError
|
|
188
|
+
|
|
189
|
+
def __gt__(
|
|
190
|
+
self,
|
|
191
|
+
other: UIntN[_TBitSize] | BigUIntN[_TBitSize] | algopy.UInt64 | algopy.BigUInt | int,
|
|
192
|
+
) -> bool:
|
|
193
|
+
raise NotImplementedError
|
|
194
|
+
|
|
195
|
+
def __bool__(self) -> bool:
|
|
196
|
+
"""Returns `True` if not equal to zero"""
|
|
197
|
+
raise NotImplementedError
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
201
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
202
|
+
|
|
203
|
+
value = as_bytes(value)
|
|
204
|
+
result = cls()
|
|
205
|
+
result._value = value
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def bytes(self) -> algopy.Bytes:
|
|
210
|
+
"""Get the underlying Bytes"""
|
|
211
|
+
import algopy
|
|
212
|
+
|
|
213
|
+
return algopy.Bytes(self._value)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
217
|
+
"""Load an ABI type from application logs,
|
|
218
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
219
|
+
import algopy
|
|
220
|
+
|
|
221
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
222
|
+
return cls.from_bytes(log[4:])
|
|
223
|
+
raise ValueError("ABI return prefix not found")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class UIntN(_UIntN[_TBitSize], typing.Generic[_TBitSize]):
|
|
227
|
+
"""An ARC4 UInt consisting of the number of bits specified.
|
|
228
|
+
|
|
229
|
+
Max Size: 64 bits"""
|
|
230
|
+
|
|
231
|
+
_max_bits_len = UINT64_SIZE
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def native(self) -> algopy.UInt64:
|
|
235
|
+
"""Return the UInt64 representation of the value after ARC4 decoding"""
|
|
236
|
+
import algopy
|
|
237
|
+
|
|
238
|
+
return algopy.UInt64(int.from_bytes(self._value))
|
|
239
|
+
|
|
240
|
+
def __eq__(self, other: object) -> bool:
|
|
241
|
+
return as_int64(self.native) == as_int(other, max=None)
|
|
242
|
+
|
|
243
|
+
def __ne__(self, other: object) -> bool:
|
|
244
|
+
return as_int64(self.native) != as_int(other, max=None)
|
|
245
|
+
|
|
246
|
+
def __le__(self, other: object) -> bool:
|
|
247
|
+
return as_int64(self.native) <= as_int(other, max=None)
|
|
248
|
+
|
|
249
|
+
def __lt__(self, other: object) -> bool:
|
|
250
|
+
return as_int64(self.native) < as_int(other, max=None)
|
|
251
|
+
|
|
252
|
+
def __ge__(self, other: object) -> bool:
|
|
253
|
+
return as_int64(self.native) >= as_int(other, max=None)
|
|
254
|
+
|
|
255
|
+
def __gt__(self, other: object) -> bool:
|
|
256
|
+
return as_int64(self.native) > as_int(other, max=None)
|
|
257
|
+
|
|
258
|
+
def __bool__(self) -> bool:
|
|
259
|
+
return bool(self.native)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class BigUIntN(_UIntN[_TBitSize], typing.Generic[_TBitSize]):
|
|
263
|
+
"""An ARC4 UInt consisting of the number of bits specified.
|
|
264
|
+
|
|
265
|
+
Max size: 512 bits"""
|
|
266
|
+
|
|
267
|
+
_max_bits_len = UINT512_SIZE
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def native(self) -> algopy.BigUInt:
|
|
271
|
+
"""Return the UInt64 representation of the value after ARC4 decoding"""
|
|
272
|
+
import algopy
|
|
273
|
+
|
|
274
|
+
return algopy.BigUInt.from_bytes(self._value)
|
|
275
|
+
|
|
276
|
+
def __eq__(self, other: object) -> bool:
|
|
277
|
+
return as_int512(self.native) == as_int(other, max=None)
|
|
278
|
+
|
|
279
|
+
def __ne__(self, other: object) -> bool:
|
|
280
|
+
return as_int512(self.native) != as_int(other, max=None)
|
|
281
|
+
|
|
282
|
+
def __le__(self, other: object) -> bool:
|
|
283
|
+
return as_int512(self.native) <= as_int(other, max=None)
|
|
284
|
+
|
|
285
|
+
def __lt__(self, other: object) -> bool:
|
|
286
|
+
return as_int512(self.native) < as_int(other, max=None)
|
|
287
|
+
|
|
288
|
+
def __ge__(self, other: object) -> bool:
|
|
289
|
+
return as_int512(self.native) >= as_int(other, max=None)
|
|
290
|
+
|
|
291
|
+
def __gt__(self, other: object) -> bool:
|
|
292
|
+
return as_int512(self.native) > as_int(other, max=None)
|
|
293
|
+
|
|
294
|
+
def __bool__(self) -> bool:
|
|
295
|
+
return bool(self.native)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
_TDecimalPlaces = typing.TypeVar("_TDecimalPlaces", bound=int)
|
|
299
|
+
_MAX_M_SIZE = 160
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class _UFixedNxMMeta(type(_ABIEncoded), typing.Generic[_TBitSize, _TDecimalPlaces]): # type: ignore[misc]
|
|
303
|
+
__concrete__: typing.ClassVar[dict[tuple[type, type], type]] = {}
|
|
304
|
+
|
|
305
|
+
def __getitem__(cls, key_t: tuple[type[_TBitSize], type[_TDecimalPlaces]]) -> type:
|
|
306
|
+
cache = cls.__concrete__
|
|
307
|
+
if c := cache.get(key_t, None):
|
|
308
|
+
return c
|
|
309
|
+
|
|
310
|
+
size_t, decimal_t = key_t
|
|
311
|
+
cache[key_t] = c = types.new_class(
|
|
312
|
+
f"{cls.__name__}[{size_t.__name__}, {decimal_t.__name__}]",
|
|
313
|
+
(cls,),
|
|
314
|
+
{},
|
|
315
|
+
lambda ns: ns.update(_size_t=size_t, _decimal_t=decimal_t),
|
|
316
|
+
)
|
|
317
|
+
return c
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class _UFixedNxM(
|
|
321
|
+
_ABIEncoded, typing.Generic[_TBitSize, _TDecimalPlaces], metaclass=_UFixedNxMMeta
|
|
322
|
+
):
|
|
323
|
+
_size_t: type[_TBitSize]
|
|
324
|
+
_decimal_t: type[_TDecimalPlaces]
|
|
325
|
+
_n: int
|
|
326
|
+
_m: int
|
|
327
|
+
_max_bits_len: int
|
|
328
|
+
_max_bytes_len: int
|
|
329
|
+
_value: bytes # underlying 'bytes' value representing the UFixedNxM
|
|
330
|
+
|
|
331
|
+
def __init__(self, value: str = "0.0", /):
|
|
332
|
+
"""
|
|
333
|
+
Construct an instance of UFixedNxM where value (v) is determined from the original
|
|
334
|
+
decimal value (d) by the formula v = round(d * (10^M))
|
|
335
|
+
"""
|
|
336
|
+
self._n = as_int(typing.get_args(self._size_t)[0], max=self._max_bits_len)
|
|
337
|
+
self._m = as_int(typing.get_args(self._decimal_t)[0], max=_MAX_M_SIZE)
|
|
338
|
+
self._max_int = 2**self._n - 1
|
|
339
|
+
self._max_bytes_len = self._n // BITS_IN_BYTE
|
|
340
|
+
|
|
341
|
+
value = as_string(value)
|
|
342
|
+
with decimal.localcontext(
|
|
343
|
+
decimal.Context(
|
|
344
|
+
prec=160,
|
|
345
|
+
traps=[
|
|
346
|
+
decimal.Rounded,
|
|
347
|
+
decimal.InvalidOperation,
|
|
348
|
+
decimal.Overflow,
|
|
349
|
+
decimal.DivisionByZero,
|
|
350
|
+
],
|
|
351
|
+
)
|
|
352
|
+
):
|
|
353
|
+
try:
|
|
354
|
+
d = decimal.Decimal(value)
|
|
355
|
+
except ArithmeticError as ex:
|
|
356
|
+
raise ValueError(f"Invalid decimal literal: {value}") from ex
|
|
357
|
+
if d < 0:
|
|
358
|
+
raise ValueError("Negative numbers not allowed")
|
|
359
|
+
try:
|
|
360
|
+
q = d.quantize(decimal.Decimal(f"1e-{self._m}"))
|
|
361
|
+
except ArithmeticError as ex:
|
|
362
|
+
raise ValueError(f"Too many decimals, expected max of {self._m}") from ex
|
|
363
|
+
|
|
364
|
+
int_value = round(q * (10**self._m))
|
|
365
|
+
int_value = as_int(int_value, max=self._max_int)
|
|
366
|
+
bytes_value = int_to_bytes(int_value, self._max_bytes_len)
|
|
367
|
+
self._value = as_bytes(bytes_value, max_size=self._max_bytes_len)
|
|
368
|
+
|
|
369
|
+
def __bool__(self) -> bool:
|
|
370
|
+
"""Returns `True` if not equal to zero"""
|
|
371
|
+
return bool(int.from_bytes(self._value))
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
375
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
376
|
+
value = as_bytes(value)
|
|
377
|
+
result = cls()
|
|
378
|
+
result._value = value
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def bytes(self) -> algopy.Bytes:
|
|
383
|
+
"""Get the underlying Bytes"""
|
|
384
|
+
import algopy
|
|
385
|
+
|
|
386
|
+
return algopy.Bytes(self._value)
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
390
|
+
"""Load an ABI type from application logs,
|
|
391
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
392
|
+
import algopy
|
|
393
|
+
|
|
394
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
395
|
+
return cls.from_bytes(log[4:])
|
|
396
|
+
raise ValueError("ABI return prefix not found")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class UFixedNxM(
|
|
400
|
+
_UFixedNxM[_TBitSize, _TDecimalPlaces], typing.Generic[_TBitSize, _TDecimalPlaces]
|
|
401
|
+
):
|
|
402
|
+
"""An ARC4 UFixed representing a decimal with the number of bits and precision specified.
|
|
403
|
+
|
|
404
|
+
Max size: 64 bits"""
|
|
405
|
+
|
|
406
|
+
_max_bits_len: int = UINT64_SIZE
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class BigUFixedNxM(
|
|
410
|
+
_UFixedNxM[_TBitSize, _TDecimalPlaces], typing.Generic[_TBitSize, _TDecimalPlaces]
|
|
411
|
+
):
|
|
412
|
+
"""An ARC4 UFixed representing a decimal with the number of bits and precision specified.
|
|
413
|
+
|
|
414
|
+
Max size: 512 bits"""
|
|
415
|
+
|
|
416
|
+
_max_bits_len: int = UINT512_SIZE
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class Byte(UIntN[typing.Literal[8]]):
|
|
420
|
+
"""An ARC4 alias for a UInt8"""
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
UInt8: typing.TypeAlias = UIntN[typing.Literal[8]]
|
|
424
|
+
"""An ARC4 UInt8"""
|
|
425
|
+
|
|
426
|
+
UInt16: typing.TypeAlias = UIntN[typing.Literal[16]]
|
|
427
|
+
"""An ARC4 UInt16"""
|
|
428
|
+
|
|
429
|
+
UInt32: typing.TypeAlias = UIntN[typing.Literal[32]]
|
|
430
|
+
"""An ARC4 UInt32"""
|
|
431
|
+
|
|
432
|
+
UInt64: typing.TypeAlias = UIntN[typing.Literal[64]]
|
|
433
|
+
"""An ARC4 UInt64"""
|
|
434
|
+
|
|
435
|
+
UInt128: typing.TypeAlias = BigUIntN[typing.Literal[128]]
|
|
436
|
+
"""An ARC4 UInt128"""
|
|
437
|
+
|
|
438
|
+
UInt256: typing.TypeAlias = BigUIntN[typing.Literal[256]]
|
|
439
|
+
"""An ARC4 UInt256"""
|
|
440
|
+
|
|
441
|
+
UInt512: typing.TypeAlias = BigUIntN[typing.Literal[512]]
|
|
442
|
+
"""An ARC4 UInt512"""
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class Bool(_ABIEncoded):
|
|
446
|
+
"""An ARC4 encoded bool"""
|
|
447
|
+
|
|
448
|
+
_value: bytes
|
|
449
|
+
|
|
450
|
+
# True value is encoded as having a 1 on the most significant bit (0x80 = 128)
|
|
451
|
+
_true_int_value = 128
|
|
452
|
+
_false_int_value = 0
|
|
453
|
+
|
|
454
|
+
def __init__(self, value: bool = False, /) -> None: # noqa: FBT001, FBT002
|
|
455
|
+
self._value = int_to_bytes(self._true_int_value if value else self._false_int_value, 1)
|
|
456
|
+
|
|
457
|
+
def __bool__(self) -> bool:
|
|
458
|
+
"""Allow Bool to be used in boolean contexts"""
|
|
459
|
+
return self.native
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def native(self) -> bool:
|
|
463
|
+
"""Return the bool representation of the value after ARC4 decoding"""
|
|
464
|
+
int_value = int.from_bytes(self._value)
|
|
465
|
+
return int_value == self._true_int_value
|
|
466
|
+
|
|
467
|
+
@classmethod
|
|
468
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
469
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
470
|
+
result = cls()
|
|
471
|
+
result._value = as_bytes(value)
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def bytes(self) -> algopy.Bytes:
|
|
476
|
+
"""Get the underlying Bytes"""
|
|
477
|
+
import algopy
|
|
478
|
+
|
|
479
|
+
return algopy.Bytes(self._value)
|
|
480
|
+
|
|
481
|
+
@classmethod
|
|
482
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
483
|
+
"""Load an ABI type from application logs,
|
|
484
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
485
|
+
import algopy
|
|
486
|
+
|
|
487
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
488
|
+
return cls.from_bytes(log[4:])
|
|
489
|
+
raise ValueError("ABI return prefix not found")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
_TArrayItem = typing.TypeVar("_TArrayItem")
|
|
493
|
+
_TArrayLength = typing.TypeVar("_TArrayLength", bound=int)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class _StaticArrayMeta(type(_ABIEncoded), typing.Generic[_TArrayItem, _TArrayLength]): # type: ignore # noqa: PGH003
|
|
497
|
+
__concrete__: typing.ClassVar[dict[tuple[type, type], type]] = {}
|
|
498
|
+
|
|
499
|
+
def __getitem__(cls, key_t: tuple[type[_TArrayItem], type[_TArrayLength]]) -> type:
|
|
500
|
+
cache = cls.__concrete__
|
|
501
|
+
if c := cache.get(key_t, None):
|
|
502
|
+
return c
|
|
503
|
+
|
|
504
|
+
cache[key_t] = c = types.new_class(
|
|
505
|
+
f"{cls.__name__}[{','.join([k.__name__ for k in key_t])}]",
|
|
506
|
+
(cls,),
|
|
507
|
+
{},
|
|
508
|
+
lambda ns: ns.update(
|
|
509
|
+
_child_types=[_TypeInfo(key_t[0])]
|
|
510
|
+
* (typing.get_args(key_t[1])[0] if len(typing.get_args(key_t[1])) > 0 else 0),
|
|
511
|
+
_array_item_t=key_t[0],
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
return c
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class _TypeInfo:
|
|
518
|
+
value: type
|
|
519
|
+
child_types: list[_TypeInfo] | None
|
|
520
|
+
|
|
521
|
+
def __init__(self, value: type, child_types: list[_TypeInfo] | None = None):
|
|
522
|
+
self.value = value
|
|
523
|
+
self.child_types = child_types or (
|
|
524
|
+
value._child_types if hasattr(value, "_child_types") else child_types
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class StaticArray(
|
|
529
|
+
_ABIEncoded,
|
|
530
|
+
typing.Generic[_TArrayItem, _TArrayLength],
|
|
531
|
+
Reversible[_TArrayItem],
|
|
532
|
+
metaclass=_StaticArrayMeta,
|
|
533
|
+
):
|
|
534
|
+
"""A fixed length ARC4 Array of the specified type and length"""
|
|
535
|
+
|
|
536
|
+
_array_item_t: type[_TArrayItem]
|
|
537
|
+
_child_types: list[_TypeInfo]
|
|
538
|
+
|
|
539
|
+
_value: bytes
|
|
540
|
+
|
|
541
|
+
def __init__(self, *items: _TArrayItem):
|
|
542
|
+
self._value = _encode(items)
|
|
543
|
+
if items:
|
|
544
|
+
self._array_item_t = type(items[0])
|
|
545
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
546
|
+
|
|
547
|
+
# ensure these two variables are set as instance variables instead of class variables
|
|
548
|
+
# to avoid sharing state between instances created by copy operation
|
|
549
|
+
if hasattr(self, "_array_item_t"):
|
|
550
|
+
self._array_item_t = self._array_item_t
|
|
551
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
552
|
+
|
|
553
|
+
def _get_type_info(self, item: _TArrayItem) -> _TypeInfo:
|
|
554
|
+
return _TypeInfo(
|
|
555
|
+
self._array_item_t,
|
|
556
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def __iter__(self) -> typing.Iterator[_TArrayItem]:
|
|
560
|
+
# """Returns an iterator for the items in the array"""
|
|
561
|
+
return iter(self._list())
|
|
562
|
+
|
|
563
|
+
def __reversed__(self) -> typing.Iterator[_TArrayItem]:
|
|
564
|
+
# """Returns an iterator for the items in the array, in reverse order"""
|
|
565
|
+
return reversed(self._list())
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def length(self) -> algopy.UInt64:
|
|
569
|
+
# """Returns the current length of the array"""
|
|
570
|
+
import algopy
|
|
571
|
+
|
|
572
|
+
return algopy.UInt64(len(self._list()))
|
|
573
|
+
|
|
574
|
+
@typing.overload
|
|
575
|
+
def __getitem__(self, value: algopy.UInt64 | int, /) -> _TArrayItem: ...
|
|
576
|
+
@typing.overload
|
|
577
|
+
def __getitem__(self, value: slice, /) -> list[_TArrayItem]: ...
|
|
578
|
+
def __getitem__(self, index: algopy.UInt64 | int | slice) -> _TArrayItem | list[_TArrayItem]:
|
|
579
|
+
if isinstance(index, slice):
|
|
580
|
+
return self._list()[index]
|
|
581
|
+
return self._list()[index]
|
|
582
|
+
|
|
583
|
+
def append(self, item: _TArrayItem, /) -> None:
|
|
584
|
+
"""Append items to this array"""
|
|
585
|
+
if not issubclass(type(item), self._array_item_t):
|
|
586
|
+
expected_type = self._array_item_t.__name__
|
|
587
|
+
actual_type = type(item).__name__
|
|
588
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
589
|
+
x = self._list()
|
|
590
|
+
x.append(item)
|
|
591
|
+
self._child_types.append(self._get_type_info(item))
|
|
592
|
+
self._value = _encode(x)
|
|
593
|
+
|
|
594
|
+
def extend(self, other: Iterable[_TArrayItem], /) -> None:
|
|
595
|
+
"""Extend this array with the contents of another array"""
|
|
596
|
+
if any(not issubclass(type(x), self._array_item_t) for x in other):
|
|
597
|
+
raise TypeError(f"items must be of type {self._array_item_t.__name__!r}")
|
|
598
|
+
x = self._list()
|
|
599
|
+
x.extend(other)
|
|
600
|
+
self._child_types.extend([self._get_type_info(x) for x in other])
|
|
601
|
+
self._value = _encode(x)
|
|
602
|
+
|
|
603
|
+
def __setitem__(self, index: algopy.UInt64 | int, value: _TArrayItem) -> _TArrayItem:
|
|
604
|
+
if not issubclass(type(value), self._array_item_t):
|
|
605
|
+
expected_type = self._array_item_t.__name__
|
|
606
|
+
actual_type = type(value).__name__
|
|
607
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
608
|
+
x = self._list()
|
|
609
|
+
x[index] = value
|
|
610
|
+
self._value = _encode(x)
|
|
611
|
+
return value
|
|
612
|
+
|
|
613
|
+
def copy(self) -> typing.Self:
|
|
614
|
+
# """Create a copy of this array"""
|
|
615
|
+
return copy.deepcopy(self)
|
|
616
|
+
|
|
617
|
+
def _list(self) -> list[_TArrayItem]:
|
|
618
|
+
return _decode(self._value, self._child_types)
|
|
619
|
+
|
|
620
|
+
@classmethod
|
|
621
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
622
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
623
|
+
value = as_bytes(value)
|
|
624
|
+
result = cls()
|
|
625
|
+
result._value = value
|
|
626
|
+
return result
|
|
627
|
+
|
|
628
|
+
@property
|
|
629
|
+
def bytes(self) -> algopy.Bytes:
|
|
630
|
+
"""Get the underlying Bytes"""
|
|
631
|
+
import algopy
|
|
632
|
+
|
|
633
|
+
return algopy.Bytes(self._value)
|
|
634
|
+
|
|
635
|
+
@classmethod
|
|
636
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
637
|
+
"""Load an ABI type from application logs,
|
|
638
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
639
|
+
import algopy
|
|
640
|
+
|
|
641
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
642
|
+
return cls.from_bytes(log[4:])
|
|
643
|
+
raise ValueError("ABI return prefix not found")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class Address(StaticArray[Byte, typing.Literal[32]]):
|
|
647
|
+
"""An alias for an array containing 32 bytes representing an Algorand address"""
|
|
648
|
+
|
|
649
|
+
def __init__(self, value: Account | str | algopy.Bytes = algosdk.constants.ZERO_ADDRESS):
|
|
650
|
+
"""
|
|
651
|
+
If `value` is a string, it should be a 58 character base32 string,
|
|
652
|
+
ie a base32 string-encoded 32 bytes public key + 4 bytes checksum.
|
|
653
|
+
If `value` is a Bytes, it's length checked to be 32 bytes - to avoid this
|
|
654
|
+
check, use `Address.from_bytes(...)` instead.
|
|
655
|
+
Defaults to the zero-address.
|
|
656
|
+
"""
|
|
657
|
+
if isinstance(value, str):
|
|
658
|
+
try:
|
|
659
|
+
bytes_value = algosdk.encoding.decode_address(value)
|
|
660
|
+
except Exception as e:
|
|
661
|
+
raise ValueError(f"cannot encode the following address: {value!r}") from e
|
|
662
|
+
else:
|
|
663
|
+
bytes_value = (
|
|
664
|
+
value.bytes.value if isinstance(value, Account) else as_bytes(value, max_size=32)
|
|
665
|
+
)
|
|
666
|
+
if len(bytes_value) != 32:
|
|
667
|
+
raise ValueError(f"expected 32 bytes, got: {len(bytes_value)}")
|
|
668
|
+
self._value = bytes_value
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def native(self) -> Account:
|
|
672
|
+
# """Return the Account representation of the address after ARC4 decoding"""
|
|
673
|
+
return Account(self.bytes)
|
|
674
|
+
|
|
675
|
+
def __bool__(self) -> bool:
|
|
676
|
+
# """Returns `True` if not equal to the zero address"""
|
|
677
|
+
zero_bytes = algosdk.encoding.decode_address(algosdk.constants.ZERO_ADDRESS)
|
|
678
|
+
return self.bytes != zero_bytes if isinstance(zero_bytes, bytes) else False
|
|
679
|
+
|
|
680
|
+
def __eq__(self, other: Address | Account | str) -> bool: # type: ignore[override]
|
|
681
|
+
"""Address equality is determined by the address of another
|
|
682
|
+
`arc4.Address`, `Account` or `str`"""
|
|
683
|
+
if isinstance(other, Address | Account):
|
|
684
|
+
return self.bytes == other.bytes
|
|
685
|
+
other_bytes = algosdk.encoding.decode_address(other)
|
|
686
|
+
return self.bytes == other_bytes if isinstance(other_bytes, bytes) else False
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class _DynamicArrayMeta(type(_ABIEncoded), typing.Generic[_TArrayItem, _TArrayLength]): # type: ignore # noqa: PGH003
|
|
690
|
+
__concrete__: typing.ClassVar[dict[type, type]] = {}
|
|
691
|
+
|
|
692
|
+
def __getitem__(cls, key_t: type[_TArrayItem]) -> type:
|
|
693
|
+
cache = cls.__concrete__
|
|
694
|
+
if c := cache.get(key_t, None):
|
|
695
|
+
return c
|
|
696
|
+
|
|
697
|
+
cache[key_t] = c = types.new_class(
|
|
698
|
+
f"{cls.__name__}[{key_t.__name__}]",
|
|
699
|
+
(cls,),
|
|
700
|
+
{},
|
|
701
|
+
lambda ns: ns.update(
|
|
702
|
+
_child_types=[],
|
|
703
|
+
_array_item_t=key_t,
|
|
704
|
+
),
|
|
705
|
+
)
|
|
706
|
+
return c
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class DynamicArray(
|
|
710
|
+
_ABIEncoded,
|
|
711
|
+
typing.Generic[_TArrayItem],
|
|
712
|
+
Reversible[_TArrayItem],
|
|
713
|
+
metaclass=_DynamicArrayMeta,
|
|
714
|
+
):
|
|
715
|
+
"""A dynamically sized ARC4 Array of the specified type"""
|
|
716
|
+
|
|
717
|
+
_array_item_t: type[_TArrayItem]
|
|
718
|
+
_child_types: list[_TypeInfo]
|
|
719
|
+
|
|
720
|
+
_value: bytes
|
|
721
|
+
|
|
722
|
+
def __init__(self, *items: _TArrayItem):
|
|
723
|
+
self._value = self._encode_with_length(items)
|
|
724
|
+
if items:
|
|
725
|
+
self._array_item_t = type(items[0])
|
|
726
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
727
|
+
|
|
728
|
+
# ensure these two variables are set as instance variables instead of class variables
|
|
729
|
+
# to avoid sharing state between instances created by copy operation
|
|
730
|
+
if hasattr(self, "_array_item_t"):
|
|
731
|
+
self._array_item_t = self._array_item_t
|
|
732
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
733
|
+
|
|
734
|
+
def _get_type_info(self, item: _TArrayItem) -> _TypeInfo:
|
|
735
|
+
return _TypeInfo(
|
|
736
|
+
self._array_item_t,
|
|
737
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def __iter__(self) -> typing.Iterator[_TArrayItem]:
|
|
741
|
+
"""Returns an iterator for the items in the array"""
|
|
742
|
+
return iter(self._list())
|
|
743
|
+
|
|
744
|
+
def __reversed__(self) -> typing.Iterator[_TArrayItem]:
|
|
745
|
+
"""Returns an iterator for the items in the array, in reverse order"""
|
|
746
|
+
return reversed(self._list())
|
|
747
|
+
|
|
748
|
+
@property
|
|
749
|
+
def length(self) -> algopy.UInt64:
|
|
750
|
+
"""Returns the current length of the array"""
|
|
751
|
+
import algopy
|
|
752
|
+
|
|
753
|
+
return algopy.UInt64(len(self._list()))
|
|
754
|
+
|
|
755
|
+
@typing.overload
|
|
756
|
+
def __getitem__(self, value: algopy.UInt64 | int, /) -> _TArrayItem: ...
|
|
757
|
+
@typing.overload
|
|
758
|
+
def __getitem__(self, value: slice, /) -> list[_TArrayItem]: ...
|
|
759
|
+
def __getitem__(self, index: algopy.UInt64 | int | slice) -> _TArrayItem | list[_TArrayItem]:
|
|
760
|
+
if isinstance(index, slice):
|
|
761
|
+
return self._list()[index]
|
|
762
|
+
return self._list()[index]
|
|
763
|
+
|
|
764
|
+
def append(self, item: _TArrayItem, /) -> None:
|
|
765
|
+
"""Append items to this array"""
|
|
766
|
+
if not issubclass(type(item), self._array_item_t):
|
|
767
|
+
expected_type = self._array_item_t.__name__
|
|
768
|
+
actual_type = type(item).__name__
|
|
769
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
770
|
+
x = self._list()
|
|
771
|
+
x.append(item)
|
|
772
|
+
self._child_types.append(self._get_type_info(item))
|
|
773
|
+
self._value = self._encode_with_length(x)
|
|
774
|
+
|
|
775
|
+
def extend(self, other: Iterable[_TArrayItem], /) -> None:
|
|
776
|
+
"""Extend this array with the contents of another array"""
|
|
777
|
+
if any(not issubclass(type(x), self._array_item_t) for x in other):
|
|
778
|
+
raise TypeError(f"items must be of type {self._array_item_t.__name__!r}")
|
|
779
|
+
x = self._list()
|
|
780
|
+
x.extend(other)
|
|
781
|
+
self._child_types.extend([self._get_type_info(x) for x in other])
|
|
782
|
+
self._value = self._encode_with_length(x)
|
|
783
|
+
|
|
784
|
+
def __setitem__(self, index: algopy.UInt64 | int, value: _TArrayItem) -> _TArrayItem:
|
|
785
|
+
if not issubclass(type(value), self._array_item_t):
|
|
786
|
+
expected_type = self._array_item_t.__name__
|
|
787
|
+
actual_type = type(value).__name__
|
|
788
|
+
raise TypeError(f"item must be of type {expected_type!r}, not {actual_type!r}")
|
|
789
|
+
x = self._list()
|
|
790
|
+
x[index] = value
|
|
791
|
+
self._value = self._encode_with_length(x)
|
|
792
|
+
return value
|
|
793
|
+
|
|
794
|
+
def __add__(self, other: Iterable[_TArrayItem]) -> DynamicArray[_TArrayItem]:
|
|
795
|
+
self.extend(other)
|
|
796
|
+
return self
|
|
797
|
+
|
|
798
|
+
def pop(self) -> _TArrayItem:
|
|
799
|
+
"""Remove and return the last item in the array"""
|
|
800
|
+
x = self._list()
|
|
801
|
+
item = x.pop()
|
|
802
|
+
self._child_types.pop()
|
|
803
|
+
self._value = self._encode_with_length(x)
|
|
804
|
+
return item
|
|
805
|
+
|
|
806
|
+
def copy(self) -> typing.Self:
|
|
807
|
+
"""Create a copy of this array"""
|
|
808
|
+
return copy.deepcopy(self)
|
|
809
|
+
|
|
810
|
+
def __bool__(self) -> bool:
|
|
811
|
+
"""Returns `True` if not an empty array"""
|
|
812
|
+
return bool(self._list())
|
|
813
|
+
|
|
814
|
+
def _list(self) -> list[_TArrayItem]:
|
|
815
|
+
length = int.from_bytes(self._value[:_ABI_LENGTH_SIZE])
|
|
816
|
+
self._child_types = self._child_types or []
|
|
817
|
+
if hasattr(self, "_array_item_t"):
|
|
818
|
+
self._child_types += [_TypeInfo(self._array_item_t)] * (
|
|
819
|
+
length - len(self._child_types)
|
|
820
|
+
)
|
|
821
|
+
return _decode(self._value[_ABI_LENGTH_SIZE:], self._child_types)
|
|
822
|
+
|
|
823
|
+
def _encode_with_length(self, items: list[_TArrayItem] | tuple[_TArrayItem, ...]) -> bytes:
|
|
824
|
+
return len(items).to_bytes(_ABI_LENGTH_SIZE) + _encode(items)
|
|
825
|
+
|
|
826
|
+
@classmethod
|
|
827
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
828
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
829
|
+
value = as_bytes(value)
|
|
830
|
+
result = cls()
|
|
831
|
+
result._value = value
|
|
832
|
+
return result
|
|
833
|
+
|
|
834
|
+
@property
|
|
835
|
+
def bytes(self) -> algopy.Bytes:
|
|
836
|
+
"""Get the underlying Bytes"""
|
|
837
|
+
import algopy
|
|
838
|
+
|
|
839
|
+
return algopy.Bytes(self._value)
|
|
840
|
+
|
|
841
|
+
@classmethod
|
|
842
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
843
|
+
"""Load an ABI type from application logs,
|
|
844
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
845
|
+
import algopy
|
|
846
|
+
|
|
847
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
848
|
+
return cls.from_bytes(log[4:])
|
|
849
|
+
raise ValueError("ABI return prefix not found")
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class DynamicBytes(DynamicArray[Byte]):
|
|
853
|
+
"""A variable sized array of bytes"""
|
|
854
|
+
|
|
855
|
+
@typing.overload
|
|
856
|
+
def __init__(self, *values: Byte | UInt8 | int): ...
|
|
857
|
+
|
|
858
|
+
@typing.overload
|
|
859
|
+
def __init__(self, value: algopy.Bytes | bytes, /): ...
|
|
860
|
+
|
|
861
|
+
def __init__(
|
|
862
|
+
self,
|
|
863
|
+
*value: algopy.Bytes | bytes | Byte | UInt8 | int,
|
|
864
|
+
):
|
|
865
|
+
import algopy
|
|
866
|
+
|
|
867
|
+
items = []
|
|
868
|
+
for x in value:
|
|
869
|
+
if isinstance(x, int):
|
|
870
|
+
items.append(Byte(x))
|
|
871
|
+
elif isinstance(x, Byte):
|
|
872
|
+
items.append(x)
|
|
873
|
+
elif isinstance(x, UInt8): # type: ignore[misc]
|
|
874
|
+
items.append(typing.cast(Byte, x))
|
|
875
|
+
elif isinstance(x, algopy.Bytes):
|
|
876
|
+
items.extend([Byte(int.from_bytes(i.value)) for i in x])
|
|
877
|
+
elif isinstance(x, bytes):
|
|
878
|
+
items.extend([Byte(int.from_bytes(i.value)) for i in algopy.Bytes(x)])
|
|
879
|
+
|
|
880
|
+
super().__init__(*items)
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def native(self) -> algopy.Bytes:
|
|
884
|
+
"""Return the Bytes representation of the address after ARC4 decoding"""
|
|
885
|
+
return self.bytes
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
_TTuple = typing.TypeVarTuple("_TTuple")
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class _TupleMeta(type(_ABIEncoded), typing.Generic[typing.Unpack[_TTuple]]): # type: ignore # noqa: PGH003
|
|
892
|
+
__concrete__: typing.ClassVar[dict[tuple, type]] = {} # type: ignore[type-arg]
|
|
893
|
+
|
|
894
|
+
def __getitem__(cls, key_t: tuple) -> type: # type: ignore[type-arg]
|
|
895
|
+
cache = cls.__concrete__
|
|
896
|
+
if c := cache.get(key_t, None):
|
|
897
|
+
return c
|
|
898
|
+
|
|
899
|
+
cache[key_t] = c = types.new_class(
|
|
900
|
+
f"{cls.__name__}[{key_t}]",
|
|
901
|
+
(cls,),
|
|
902
|
+
{},
|
|
903
|
+
lambda ns: ns.update(
|
|
904
|
+
_child_types=[_TypeInfo(typing.cast(type, item)) for item in key_t],
|
|
905
|
+
),
|
|
906
|
+
)
|
|
907
|
+
return c
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
class Tuple(
|
|
911
|
+
_ABIEncoded,
|
|
912
|
+
tuple[*_TTuple],
|
|
913
|
+
typing.Generic[typing.Unpack[_TTuple]],
|
|
914
|
+
metaclass=_TupleMeta,
|
|
915
|
+
):
|
|
916
|
+
"""An ARC4 ABI tuple, containing other ARC4 ABI types"""
|
|
917
|
+
|
|
918
|
+
__slots__ = ()
|
|
919
|
+
|
|
920
|
+
_child_types: list[_TypeInfo]
|
|
921
|
+
_value: bytes
|
|
922
|
+
|
|
923
|
+
def __init__(self, items: tuple[typing.Unpack[_TTuple]] = (), /): # type: ignore[assignment]
|
|
924
|
+
"""Construct an ARC4 tuple from a python tuple"""
|
|
925
|
+
self._value = _encode(items)
|
|
926
|
+
if items:
|
|
927
|
+
self._child_types = [self._get_type_info(item) for item in items]
|
|
928
|
+
|
|
929
|
+
# ensure the variable is set as instance variables instead of class variables
|
|
930
|
+
# to avoid sharing state between instances created by copy operation
|
|
931
|
+
self._child_types = self._child_types or [] if hasattr(self, "_child_types") else []
|
|
932
|
+
|
|
933
|
+
def _get_type_info(self, item: typing.Any) -> _TypeInfo:
|
|
934
|
+
return _TypeInfo(
|
|
935
|
+
type(item),
|
|
936
|
+
item._child_types if hasattr(item, "_child_types") else None,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
def __len__(self) -> int:
|
|
940
|
+
return len(self.native)
|
|
941
|
+
|
|
942
|
+
@typing.overload
|
|
943
|
+
def __getitem__(self, value: typing.SupportsIndex, /) -> object: ...
|
|
944
|
+
@typing.overload
|
|
945
|
+
def __getitem__(self, value: slice, /) -> tuple[object, ...]: ...
|
|
946
|
+
|
|
947
|
+
def __getitem__(self, index: typing.SupportsIndex | slice) -> tuple[object, ...] | object:
|
|
948
|
+
return self.native[index]
|
|
949
|
+
|
|
950
|
+
def __iter__(self) -> typing.Iterator[object]:
|
|
951
|
+
return iter(self.native)
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def native(self) -> tuple[typing.Unpack[_TTuple]]:
|
|
955
|
+
"""Return the Bytes representation of the address after ARC4 decoding"""
|
|
956
|
+
return typing.cast(
|
|
957
|
+
tuple[typing.Unpack[_TTuple]], tuple(_decode(self._value, self._child_types))
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
@classmethod
|
|
961
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
962
|
+
"""Construct an instance from the underlying bytes (no validation)"""
|
|
963
|
+
value = as_bytes(value)
|
|
964
|
+
result = cls()
|
|
965
|
+
result._value = value
|
|
966
|
+
return result
|
|
967
|
+
|
|
968
|
+
@property
|
|
969
|
+
def bytes(self) -> algopy.Bytes:
|
|
970
|
+
"""Get the underlying Bytes"""
|
|
971
|
+
import algopy
|
|
972
|
+
|
|
973
|
+
return algopy.Bytes(self._value)
|
|
974
|
+
|
|
975
|
+
@classmethod
|
|
976
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
977
|
+
"""Load an ABI type from application logs,
|
|
978
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
979
|
+
import algopy
|
|
980
|
+
|
|
981
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
982
|
+
return cls.from_bytes(log[4:])
|
|
983
|
+
raise ValueError("ABI return prefix not found")
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
@typing.dataclass_transform(
|
|
987
|
+
eq_default=False, order_default=False, kw_only_default=False, field_specifiers=()
|
|
988
|
+
)
|
|
989
|
+
class _StructMeta(type):
|
|
990
|
+
def __new__(
|
|
991
|
+
cls,
|
|
992
|
+
name: str,
|
|
993
|
+
bases: tuple[type, ...],
|
|
994
|
+
namespace: dict[str, object],
|
|
995
|
+
) -> _StructMeta:
|
|
996
|
+
return super().__new__(cls, name, bases, namespace)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
class Struct(metaclass=_StructMeta):
|
|
1000
|
+
"""Base class for ARC4 Struct types"""
|
|
1001
|
+
|
|
1002
|
+
def __init__(self, *args: typing.Any):
|
|
1003
|
+
self._value = Tuple(args)
|
|
1004
|
+
|
|
1005
|
+
@classmethod
|
|
1006
|
+
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
|
|
1007
|
+
annotations = _all_annotations(cls)
|
|
1008
|
+
|
|
1009
|
+
result = cls()
|
|
1010
|
+
tuple_value = Tuple[tuple(v for k, v in annotations.items())].from_bytes(value) # type: ignore[misc, attr-defined]
|
|
1011
|
+
result._value = tuple_value
|
|
1012
|
+
|
|
1013
|
+
return result
|
|
1014
|
+
|
|
1015
|
+
@property
|
|
1016
|
+
def bytes(self) -> algopy.Bytes:
|
|
1017
|
+
"""Get the underlying bytes[]"""
|
|
1018
|
+
return self._value.bytes
|
|
1019
|
+
|
|
1020
|
+
@classmethod
|
|
1021
|
+
def from_log(cls, log: algopy.Bytes, /) -> typing.Self:
|
|
1022
|
+
"""Load an ABI type from application logs,
|
|
1023
|
+
checking for the ABI return prefix `0x151f7c75`"""
|
|
1024
|
+
import algopy
|
|
1025
|
+
|
|
1026
|
+
if log[:4] == algopy.Bytes(ARC4_RETURN_PREFIX):
|
|
1027
|
+
return cls.from_bytes(log[4:])
|
|
1028
|
+
raise ValueError("ABI return prefix not found")
|
|
1029
|
+
|
|
1030
|
+
def copy(self) -> typing.Self:
|
|
1031
|
+
"""Create a copy of this struct"""
|
|
1032
|
+
return copy.deepcopy(self)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
class ARC4Client(typing.Protocol): ...
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
if typing.TYPE_CHECKING:
|
|
1039
|
+
_TABIArg: typing.TypeAlias = (
|
|
1040
|
+
algopy.String
|
|
1041
|
+
| algopy.BigUInt
|
|
1042
|
+
| algopy.UInt64
|
|
1043
|
+
| algopy.Bytes
|
|
1044
|
+
| algopy.Asset
|
|
1045
|
+
| algopy.Account
|
|
1046
|
+
| algopy.Application
|
|
1047
|
+
| UIntN[typing.Any]
|
|
1048
|
+
| BigUIntN[typing.Any]
|
|
1049
|
+
| UFixedNxM[typing.Any, typing.Any]
|
|
1050
|
+
| BigUFixedNxM[typing.Any, typing.Any]
|
|
1051
|
+
| Bool
|
|
1052
|
+
| String
|
|
1053
|
+
| StaticArray[typing.Any, typing.Any]
|
|
1054
|
+
| DynamicArray[typing.Any]
|
|
1055
|
+
| Tuple # type: ignore[type-arg]
|
|
1056
|
+
| int
|
|
1057
|
+
| bool
|
|
1058
|
+
| bytes
|
|
1059
|
+
| str
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
_TABIResult_co = typing.TypeVar("_TABIResult_co", covariant=True)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
class _ABICallWithReturnProtocol(typing.Protocol[_TABIResult_co]):
|
|
1067
|
+
def __call__( # noqa: PLR0913
|
|
1068
|
+
self,
|
|
1069
|
+
method: str,
|
|
1070
|
+
/,
|
|
1071
|
+
*args: _TABIArg,
|
|
1072
|
+
app_id: algopy.Application | algopy.UInt64 | int = ...,
|
|
1073
|
+
on_completion: algopy.OnCompleteAction = ...,
|
|
1074
|
+
approval_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1075
|
+
clear_state_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1076
|
+
global_num_uint: UInt64 | int = ...,
|
|
1077
|
+
global_num_bytes: UInt64 | int = ...,
|
|
1078
|
+
local_num_uint: UInt64 | int = ...,
|
|
1079
|
+
local_num_bytes: UInt64 | int = ...,
|
|
1080
|
+
extra_program_pages: UInt64 | int = ...,
|
|
1081
|
+
fee: algopy.UInt64 | int = 0,
|
|
1082
|
+
sender: algopy.Account | str = ...,
|
|
1083
|
+
note: algopy.Bytes | bytes | str = ...,
|
|
1084
|
+
rekey_to: algopy.Account | str = ...,
|
|
1085
|
+
) -> tuple[_TABIResult_co, algopy.itxn.ApplicationCallInnerTransaction]: ...
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
class _ABICallProtocolType(typing.Protocol):
|
|
1089
|
+
@typing.overload
|
|
1090
|
+
def __call__(
|
|
1091
|
+
self,
|
|
1092
|
+
method: typing.Callable[..., None] | str,
|
|
1093
|
+
/,
|
|
1094
|
+
*args: _TABIArg,
|
|
1095
|
+
app_id: algopy.Application | algopy.UInt64 | int = ...,
|
|
1096
|
+
on_completion: algopy.OnCompleteAction = ...,
|
|
1097
|
+
approval_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1098
|
+
clear_state_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1099
|
+
global_num_uint: UInt64 | int = ...,
|
|
1100
|
+
global_num_bytes: UInt64 | int = ...,
|
|
1101
|
+
local_num_uint: UInt64 | int = ...,
|
|
1102
|
+
local_num_bytes: UInt64 | int = ...,
|
|
1103
|
+
extra_program_pages: UInt64 | int = ...,
|
|
1104
|
+
fee: algopy.UInt64 | int = 0,
|
|
1105
|
+
sender: algopy.Account | str = ...,
|
|
1106
|
+
note: algopy.Bytes | bytes | str = ...,
|
|
1107
|
+
rekey_to: algopy.Account | str = ...,
|
|
1108
|
+
) -> algopy.itxn.ApplicationCallInnerTransaction: ...
|
|
1109
|
+
|
|
1110
|
+
@typing.overload
|
|
1111
|
+
def __call__( # type: ignore[misc, unused-ignore]
|
|
1112
|
+
self,
|
|
1113
|
+
method: typing.Callable[..., _TABIResult_co],
|
|
1114
|
+
/,
|
|
1115
|
+
*args: _TABIArg,
|
|
1116
|
+
app_id: algopy.Application | algopy.UInt64 | int = ...,
|
|
1117
|
+
on_completion: algopy.OnCompleteAction = ...,
|
|
1118
|
+
approval_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1119
|
+
clear_state_program: algopy.Bytes | bytes | tuple[algopy.Bytes, ...] = ...,
|
|
1120
|
+
global_num_uint: UInt64 | int = ...,
|
|
1121
|
+
global_num_bytes: UInt64 | int = ...,
|
|
1122
|
+
local_num_uint: UInt64 | int = ...,
|
|
1123
|
+
local_num_bytes: UInt64 | int = ...,
|
|
1124
|
+
extra_program_pages: UInt64 | int = ...,
|
|
1125
|
+
fee: algopy.UInt64 | int = 0,
|
|
1126
|
+
sender: algopy.Account | str = ...,
|
|
1127
|
+
note: algopy.Bytes | bytes | str = ...,
|
|
1128
|
+
rekey_to: algopy.Account | str = ...,
|
|
1129
|
+
) -> tuple[_TABIResult_co, algopy.itxn.ApplicationCallInnerTransaction]: ...
|
|
1130
|
+
|
|
1131
|
+
def __getitem__(
|
|
1132
|
+
self, _: type[_TABIResult_co]
|
|
1133
|
+
) -> _ABICallWithReturnProtocol[_TABIResult_co]: ...
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
class _ABICall:
|
|
1137
|
+
def __call__(
|
|
1138
|
+
self,
|
|
1139
|
+
method: typing.Callable[..., typing.Any] | str,
|
|
1140
|
+
/,
|
|
1141
|
+
*args: _TABIArg,
|
|
1142
|
+
**kwargs: typing.Any,
|
|
1143
|
+
) -> typing.Any:
|
|
1144
|
+
# Implement the actual abi_call logic here
|
|
1145
|
+
raise NotImplementedError("abi_call is not implemented")
|
|
1146
|
+
|
|
1147
|
+
def __getitem__(
|
|
1148
|
+
self, return_type: type[_TABIResult_co]
|
|
1149
|
+
) -> _ABICallWithReturnProtocol[_TABIResult_co]:
|
|
1150
|
+
return self
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# TODO: Implement abi_call
|
|
1154
|
+
abi_call: _ABICallProtocolType = _ABICall()
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@typing.overload
|
|
1158
|
+
def emit(event: Struct, /) -> None: ...
|
|
1159
|
+
@typing.overload
|
|
1160
|
+
def emit(event: str, /, *args: _TABIArg) -> None: ...
|
|
1161
|
+
def emit(event: str | Struct, /, *args: _TABIArg) -> None:
|
|
1162
|
+
"""Emit an ARC-28 event for the provided event signature or name, and provided args.
|
|
1163
|
+
|
|
1164
|
+
:param event: Either an ARC4 Struct, an event name, or event signature.
|
|
1165
|
+
* If event is an ARC4 Struct, the event signature will be determined from the Struct name and fields
|
|
1166
|
+
* If event is a signature, then the following args will be typed checked to ensure they match.
|
|
1167
|
+
* If event is just a name, the event signature will be inferred from the name and following arguments
|
|
1168
|
+
|
|
1169
|
+
:param args: When event is a signature or name, args will be used as the event data.
|
|
1170
|
+
They will all be encoded as single ARC4 Tuple
|
|
1171
|
+
|
|
1172
|
+
Example:
|
|
1173
|
+
```
|
|
1174
|
+
from algopy import ARC4Contract, arc4
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
class Swapped(arc4.Struct):
|
|
1178
|
+
a: arc4.UInt64
|
|
1179
|
+
b: arc4.UInt64
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
class EventEmitter(ARC4Contract):
|
|
1183
|
+
@arc4.abimethod
|
|
1184
|
+
def emit_swapped(self, a: arc4.UInt64, b: arc4.UInt64) -> None:
|
|
1185
|
+
arc4.emit(Swapped(b, a))
|
|
1186
|
+
arc4.emit("Swapped(uint64,uint64)", b, a)
|
|
1187
|
+
arc4.emit("Swapped", b, a)
|
|
1188
|
+
```
|
|
1189
|
+
""" # noqa: E501
|
|
1190
|
+
import algopy
|
|
1191
|
+
|
|
1192
|
+
from algopy_testing.context import get_test_context
|
|
1193
|
+
|
|
1194
|
+
context = get_test_context()
|
|
1195
|
+
active_txn = context.get_active_transaction()
|
|
1196
|
+
|
|
1197
|
+
if not active_txn:
|
|
1198
|
+
raise ValueError("Cannot emit events outside of application call context!")
|
|
1199
|
+
if active_txn.type != algopy.TransactionType.ApplicationCall:
|
|
1200
|
+
raise ValueError("Cannot emit events outside of application call context!")
|
|
1201
|
+
if not active_txn.app_id:
|
|
1202
|
+
raise ValueError("Cannot emit event: missing `app_id` in associated call transaction!")
|
|
1203
|
+
|
|
1204
|
+
if isinstance(event, str):
|
|
1205
|
+
arg_types = "(" + ",".join(abi_type_name_for_arg(arg=arg) for arg in args) + ")"
|
|
1206
|
+
|
|
1207
|
+
if event.find("(") == -1:
|
|
1208
|
+
event += arg_types
|
|
1209
|
+
elif event.find(arg_types) == -1:
|
|
1210
|
+
raise ValueError(f"Event signature {event} does not match args {args}")
|
|
1211
|
+
|
|
1212
|
+
args_tuple = tuple(_cast_arg_as_arc4(arg) for arg in args)
|
|
1213
|
+
event_hash = algopy.Bytes(SHA512.new(event.encode(), truncate="256").digest())
|
|
1214
|
+
context.add_application_logs(
|
|
1215
|
+
app_id=active_txn.app_id(),
|
|
1216
|
+
logs=(event_hash[:4] + Struct(*args_tuple).bytes).value,
|
|
1217
|
+
)
|
|
1218
|
+
elif isinstance(event, Struct):
|
|
1219
|
+
arg_types = "(" + ",".join(abi_type_name_for_arg(arg=arg) for arg in event._value) + ")"
|
|
1220
|
+
event_str = event.__class__.__name__ + arg_types
|
|
1221
|
+
event_hash = algopy.Bytes(SHA512.new(event_str.encode(), truncate="256").digest())
|
|
1222
|
+
context.add_application_logs(
|
|
1223
|
+
app_id=active_txn.app_id(),
|
|
1224
|
+
logs=(event_hash[:4] + event.bytes).value,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _cast_arg_as_arc4(arg: object) -> _TABIArg: # noqa: C901, PLR0911
|
|
1229
|
+
import algopy
|
|
1230
|
+
|
|
1231
|
+
if isinstance(arg, bool):
|
|
1232
|
+
return Bool(arg)
|
|
1233
|
+
if isinstance(arg, algopy.UInt64):
|
|
1234
|
+
return UInt64(arg)
|
|
1235
|
+
if isinstance(arg, algopy.BigUInt):
|
|
1236
|
+
return UInt512(arg)
|
|
1237
|
+
if isinstance(arg, int):
|
|
1238
|
+
return UInt64(arg) if arg <= MAX_UINT64 else UInt512(arg)
|
|
1239
|
+
if isinstance(arg, algopy.Bytes | bytes):
|
|
1240
|
+
return DynamicBytes(arg)
|
|
1241
|
+
if isinstance(arg, algopy.String | str):
|
|
1242
|
+
return String(arg)
|
|
1243
|
+
if isinstance(arg, algopy.Asset):
|
|
1244
|
+
return UInt64(arg.id)
|
|
1245
|
+
if isinstance(arg, algopy.Account):
|
|
1246
|
+
return Address(arg)
|
|
1247
|
+
if isinstance(arg, algopy.Application):
|
|
1248
|
+
return UInt64(arg.id)
|
|
1249
|
+
|
|
1250
|
+
if isinstance(
|
|
1251
|
+
arg,
|
|
1252
|
+
UIntN | BigUIntN | UFixedNxM | BigUFixedNxM | Bool | StaticArray | DynamicArray | Tuple,
|
|
1253
|
+
):
|
|
1254
|
+
return arg
|
|
1255
|
+
raise ValueError(f"Unsupported type {type(arg)}")
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
# https://stackoverflow.com/a/72037059
|
|
1259
|
+
def _all_annotations(cls: type) -> ChainMap[str, type]:
|
|
1260
|
+
"""Returns a dictionary-like ChainMap that includes annotations for all
|
|
1261
|
+
attributes defined in cls or inherited from superclasses."""
|
|
1262
|
+
return ChainMap(*(c.__annotations__ for c in cls.__mro__ if "__annotations__" in c.__dict__))
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _is_arc4_dynamic(value: object) -> bool:
|
|
1266
|
+
if isinstance(value, DynamicArray):
|
|
1267
|
+
return True
|
|
1268
|
+
elif isinstance(value, StaticArray | Tuple):
|
|
1269
|
+
return any(_is_arc4_dynamic(v) for v in value)
|
|
1270
|
+
return not isinstance(value, BigUFixedNxM | BigUIntN | UFixedNxM | UIntN | Bool)
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def _is_arc4_dynamic_type(value: _TypeInfo) -> bool:
|
|
1274
|
+
if issubclass(value.value, DynamicArray):
|
|
1275
|
+
return True
|
|
1276
|
+
elif value.child_types:
|
|
1277
|
+
return any(_is_arc4_dynamic_type(v) for v in value.child_types)
|
|
1278
|
+
return not issubclass(value.value, BigUFixedNxM | BigUIntN | UFixedNxM | UIntN | Bool)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _find_bool(
|
|
1282
|
+
values: (
|
|
1283
|
+
StaticArray[typing.Any, typing.Any]
|
|
1284
|
+
| DynamicArray[typing.Any]
|
|
1285
|
+
| Tuple[typing.Any]
|
|
1286
|
+
| tuple[typing.Any, ...]
|
|
1287
|
+
| list[typing.Any]
|
|
1288
|
+
),
|
|
1289
|
+
index: int,
|
|
1290
|
+
delta: int,
|
|
1291
|
+
) -> int:
|
|
1292
|
+
"""
|
|
1293
|
+
Helper function to find consecutive booleans from current index in a tuple.
|
|
1294
|
+
"""
|
|
1295
|
+
until = 0
|
|
1296
|
+
values_length = len(values) if isinstance(values, tuple | list) else values.length.value
|
|
1297
|
+
while True:
|
|
1298
|
+
curr = index + delta * until
|
|
1299
|
+
if isinstance(values[curr], Bool):
|
|
1300
|
+
if curr != values_length - 1 and delta > 0 or curr > 0 and delta < 0:
|
|
1301
|
+
until += 1
|
|
1302
|
+
else:
|
|
1303
|
+
break
|
|
1304
|
+
else:
|
|
1305
|
+
until -= 1
|
|
1306
|
+
break
|
|
1307
|
+
return until
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _find_bool_types(values: typing.Sequence[_TypeInfo], index: int, delta: int) -> int:
|
|
1311
|
+
"""
|
|
1312
|
+
Helper function to find consecutive booleans from current index in a tuple.
|
|
1313
|
+
"""
|
|
1314
|
+
until = 0
|
|
1315
|
+
values_length = len(values)
|
|
1316
|
+
while True:
|
|
1317
|
+
curr = index + delta * until
|
|
1318
|
+
if issubclass(values[curr].value, Bool):
|
|
1319
|
+
if curr != values_length - 1 and delta > 0 or curr > 0 and delta < 0:
|
|
1320
|
+
until += 1
|
|
1321
|
+
else:
|
|
1322
|
+
break
|
|
1323
|
+
else:
|
|
1324
|
+
until -= 1
|
|
1325
|
+
break
|
|
1326
|
+
return until
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def _compress_multiple_bool(value_list: list[Bool]) -> int:
|
|
1330
|
+
"""
|
|
1331
|
+
Compress consecutive boolean values into a byte for a Tuple/Array.
|
|
1332
|
+
"""
|
|
1333
|
+
result = 0
|
|
1334
|
+
if len(value_list) > 8:
|
|
1335
|
+
raise ValueError("length of list should not be greater than 8")
|
|
1336
|
+
for i, value in enumerate(value_list):
|
|
1337
|
+
assert isinstance(value, Bool)
|
|
1338
|
+
bool_val = value.native
|
|
1339
|
+
if bool_val:
|
|
1340
|
+
result |= 1 << (7 - i)
|
|
1341
|
+
return result
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _get_max_bytes_len(type_info: _TypeInfo) -> int:
|
|
1345
|
+
size = 0
|
|
1346
|
+
if issubclass(type_info.value, DynamicArray):
|
|
1347
|
+
size += _ABI_LENGTH_SIZE
|
|
1348
|
+
elif type_info.child_types:
|
|
1349
|
+
i = 0
|
|
1350
|
+
child_types = type_info.child_types or []
|
|
1351
|
+
while i < len(child_types):
|
|
1352
|
+
if issubclass(child_types[i].value, Bool):
|
|
1353
|
+
after = _find_bool_types(child_types, i, 1)
|
|
1354
|
+
i += after
|
|
1355
|
+
bool_num = after + 1
|
|
1356
|
+
size += bool_num // 8
|
|
1357
|
+
if bool_num % 8 != 0:
|
|
1358
|
+
size += 1
|
|
1359
|
+
else:
|
|
1360
|
+
child_byte_size = _get_max_bytes_len(child_types[i])
|
|
1361
|
+
size += child_byte_size
|
|
1362
|
+
i += 1
|
|
1363
|
+
|
|
1364
|
+
else:
|
|
1365
|
+
value = type_info.value()
|
|
1366
|
+
if hasattr(value, "_max_bytes_len"):
|
|
1367
|
+
size = typing.cast(int, value._max_bytes_len)
|
|
1368
|
+
|
|
1369
|
+
return size
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def _encode(
|
|
1373
|
+
values: (
|
|
1374
|
+
StaticArray[typing.Any, typing.Any]
|
|
1375
|
+
| DynamicArray[typing.Any]
|
|
1376
|
+
| Tuple[typing.Any]
|
|
1377
|
+
| tuple[typing.Any, ...]
|
|
1378
|
+
| list[typing.Any]
|
|
1379
|
+
),
|
|
1380
|
+
) -> bytes:
|
|
1381
|
+
heads = []
|
|
1382
|
+
tails = []
|
|
1383
|
+
is_dynamic_index = []
|
|
1384
|
+
i = 0
|
|
1385
|
+
values_length = len(values) if hasattr(values, "__len__") else values.length.value
|
|
1386
|
+
values_length_bytes = (
|
|
1387
|
+
int_to_bytes(values_length, _ABI_LENGTH_SIZE) if isinstance(values, DynamicArray) else b""
|
|
1388
|
+
)
|
|
1389
|
+
while i < values_length:
|
|
1390
|
+
value = values[i]
|
|
1391
|
+
is_dynamic_index.append(_is_arc4_dynamic(value))
|
|
1392
|
+
if is_dynamic_index[-1]:
|
|
1393
|
+
heads.append(b"\x00\x00")
|
|
1394
|
+
tail_encoding = value.bytes.value if isinstance(value, String) else _encode(value)
|
|
1395
|
+
tails.append(tail_encoding)
|
|
1396
|
+
else:
|
|
1397
|
+
if isinstance(value, Bool):
|
|
1398
|
+
before = _find_bool(values, i, -1)
|
|
1399
|
+
after = _find_bool(values, i, 1)
|
|
1400
|
+
|
|
1401
|
+
# Pack bytes to heads and tails
|
|
1402
|
+
if before % 8 != 0:
|
|
1403
|
+
raise ValueError(
|
|
1404
|
+
"expected before index should have number of bool mod 8 equal 0"
|
|
1405
|
+
)
|
|
1406
|
+
after = min(7, after)
|
|
1407
|
+
consecutive_bool_list = typing.cast(list[Bool], values[i : i + after + 1])
|
|
1408
|
+
compressed_int = _compress_multiple_bool(consecutive_bool_list)
|
|
1409
|
+
heads.append(bytes([compressed_int]))
|
|
1410
|
+
i += after
|
|
1411
|
+
else:
|
|
1412
|
+
heads.append(value.bytes.value)
|
|
1413
|
+
tails.append(b"")
|
|
1414
|
+
i += 1
|
|
1415
|
+
|
|
1416
|
+
# Adjust heads for dynamic types
|
|
1417
|
+
head_length = 0
|
|
1418
|
+
for head_element in heads:
|
|
1419
|
+
# If the element is not a placeholder, append the length of the element
|
|
1420
|
+
head_length += len(head_element)
|
|
1421
|
+
|
|
1422
|
+
# Correctly encode dynamic types and replace placeholder
|
|
1423
|
+
tail_curr_length = 0
|
|
1424
|
+
for i in range(len(heads)):
|
|
1425
|
+
if is_dynamic_index[i]:
|
|
1426
|
+
head_value = as_int16(head_length + tail_curr_length)
|
|
1427
|
+
heads[i] = int_to_bytes(head_value, _ABI_LENGTH_SIZE)
|
|
1428
|
+
|
|
1429
|
+
tail_curr_length += len(tails[i])
|
|
1430
|
+
|
|
1431
|
+
# Concatenate bytes
|
|
1432
|
+
return values_length_bytes + b"".join(heads) + b"".join(tails)
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _decode( # noqa: PLR0912, C901
|
|
1436
|
+
value: bytes, child_types: typing.Sequence[_TypeInfo]
|
|
1437
|
+
) -> list[typing.Any]:
|
|
1438
|
+
dynamic_segments: list[list[int]] = [] # Store the start and end of a dynamic element
|
|
1439
|
+
value_partitions: list[bytes | None] = []
|
|
1440
|
+
i = 0
|
|
1441
|
+
array_index = 0
|
|
1442
|
+
|
|
1443
|
+
while i < len(child_types):
|
|
1444
|
+
child_type = child_types[i]
|
|
1445
|
+
if _is_arc4_dynamic_type(child_type):
|
|
1446
|
+
# Decode the size of the dynamic element
|
|
1447
|
+
dynamic_index = int.from_bytes(value[array_index : array_index + _ABI_LENGTH_SIZE])
|
|
1448
|
+
if len(dynamic_segments) > 0:
|
|
1449
|
+
dynamic_segments[-1][1] = dynamic_index
|
|
1450
|
+
|
|
1451
|
+
# Since we do not know where the current dynamic element ends,
|
|
1452
|
+
# put a placeholder and update later
|
|
1453
|
+
dynamic_segments.append([dynamic_index, -1])
|
|
1454
|
+
value_partitions.append(None)
|
|
1455
|
+
array_index += _ABI_LENGTH_SIZE
|
|
1456
|
+
elif issubclass(child_type.value, Bool):
|
|
1457
|
+
before = _find_bool_types(child_types, i, -1)
|
|
1458
|
+
after = _find_bool_types(child_types, i, 1)
|
|
1459
|
+
|
|
1460
|
+
if before % 8 != 0:
|
|
1461
|
+
raise ValueError("expected before index should have number of bool mod 8 equal 0")
|
|
1462
|
+
after = min(7, after)
|
|
1463
|
+
bits = int.from_bytes(value[array_index : array_index + 1])
|
|
1464
|
+
# Parse bool values into multiple byte strings
|
|
1465
|
+
for bool_i in range(after + 1):
|
|
1466
|
+
mask = 128 >> bool_i
|
|
1467
|
+
if mask & bits:
|
|
1468
|
+
value_partitions.append(b"\x80")
|
|
1469
|
+
else:
|
|
1470
|
+
value_partitions.append(b"\x00")
|
|
1471
|
+
i += after
|
|
1472
|
+
array_index += 1
|
|
1473
|
+
else:
|
|
1474
|
+
curr_len = _get_max_bytes_len(child_type)
|
|
1475
|
+
value_partitions.append(value[array_index : array_index + curr_len])
|
|
1476
|
+
array_index += curr_len
|
|
1477
|
+
|
|
1478
|
+
if array_index >= len(value) and i != len(child_types) - 1:
|
|
1479
|
+
raise ValueError(f"input string is not long enough to be decoded: {value!r}")
|
|
1480
|
+
|
|
1481
|
+
i += 1
|
|
1482
|
+
|
|
1483
|
+
if len(dynamic_segments) > 0:
|
|
1484
|
+
dynamic_segments[len(dynamic_segments) - 1][1] = len(value)
|
|
1485
|
+
array_index = len(value)
|
|
1486
|
+
if array_index < len(value):
|
|
1487
|
+
raise ValueError(f"input string was not fully consumed: {value!r}")
|
|
1488
|
+
|
|
1489
|
+
# Check dynamic element partitions
|
|
1490
|
+
segment_index = 0
|
|
1491
|
+
for i, child_type in enumerate(child_types):
|
|
1492
|
+
if _is_arc4_dynamic_type(child_type):
|
|
1493
|
+
segment_start, segment_end = dynamic_segments[segment_index]
|
|
1494
|
+
value_partitions[i] = value[segment_start:segment_end]
|
|
1495
|
+
segment_index += 1
|
|
1496
|
+
|
|
1497
|
+
# Decode individual tuple elements
|
|
1498
|
+
values = []
|
|
1499
|
+
for i, child_type in enumerate(child_types):
|
|
1500
|
+
val = child_type.value.from_bytes(value_partitions[i]) # type: ignore[attr-defined]
|
|
1501
|
+
val._child_types = child_type.child_types
|
|
1502
|
+
values.append(val)
|
|
1503
|
+
return values
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
__all__ = [
|
|
1507
|
+
"ARC4Client",
|
|
1508
|
+
"Address",
|
|
1509
|
+
"BigUFixedNxM",
|
|
1510
|
+
"BigUIntN",
|
|
1511
|
+
"Bool",
|
|
1512
|
+
"Byte",
|
|
1513
|
+
"DynamicArray",
|
|
1514
|
+
"DynamicBytes",
|
|
1515
|
+
"StaticArray",
|
|
1516
|
+
"String",
|
|
1517
|
+
"Struct",
|
|
1518
|
+
"Tuple",
|
|
1519
|
+
"UFixedNxM",
|
|
1520
|
+
"UInt128",
|
|
1521
|
+
"UInt16",
|
|
1522
|
+
"UInt256",
|
|
1523
|
+
"UInt32",
|
|
1524
|
+
"UInt512",
|
|
1525
|
+
"UInt64",
|
|
1526
|
+
"UInt8",
|
|
1527
|
+
"UIntN",
|
|
1528
|
+
"abi_call",
|
|
1529
|
+
"abimethod",
|
|
1530
|
+
"arc4_signature",
|
|
1531
|
+
"baremethod",
|
|
1532
|
+
"emit",
|
|
1533
|
+
]
|