algorand-python-testing 0.2.1__py3-none-any.whl → 0.2.2b2__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 +6 -0
- algopy_testing/context.py +12 -8
- algopy_testing/enums.py +35 -15
- algopy_testing/models/box.py +458 -96
- algopy_testing/models/contract.py +12 -0
- algopy_testing/op.py +156 -1
- algopy_testing/state/global_state.py +18 -1
- algopy_testing/state/local_state.py +14 -1
- algopy_testing/utilities/__init__.py +1 -0
- {algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/METADATA +14 -3
- {algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/RECORD +13 -13
- {algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/WHEEL +0 -0
- {algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/licenses/LICENSE +0 -0
algopy/__init__.py
CHANGED
|
@@ -10,11 +10,14 @@ from algopy_testing.models import (
|
|
|
10
10
|
GTxn,
|
|
11
11
|
ITxn,
|
|
12
12
|
LogicSig,
|
|
13
|
+
StateTotals,
|
|
13
14
|
TemplateVar,
|
|
14
15
|
Txn,
|
|
15
16
|
logicsig,
|
|
17
|
+
uenumerate,
|
|
16
18
|
urange,
|
|
17
19
|
)
|
|
20
|
+
from algopy_testing.models.box import Box, BoxMap, BoxRef
|
|
18
21
|
from algopy_testing.primitives import BigUInt, Bytes, String, UInt64
|
|
19
22
|
from algopy_testing.protocols import BytesBacked
|
|
20
23
|
from algopy_testing.state import GlobalState, LocalState
|
|
@@ -55,4 +58,7 @@ __all__ = [
|
|
|
55
58
|
"subroutine",
|
|
56
59
|
"uenumerate",
|
|
57
60
|
"urange",
|
|
61
|
+
"Box",
|
|
62
|
+
"BoxRef",
|
|
63
|
+
"BoxMap",
|
|
58
64
|
]
|
algopy_testing/context.py
CHANGED
|
@@ -285,7 +285,7 @@ class AlgopyTestContext:
|
|
|
285
285
|
self._scratch_spaces: dict[str, list[algopy.Bytes | algopy.UInt64 | bytes | int]] = {}
|
|
286
286
|
self._template_vars: dict[str, Any] = template_vars or {}
|
|
287
287
|
self._blocks: dict[int, dict[str, int]] = {}
|
|
288
|
-
self._boxes: dict[bytes,
|
|
288
|
+
self._boxes: dict[bytes, bytes] = {}
|
|
289
289
|
self._lsigs: dict[algopy.LogicSig, Callable[[], algopy.UInt64 | bool]] = {}
|
|
290
290
|
self._active_lsig_args: Sequence[algopy.Bytes] = []
|
|
291
291
|
|
|
@@ -1047,20 +1047,23 @@ class AlgopyTestContext:
|
|
|
1047
1047
|
|
|
1048
1048
|
return new_txn
|
|
1049
1049
|
|
|
1050
|
-
def
|
|
1050
|
+
def does_box_exist(self, name: algopy.Bytes | bytes) -> bool:
|
|
1051
|
+
"""return true if the box with the given name exists."""
|
|
1052
|
+
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1053
|
+
return name_bytes in self._boxes
|
|
1054
|
+
|
|
1055
|
+
def get_box(self, name: algopy.Bytes | bytes) -> bytes:
|
|
1051
1056
|
"""Get the content of a box."""
|
|
1052
|
-
import algopy
|
|
1053
1057
|
|
|
1054
1058
|
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1055
|
-
return self._boxes.get(name_bytes,
|
|
1059
|
+
return self._boxes.get(name_bytes, b"")
|
|
1056
1060
|
|
|
1057
1061
|
def set_box(self, name: algopy.Bytes | bytes, content: algopy.Bytes | bytes) -> None:
|
|
1058
1062
|
"""Set the content of a box."""
|
|
1059
|
-
import algopy
|
|
1060
1063
|
|
|
1061
1064
|
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1062
1065
|
content_bytes = content if isinstance(content, bytes) else content.value
|
|
1063
|
-
self._boxes[name_bytes] =
|
|
1066
|
+
self._boxes[name_bytes] = content_bytes
|
|
1064
1067
|
|
|
1065
1068
|
def execute_logicsig(
|
|
1066
1069
|
self, lsig: algopy.LogicSig, lsig_args: Sequence[algopy.Bytes] | None = None
|
|
@@ -1071,12 +1074,14 @@ class AlgopyTestContext:
|
|
|
1071
1074
|
self._lsigs[lsig] = lsig.func
|
|
1072
1075
|
return lsig.func()
|
|
1073
1076
|
|
|
1074
|
-
def clear_box(self, name: algopy.Bytes | bytes) ->
|
|
1077
|
+
def clear_box(self, name: algopy.Bytes | bytes) -> bool:
|
|
1075
1078
|
"""Clear the content of a box."""
|
|
1076
1079
|
|
|
1077
1080
|
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1078
1081
|
if name_bytes in self._boxes:
|
|
1079
1082
|
del self._boxes[name_bytes]
|
|
1083
|
+
return True
|
|
1084
|
+
return False
|
|
1080
1085
|
|
|
1081
1086
|
def clear_all_boxes(self) -> None:
|
|
1082
1087
|
"""Clear all boxes."""
|
|
@@ -1170,7 +1175,6 @@ class AlgopyTestContext:
|
|
|
1170
1175
|
self._app_id = iter(range(1, 2**64))
|
|
1171
1176
|
|
|
1172
1177
|
|
|
1173
|
-
#
|
|
1174
1178
|
_var: ContextVar[AlgopyTestContext] = ContextVar("_var")
|
|
1175
1179
|
|
|
1176
1180
|
|
algopy_testing/enums.py
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from enum import Enum, StrEnum
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
NoOp = 0
|
|
6
|
-
OptIn = 1
|
|
7
|
-
CloseOut = 2
|
|
8
|
-
ClearState = 3
|
|
9
|
-
UpdateApplication = 4
|
|
10
|
-
DeleteApplication = 5
|
|
5
|
+
from algopy_testing.primitives import UInt64
|
|
11
6
|
|
|
12
7
|
|
|
13
|
-
class
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
class OnCompleteAction(UInt64):
|
|
9
|
+
NoOp: OnCompleteAction
|
|
10
|
+
OptIn: OnCompleteAction
|
|
11
|
+
CloseOut: OnCompleteAction
|
|
12
|
+
ClearState: OnCompleteAction
|
|
13
|
+
UpdateApplication: OnCompleteAction
|
|
14
|
+
DeleteApplication: OnCompleteAction
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
OnCompleteAction.NoOp = OnCompleteAction(0)
|
|
18
|
+
OnCompleteAction.OptIn = OnCompleteAction(1)
|
|
19
|
+
OnCompleteAction.CloseOut = OnCompleteAction(2)
|
|
20
|
+
OnCompleteAction.ClearState = OnCompleteAction(3)
|
|
21
|
+
OnCompleteAction.UpdateApplication = OnCompleteAction(4)
|
|
22
|
+
OnCompleteAction.DeleteApplication = OnCompleteAction(5)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TransactionType(UInt64):
|
|
26
|
+
Payment: TransactionType
|
|
27
|
+
KeyRegistration: TransactionType
|
|
28
|
+
AssetConfig: TransactionType
|
|
29
|
+
AssetTransfer: TransactionType
|
|
30
|
+
AssetFreeze: TransactionType
|
|
31
|
+
ApplicationCall: TransactionType
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
TransactionType.Payment = TransactionType(0)
|
|
35
|
+
TransactionType.KeyRegistration = TransactionType(1)
|
|
36
|
+
TransactionType.AssetConfig = TransactionType(2)
|
|
37
|
+
TransactionType.AssetTransfer = TransactionType(3)
|
|
38
|
+
TransactionType.AssetFreeze = TransactionType(4)
|
|
39
|
+
TransactionType.ApplicationCall = TransactionType(5)
|
|
20
40
|
|
|
21
41
|
|
|
22
42
|
class ECDSA(Enum):
|
algopy_testing/models/box.py
CHANGED
|
@@ -1,141 +1,258 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import typing
|
|
4
4
|
|
|
5
|
+
from algopy_testing.constants import MAX_BOX_SIZE
|
|
5
6
|
from algopy_testing.context import get_test_context
|
|
7
|
+
from algopy_testing.utils import as_bytes, as_string
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
_TKey = typing.TypeVar("_TKey")
|
|
10
|
+
_TValue = typing.TypeVar("_TValue")
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
8
13
|
import algopy
|
|
9
14
|
|
|
10
15
|
|
|
11
|
-
class Box:
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
class Box(typing.Generic[_TValue]):
|
|
17
|
+
"""
|
|
18
|
+
Box abstracts the reading and writing of a single value to a single box.
|
|
19
|
+
The box size will be reconfigured dynamically to fit the size of the value being assigned to
|
|
20
|
+
it.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, type_: type[_TValue], /, *, key: bytes | str | algopy.Bytes | algopy.String = ""
|
|
25
|
+
) -> None:
|
|
14
26
|
import algopy
|
|
15
27
|
|
|
28
|
+
self._type = type_
|
|
29
|
+
|
|
30
|
+
self._key = (
|
|
31
|
+
algopy.String(as_string(key)).bytes
|
|
32
|
+
if isinstance(key, str | algopy.String)
|
|
33
|
+
else algopy.Bytes(as_bytes(key))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __bool__(self) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Returns True if the box exists, regardless of the truthiness of the contents
|
|
39
|
+
of the box
|
|
40
|
+
"""
|
|
16
41
|
context = get_test_context()
|
|
17
|
-
|
|
18
|
-
size = int(b)
|
|
19
|
-
if not name_bytes or size > 32768:
|
|
20
|
-
raise ValueError("Invalid box name or size")
|
|
21
|
-
if context.get_box(name_bytes):
|
|
22
|
-
return False
|
|
23
|
-
context.set_box(name_bytes, b"\x00" * size)
|
|
24
|
-
return True
|
|
42
|
+
return context.does_box_exist(self.key)
|
|
25
43
|
|
|
26
|
-
@
|
|
27
|
-
def
|
|
28
|
-
|
|
44
|
+
@property
|
|
45
|
+
def key(self) -> algopy.Bytes:
|
|
46
|
+
"""Provides access to the raw storage key"""
|
|
47
|
+
if not self._key:
|
|
48
|
+
raise RuntimeError("Box key is empty")
|
|
49
|
+
return self._key
|
|
29
50
|
|
|
51
|
+
@property
|
|
52
|
+
def value(self) -> _TValue:
|
|
53
|
+
"""Retrieve the contents of the box. Fails if the box has not been created."""
|
|
30
54
|
context = get_test_context()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return True
|
|
35
|
-
return False
|
|
55
|
+
if not context.does_box_exist(self.key):
|
|
56
|
+
raise RuntimeError("Box has not been created")
|
|
57
|
+
return _cast_to_value_type(self._type, context.get_box(self.key))
|
|
36
58
|
|
|
37
|
-
@
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
@value.setter
|
|
60
|
+
def value(self, value: _TValue) -> None:
|
|
61
|
+
"""Write _value_ to the box. Creates the box if it does not exist."""
|
|
62
|
+
context = get_test_context()
|
|
63
|
+
bytes_value = _cast_to_bytes(value)
|
|
64
|
+
context.set_box(self.key, bytes_value)
|
|
42
65
|
|
|
66
|
+
@value.deleter
|
|
67
|
+
def value(self) -> None:
|
|
68
|
+
"""Delete the box"""
|
|
43
69
|
context = get_test_context()
|
|
44
|
-
|
|
45
|
-
start = int(b)
|
|
46
|
-
length = int(c)
|
|
47
|
-
box_content = context.get_box(name_bytes)
|
|
48
|
-
if not box_content:
|
|
49
|
-
raise ValueError("Box does not exist")
|
|
50
|
-
return box_content[start : start + length]
|
|
70
|
+
context.clear_box(self.key)
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
def get(self, *, default: _TValue) -> _TValue:
|
|
73
|
+
"""
|
|
74
|
+
Retrieve the contents of the box, or return the default value if the box has not been
|
|
75
|
+
created.
|
|
76
|
+
|
|
77
|
+
:arg default: The default value to return if the box has not been created
|
|
78
|
+
"""
|
|
79
|
+
box_content, box_exists = self.maybe()
|
|
80
|
+
return default if not box_exists else box_content
|
|
55
81
|
|
|
82
|
+
def maybe(self) -> tuple[_TValue, bool]:
|
|
83
|
+
"""
|
|
84
|
+
Retrieve the contents of the box if it exists, and return a boolean indicating if the box
|
|
85
|
+
exists.
|
|
86
|
+
|
|
87
|
+
"""
|
|
56
88
|
context = get_test_context()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
box_exists = context.does_box_exist(self.key)
|
|
90
|
+
box_content_bytes = context.get_box(self.key)
|
|
91
|
+
box_content = _cast_to_value_type(self._type, box_content_bytes)
|
|
92
|
+
return box_content, box_exists
|
|
60
93
|
|
|
61
|
-
@
|
|
62
|
-
def length(
|
|
94
|
+
@property
|
|
95
|
+
def length(self) -> algopy.UInt64:
|
|
96
|
+
"""
|
|
97
|
+
Get the length of this Box. Fails if the box does not exist
|
|
98
|
+
"""
|
|
63
99
|
import algopy
|
|
64
100
|
|
|
65
101
|
context = get_test_context()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return algopy.UInt64(len(
|
|
102
|
+
if not context.does_box_exist(self.key):
|
|
103
|
+
raise RuntimeError("Box has not been created")
|
|
104
|
+
return algopy.UInt64(len(context.get_box(self.key)))
|
|
105
|
+
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
class BoxRef:
|
|
108
|
+
"""
|
|
109
|
+
BoxRef abstracts the reading and writing of boxes containing raw binary data. The size is
|
|
110
|
+
configured manually, and can be set to values larger than what the AVM can handle in a single
|
|
111
|
+
value.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, /, *, key: bytes | str | algopy.Bytes | algopy.String = "") -> None:
|
|
72
115
|
import algopy
|
|
73
116
|
|
|
117
|
+
self._key = (
|
|
118
|
+
algopy.String(as_string(key)).bytes
|
|
119
|
+
if isinstance(key, str | algopy.String)
|
|
120
|
+
else algopy.Bytes(as_bytes(key))
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def __bool__(self) -> bool:
|
|
124
|
+
"""Returns True if the box has a value set, regardless of the truthiness of that value"""
|
|
74
125
|
context = get_test_context()
|
|
75
|
-
|
|
76
|
-
content = b.value if isinstance(b, algopy.Bytes) else b
|
|
77
|
-
existing_content = context.get_box(name_bytes)
|
|
78
|
-
if existing_content and len(existing_content) != len(content):
|
|
79
|
-
raise ValueError("New content length does not match existing box length")
|
|
80
|
-
context.set_box(name_bytes, content)
|
|
126
|
+
return context.does_box_exist(self.key)
|
|
81
127
|
|
|
82
|
-
@
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
@property
|
|
129
|
+
def key(self) -> algopy.Bytes:
|
|
130
|
+
"""Provides access to the raw storage key"""
|
|
131
|
+
if not self._key:
|
|
132
|
+
raise RuntimeError("Box key is empty")
|
|
133
|
+
|
|
134
|
+
return self._key
|
|
135
|
+
|
|
136
|
+
def create(self, *, size: algopy.UInt64 | int) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Creates a box with the specified size, setting all bits to zero. Fails if the box already
|
|
139
|
+
exists with a different size. Fails if the specified size is greater than the max box size
|
|
140
|
+
(32,768)
|
|
141
|
+
|
|
142
|
+
Returns True if the box was created, False if the box already existed
|
|
143
|
+
"""
|
|
144
|
+
size_int = int(size)
|
|
145
|
+
if size_int > MAX_BOX_SIZE:
|
|
146
|
+
raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")
|
|
147
|
+
|
|
148
|
+
box_content, box_exists = self._maybe()
|
|
149
|
+
if box_exists and len(box_content) != size_int:
|
|
150
|
+
raise ValueError("Box already exists with a different size")
|
|
151
|
+
if box_exists:
|
|
152
|
+
return False
|
|
153
|
+
context = get_test_context()
|
|
154
|
+
context.set_box(self.key, b"\x00" * size_int)
|
|
155
|
+
return True
|
|
87
156
|
|
|
157
|
+
def delete(self) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Deletes the box if it exists and returns a value indicating if the box existed
|
|
160
|
+
"""
|
|
88
161
|
context = get_test_context()
|
|
89
|
-
|
|
90
|
-
start = int(b)
|
|
91
|
-
new_content = c.value if isinstance(c, algopy.Bytes) else c
|
|
92
|
-
box_content = context.get_box(name_bytes)
|
|
93
|
-
if not box_content:
|
|
94
|
-
raise ValueError("Box does not exist")
|
|
95
|
-
if start + len(new_content) > len(box_content):
|
|
96
|
-
raise ValueError("Replacement content exceeds box size")
|
|
97
|
-
updated_content = (
|
|
98
|
-
box_content[:start] + new_content + box_content[start + len(new_content) :]
|
|
99
|
-
)
|
|
100
|
-
context.set_box(name_bytes, updated_content)
|
|
162
|
+
return context.clear_box(self.key)
|
|
101
163
|
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
def extract(
|
|
165
|
+
self, start_index: algopy.UInt64 | int, length: algopy.UInt64 | int
|
|
166
|
+
) -> algopy.Bytes:
|
|
167
|
+
"""
|
|
168
|
+
Extract a slice of bytes from the box.
|
|
169
|
+
|
|
170
|
+
Fails if the box does not exist, or if `start_index + length > len(box)`
|
|
171
|
+
|
|
172
|
+
:arg start_index: The offset to start extracting bytes from
|
|
173
|
+
:arg length: The number of bytes to extract
|
|
174
|
+
"""
|
|
104
175
|
import algopy
|
|
105
176
|
|
|
177
|
+
box_content, box_exists = self._maybe()
|
|
178
|
+
start_int = int(start_index)
|
|
179
|
+
length_int = int(length)
|
|
180
|
+
if not box_exists:
|
|
181
|
+
raise RuntimeError("Box has not been created")
|
|
182
|
+
if (start_int + length_int) > len(box_content):
|
|
183
|
+
raise ValueError("Index out of bounds")
|
|
184
|
+
result = box_content[start_int : start_int + length_int]
|
|
185
|
+
return algopy.Bytes(result)
|
|
186
|
+
|
|
187
|
+
def resize(self, new_size: algopy.UInt64 | int) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Resizes the box the specified `new_size`. Truncating existing data if the new value is
|
|
190
|
+
shorter or padding with zero bytes if it is longer.
|
|
191
|
+
|
|
192
|
+
:arg new_size: The new size of the box
|
|
193
|
+
"""
|
|
106
194
|
context = get_test_context()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if
|
|
110
|
-
raise ValueError("
|
|
111
|
-
box_content =
|
|
112
|
-
if not
|
|
113
|
-
raise
|
|
114
|
-
if
|
|
115
|
-
updated_content = box_content + b"\x00" * (
|
|
195
|
+
new_size_int = int(new_size)
|
|
196
|
+
|
|
197
|
+
if new_size_int > MAX_BOX_SIZE:
|
|
198
|
+
raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")
|
|
199
|
+
box_content, box_exists = self._maybe()
|
|
200
|
+
if not box_exists:
|
|
201
|
+
raise RuntimeError("Box has not been created")
|
|
202
|
+
if new_size_int > len(box_content):
|
|
203
|
+
updated_content = box_content + b"\x00" * (new_size_int - len(box_content))
|
|
116
204
|
else:
|
|
117
|
-
updated_content = box_content[:
|
|
118
|
-
context.set_box(
|
|
205
|
+
updated_content = box_content[:new_size_int]
|
|
206
|
+
context.set_box(self.key, updated_content)
|
|
207
|
+
|
|
208
|
+
def replace(self, start_index: algopy.UInt64 | int, value: algopy.Bytes | bytes) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Write `value` to the box starting at `start_index`. Fails if the box does not exist,
|
|
211
|
+
or if `start_index + len(value) > len(box)`
|
|
212
|
+
|
|
213
|
+
:arg start_index: The offset to start writing bytes from
|
|
214
|
+
:arg value: The bytes to be written
|
|
215
|
+
"""
|
|
216
|
+
context = get_test_context()
|
|
217
|
+
box_content, box_exists = self._maybe()
|
|
218
|
+
if not box_exists:
|
|
219
|
+
raise RuntimeError("Box has not been created")
|
|
220
|
+
start = int(start_index)
|
|
221
|
+
length = len(value)
|
|
222
|
+
if (start + length) > len(box_content):
|
|
223
|
+
raise ValueError("Replacement content exceeds box size")
|
|
224
|
+
updated_content = box_content[:start] + value + box_content[start + length :]
|
|
225
|
+
context.set_box(self.key, updated_content)
|
|
119
226
|
|
|
120
|
-
@staticmethod
|
|
121
227
|
def splice(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/,
|
|
228
|
+
self,
|
|
229
|
+
start_index: algopy.UInt64 | int,
|
|
230
|
+
length: algopy.UInt64 | int,
|
|
231
|
+
value: algopy.Bytes | bytes,
|
|
127
232
|
) -> None:
|
|
233
|
+
"""
|
|
234
|
+
set box to contain its previous bytes up to index `start_index`, followed by `bytes`,
|
|
235
|
+
followed by the original bytes of the box that began at index `start_index + length`
|
|
236
|
+
|
|
237
|
+
**Important: This op does not resize the box**
|
|
238
|
+
If the new value is longer than the box size, it will be truncated.
|
|
239
|
+
If the new value is shorter than the box size, it will be padded with zero bytes
|
|
240
|
+
|
|
241
|
+
:arg start_index: The index to start inserting `value`
|
|
242
|
+
:arg length: The number of bytes after `start_index` to omit from the new value
|
|
243
|
+
:arg value: The `value` to be inserted.
|
|
244
|
+
"""
|
|
128
245
|
import algopy
|
|
129
246
|
|
|
130
247
|
context = get_test_context()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
248
|
+
box_content, box_exists = self._maybe()
|
|
249
|
+
|
|
250
|
+
start = int(start_index)
|
|
251
|
+
delete_count = int(length)
|
|
252
|
+
insert_content = value.value if isinstance(value, algopy.Bytes) else value
|
|
136
253
|
|
|
137
|
-
if not
|
|
138
|
-
raise
|
|
254
|
+
if not box_exists:
|
|
255
|
+
raise RuntimeError("Box has not been created")
|
|
139
256
|
|
|
140
257
|
if start > len(box_content):
|
|
141
258
|
raise ValueError("Start index exceeds box size")
|
|
@@ -155,4 +272,249 @@ class Box:
|
|
|
155
272
|
new_content += b"\x00" * (len(box_content) - len(new_content))
|
|
156
273
|
|
|
157
274
|
# Update the box with the new content
|
|
158
|
-
context.set_box(
|
|
275
|
+
context.set_box(self.key, new_content)
|
|
276
|
+
|
|
277
|
+
def get(self, *, default: algopy.Bytes | bytes) -> algopy.Bytes:
|
|
278
|
+
"""
|
|
279
|
+
Retrieve the contents of the box, or return the default value if the box has not been
|
|
280
|
+
created.
|
|
281
|
+
|
|
282
|
+
:arg default: The default value to return if the box has not been created
|
|
283
|
+
"""
|
|
284
|
+
import algopy
|
|
285
|
+
|
|
286
|
+
box_content, box_exists = self._maybe()
|
|
287
|
+
default_bytes = default if isinstance(default, algopy.Bytes) else algopy.Bytes(default)
|
|
288
|
+
return default_bytes if not box_exists else algopy.Bytes(box_content)
|
|
289
|
+
|
|
290
|
+
def put(self, value: algopy.Bytes | bytes) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Replaces the contents of box with value. Fails if box exists and len(box) != len(value).
|
|
293
|
+
Creates box if it does not exist
|
|
294
|
+
|
|
295
|
+
:arg value: The value to write to the box
|
|
296
|
+
"""
|
|
297
|
+
import algopy
|
|
298
|
+
|
|
299
|
+
box_content, box_exists = self._maybe()
|
|
300
|
+
if box_exists and len(box_content) != len(value):
|
|
301
|
+
raise ValueError("Box already exists with a different size")
|
|
302
|
+
|
|
303
|
+
context = get_test_context()
|
|
304
|
+
content = value if isinstance(value, algopy.Bytes) else algopy.Bytes(value)
|
|
305
|
+
context.set_box(self.key, content)
|
|
306
|
+
|
|
307
|
+
def maybe(self) -> tuple[algopy.Bytes, bool]:
|
|
308
|
+
"""
|
|
309
|
+
Retrieve the contents of the box if it exists, and return a boolean indicating if the box
|
|
310
|
+
exists.
|
|
311
|
+
"""
|
|
312
|
+
import algopy
|
|
313
|
+
|
|
314
|
+
box_content, box_exists = self._maybe()
|
|
315
|
+
return algopy.Bytes(box_content), box_exists
|
|
316
|
+
|
|
317
|
+
def _maybe(self) -> tuple[bytes, bool]:
|
|
318
|
+
context = get_test_context()
|
|
319
|
+
box_exists = context.does_box_exist(self.key)
|
|
320
|
+
box_content = context.get_box(self.key)
|
|
321
|
+
return box_content, box_exists
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def length(self) -> algopy.UInt64:
|
|
325
|
+
"""
|
|
326
|
+
Get the length of this Box. Fails if the box does not exist
|
|
327
|
+
"""
|
|
328
|
+
import algopy
|
|
329
|
+
|
|
330
|
+
box_content, box_exists = self._maybe()
|
|
331
|
+
if not box_exists:
|
|
332
|
+
raise RuntimeError("Box has not been created")
|
|
333
|
+
return algopy.UInt64(len(box_content))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class BoxMap(typing.Generic[_TKey, _TValue]):
|
|
337
|
+
"""
|
|
338
|
+
BoxMap abstracts the reading and writing of a set of boxes using a common key and content type.
|
|
339
|
+
Each composite key (prefix + key) still needs to be made available to the application via the
|
|
340
|
+
`boxes` property of the Transaction.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
def __init__(
|
|
344
|
+
self,
|
|
345
|
+
key_type: type[_TKey],
|
|
346
|
+
value_type: type[_TValue],
|
|
347
|
+
/,
|
|
348
|
+
*,
|
|
349
|
+
key_prefix: bytes | str | algopy.Bytes | algopy.String = "",
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Declare a box map.
|
|
352
|
+
|
|
353
|
+
:arg key_type: The type of the keys
|
|
354
|
+
:arg value_type: The type of the values
|
|
355
|
+
:arg key_prefix: The value used as a prefix to key data, can be empty.
|
|
356
|
+
When the BoxMap is being assigned to a member variable,
|
|
357
|
+
this argument is optional and defaults to the member variable name,
|
|
358
|
+
and if a custom value is supplied it must be static.
|
|
359
|
+
"""
|
|
360
|
+
import algopy
|
|
361
|
+
|
|
362
|
+
self._key_type = key_type
|
|
363
|
+
self._value_type = value_type
|
|
364
|
+
self._key_prefix = (
|
|
365
|
+
algopy.String(as_string(key_prefix)).bytes
|
|
366
|
+
if isinstance(key_prefix, str | algopy.String)
|
|
367
|
+
else algopy.Bytes(as_bytes(key_prefix))
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def key_prefix(self) -> algopy.Bytes:
|
|
372
|
+
"""Provides access to the raw storage key-prefix"""
|
|
373
|
+
if not self._key_prefix:
|
|
374
|
+
raise RuntimeError("Box key prefix is empty")
|
|
375
|
+
return self._key_prefix
|
|
376
|
+
|
|
377
|
+
def __getitem__(self, key: _TKey) -> _TValue:
|
|
378
|
+
"""
|
|
379
|
+
Retrieve the contents of a keyed box. Fails if the box for the key has not been created.
|
|
380
|
+
"""
|
|
381
|
+
box_content, box_exists = self.maybe(key)
|
|
382
|
+
if not box_exists:
|
|
383
|
+
raise RuntimeError("Box has not been created")
|
|
384
|
+
return box_content
|
|
385
|
+
|
|
386
|
+
def __setitem__(self, key: _TKey, value: _TValue) -> None:
|
|
387
|
+
"""Write _value_ to a keyed box. Creates the box if it does not exist"""
|
|
388
|
+
context = get_test_context()
|
|
389
|
+
key_bytes = self._full_key(key)
|
|
390
|
+
bytes_value = _cast_to_bytes(value)
|
|
391
|
+
context.set_box(key_bytes, bytes_value)
|
|
392
|
+
|
|
393
|
+
def __delitem__(self, key: _TKey) -> None:
|
|
394
|
+
"""Deletes a keyed box"""
|
|
395
|
+
context = get_test_context()
|
|
396
|
+
key_bytes = self._full_key(key)
|
|
397
|
+
context.clear_box(key_bytes)
|
|
398
|
+
|
|
399
|
+
def __contains__(self, key: _TKey) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Returns True if a box with the specified key exists in the map, regardless of the
|
|
402
|
+
truthiness of the contents of the box
|
|
403
|
+
"""
|
|
404
|
+
context = get_test_context()
|
|
405
|
+
key_bytes = self._full_key(key)
|
|
406
|
+
return context.does_box_exist(key_bytes)
|
|
407
|
+
|
|
408
|
+
def get(self, key: _TKey, *, default: _TValue) -> _TValue:
|
|
409
|
+
"""
|
|
410
|
+
Retrieve the contents of a keyed box, or return the default value if the box has not been
|
|
411
|
+
created.
|
|
412
|
+
|
|
413
|
+
:arg key: The key of the box to get
|
|
414
|
+
:arg default: The default value to return if the box has not been created.
|
|
415
|
+
"""
|
|
416
|
+
box_content, box_exists = self.maybe(key)
|
|
417
|
+
return default if not box_exists else box_content
|
|
418
|
+
|
|
419
|
+
def maybe(self, key: _TKey) -> tuple[_TValue, bool]:
|
|
420
|
+
"""
|
|
421
|
+
Retrieve the contents of a keyed box if it exists, and return a boolean indicating if the
|
|
422
|
+
box exists.
|
|
423
|
+
|
|
424
|
+
:arg key: The key of the box to get
|
|
425
|
+
"""
|
|
426
|
+
context = get_test_context()
|
|
427
|
+
key_bytes = self._full_key(key)
|
|
428
|
+
box_exists = context.does_box_exist(key_bytes)
|
|
429
|
+
box_content_bytes = context.get_box(key_bytes)
|
|
430
|
+
box_content = _cast_to_value_type(self._value_type, box_content_bytes)
|
|
431
|
+
return box_content, box_exists
|
|
432
|
+
|
|
433
|
+
def length(self, key: _TKey) -> algopy.UInt64:
|
|
434
|
+
"""
|
|
435
|
+
Get the length of an item in this BoxMap. Fails if the box does not exist
|
|
436
|
+
|
|
437
|
+
:arg key: The key of the box to get
|
|
438
|
+
"""
|
|
439
|
+
import algopy
|
|
440
|
+
|
|
441
|
+
context = get_test_context()
|
|
442
|
+
key_bytes = self._full_key(key)
|
|
443
|
+
box_exists = context.does_box_exist(key_bytes)
|
|
444
|
+
if not box_exists:
|
|
445
|
+
raise RuntimeError("Box has not been created")
|
|
446
|
+
box_content_bytes = context.get_box(key_bytes)
|
|
447
|
+
return algopy.UInt64(len(box_content_bytes))
|
|
448
|
+
|
|
449
|
+
def _full_key(self, key: _TKey) -> algopy.Bytes:
|
|
450
|
+
return self.key_prefix + _cast_to_bytes(key)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _cast_to_value_type(t: type[_TValue], value: bytes) -> _TValue: # noqa: PLR0911
|
|
454
|
+
"""
|
|
455
|
+
assuming _TValue to be one of the followings:
|
|
456
|
+
- bool,
|
|
457
|
+
- algopy.Bytes,
|
|
458
|
+
- algopy.UInt64
|
|
459
|
+
- algopy.Asset,
|
|
460
|
+
- algopy.Application,
|
|
461
|
+
- algopy.UInt64 enums
|
|
462
|
+
- algopy.arc4.Struct
|
|
463
|
+
- algopy_testing.BytesBacked
|
|
464
|
+
- any type with `from_bytes` class method and `bytes` property
|
|
465
|
+
- .e.g algopy.String, algopy.Address, algopy.arc4.DynamicArray etc.
|
|
466
|
+
"""
|
|
467
|
+
import algopy
|
|
468
|
+
|
|
469
|
+
context = get_test_context()
|
|
470
|
+
|
|
471
|
+
if t is bool:
|
|
472
|
+
return algopy.op.btoi(value) == 1 # type: ignore[return-value]
|
|
473
|
+
elif t is algopy.Bytes:
|
|
474
|
+
return algopy.Bytes(value) # type: ignore[return-value]
|
|
475
|
+
elif t is algopy.UInt64:
|
|
476
|
+
return algopy.op.btoi(value) # type: ignore[return-value]
|
|
477
|
+
elif t is algopy.OnCompleteAction:
|
|
478
|
+
return algopy.OnCompleteAction(algopy.op.btoi(value).value) # type: ignore[return-value]
|
|
479
|
+
elif t is algopy.TransactionType:
|
|
480
|
+
return algopy.TransactionType(algopy.op.btoi(value).value) # type: ignore[return-value]
|
|
481
|
+
elif t is algopy.Asset:
|
|
482
|
+
asset_id = algopy.op.btoi(value)
|
|
483
|
+
return context.get_asset(asset_id) # type: ignore[return-value]
|
|
484
|
+
elif t is algopy.Application:
|
|
485
|
+
application_id = algopy.op.btoi(value)
|
|
486
|
+
return context.get_application(application_id) # type: ignore[return-value]
|
|
487
|
+
elif hasattr(t, "from_bytes"):
|
|
488
|
+
return t.from_bytes(value) # type: ignore[attr-defined, no-any-return]
|
|
489
|
+
|
|
490
|
+
raise ValueError(f"Unsupported type: {t}")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _cast_to_bytes(value: _TValue) -> algopy.Bytes:
|
|
494
|
+
"""
|
|
495
|
+
assuming _TValue to be one of the followings:
|
|
496
|
+
- bool,
|
|
497
|
+
- algopy.Bytes,
|
|
498
|
+
- algopy.UInt64
|
|
499
|
+
- algopy.Asset,
|
|
500
|
+
- algopy.Application,
|
|
501
|
+
- algopy.UInt64 enums
|
|
502
|
+
- algopy.arc4.Struct
|
|
503
|
+
- algopy_testing.BytesBacked
|
|
504
|
+
- any type with `from_bytes` class method and `bytes` property
|
|
505
|
+
- .e.g algopy.String, algopy.Address, algopy.arc4.DynamicArray etc.
|
|
506
|
+
"""
|
|
507
|
+
import algopy
|
|
508
|
+
|
|
509
|
+
if isinstance(value, bool):
|
|
510
|
+
return algopy.op.itob(1 if value else 0)
|
|
511
|
+
elif isinstance(value, algopy.Bytes):
|
|
512
|
+
return value
|
|
513
|
+
elif isinstance(value, algopy.UInt64):
|
|
514
|
+
return algopy.op.itob(value)
|
|
515
|
+
elif isinstance(value, algopy.Asset | algopy.Application):
|
|
516
|
+
return algopy.op.itob(value.id)
|
|
517
|
+
elif hasattr(value, "bytes"):
|
|
518
|
+
return typing.cast(algopy.Bytes, value.bytes)
|
|
519
|
+
|
|
520
|
+
raise ValueError(f"Unsupported type: {type(value)}")
|
|
@@ -69,6 +69,18 @@ class Contract(metaclass=_ContractMeta):
|
|
|
69
69
|
return wrapper
|
|
70
70
|
return attr
|
|
71
71
|
|
|
72
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
73
|
+
import algopy
|
|
74
|
+
|
|
75
|
+
name_bytes = algopy.String(name).bytes
|
|
76
|
+
match value:
|
|
77
|
+
case algopy.Box() | algopy.BoxRef() | algopy.GlobalState() | algopy.LocalState():
|
|
78
|
+
value._key = name_bytes
|
|
79
|
+
case algopy.BoxMap():
|
|
80
|
+
value._key_prefix = name_bytes
|
|
81
|
+
|
|
82
|
+
super().__setattr__(name, value)
|
|
83
|
+
|
|
72
84
|
|
|
73
85
|
class ARC4Contract(Contract):
|
|
74
86
|
@final
|
algopy_testing/op.py
CHANGED
|
@@ -22,12 +22,13 @@ from ecdsa import ( # type: ignore # noqa: PGH003
|
|
|
22
22
|
from algopy_testing.constants import (
|
|
23
23
|
BITS_IN_BYTE,
|
|
24
24
|
DEFAULT_ACCOUNT_MIN_BALANCE,
|
|
25
|
+
MAX_BOX_SIZE,
|
|
25
26
|
MAX_BYTES_SIZE,
|
|
26
27
|
MAX_UINT64,
|
|
27
28
|
)
|
|
29
|
+
from algopy_testing.context import get_test_context
|
|
28
30
|
from algopy_testing.enums import EC, ECDSA, Base64, VrfVerify
|
|
29
31
|
from algopy_testing.models.block import Block
|
|
30
|
-
from algopy_testing.models.box import Box
|
|
31
32
|
from algopy_testing.models.gitxn import GITxn
|
|
32
33
|
from algopy_testing.models.global_values import Global
|
|
33
34
|
from algopy_testing.models.gtxn import GTxn
|
|
@@ -1023,6 +1024,160 @@ class _EllipticCurve:
|
|
|
1023
1024
|
|
|
1024
1025
|
EllipticCurve = _EllipticCurve()
|
|
1025
1026
|
|
|
1027
|
+
|
|
1028
|
+
class Box:
|
|
1029
|
+
@staticmethod
|
|
1030
|
+
def create(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> bool:
|
|
1031
|
+
import algopy
|
|
1032
|
+
|
|
1033
|
+
context = get_test_context()
|
|
1034
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1035
|
+
size = int(b)
|
|
1036
|
+
if not name_bytes or size > MAX_BOX_SIZE:
|
|
1037
|
+
raise ValueError("Invalid box name or size")
|
|
1038
|
+
if context.get_box(name_bytes):
|
|
1039
|
+
return False
|
|
1040
|
+
context.set_box(name_bytes, b"\x00" * size)
|
|
1041
|
+
return True
|
|
1042
|
+
|
|
1043
|
+
@staticmethod
|
|
1044
|
+
def delete(a: algopy.Bytes | bytes, /) -> bool:
|
|
1045
|
+
import algopy
|
|
1046
|
+
|
|
1047
|
+
context = get_test_context()
|
|
1048
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1049
|
+
if context.get_box(name_bytes):
|
|
1050
|
+
context.clear_box(name_bytes)
|
|
1051
|
+
return True
|
|
1052
|
+
return False
|
|
1053
|
+
|
|
1054
|
+
@staticmethod
|
|
1055
|
+
def extract(
|
|
1056
|
+
a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.UInt64 | int, /
|
|
1057
|
+
) -> algopy.Bytes:
|
|
1058
|
+
import algopy
|
|
1059
|
+
|
|
1060
|
+
context = get_test_context()
|
|
1061
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1062
|
+
start = int(b)
|
|
1063
|
+
length = int(c)
|
|
1064
|
+
box_content = context.get_box(name_bytes)
|
|
1065
|
+
if not box_content:
|
|
1066
|
+
raise RuntimeError("Box does not exist")
|
|
1067
|
+
result = box_content[start : start + length]
|
|
1068
|
+
return algopy.Bytes(result)
|
|
1069
|
+
|
|
1070
|
+
@staticmethod
|
|
1071
|
+
def get(a: algopy.Bytes | bytes, /) -> tuple[algopy.Bytes, bool]:
|
|
1072
|
+
import algopy
|
|
1073
|
+
|
|
1074
|
+
context = get_test_context()
|
|
1075
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1076
|
+
box_content = algopy.Bytes(context.get_box(name_bytes))
|
|
1077
|
+
box_exists = context.does_box_exist(name_bytes)
|
|
1078
|
+
return box_content, box_exists
|
|
1079
|
+
|
|
1080
|
+
@staticmethod
|
|
1081
|
+
def length(a: algopy.Bytes | bytes, /) -> tuple[algopy.UInt64, bool]:
|
|
1082
|
+
import algopy
|
|
1083
|
+
|
|
1084
|
+
context = get_test_context()
|
|
1085
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1086
|
+
box_content = context.get_box(name_bytes)
|
|
1087
|
+
box_exists = context.does_box_exist(name_bytes)
|
|
1088
|
+
return algopy.UInt64(len(box_content)), box_exists
|
|
1089
|
+
|
|
1090
|
+
@staticmethod
|
|
1091
|
+
def put(a: algopy.Bytes | bytes, b: algopy.Bytes | bytes, /) -> None:
|
|
1092
|
+
import algopy
|
|
1093
|
+
|
|
1094
|
+
context = get_test_context()
|
|
1095
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1096
|
+
content = b.value if isinstance(b, algopy.Bytes) else b
|
|
1097
|
+
existing_content = context.get_box(name_bytes)
|
|
1098
|
+
if existing_content and len(existing_content) != len(content):
|
|
1099
|
+
raise ValueError("New content length does not match existing box length")
|
|
1100
|
+
context.set_box(name_bytes, algopy.Bytes(content))
|
|
1101
|
+
|
|
1102
|
+
@staticmethod
|
|
1103
|
+
def replace(
|
|
1104
|
+
a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.Bytes | bytes, /
|
|
1105
|
+
) -> None:
|
|
1106
|
+
import algopy
|
|
1107
|
+
|
|
1108
|
+
context = get_test_context()
|
|
1109
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1110
|
+
start = int(b)
|
|
1111
|
+
new_content = c.value if isinstance(c, algopy.Bytes) else c
|
|
1112
|
+
box_content = context.get_box(name_bytes)
|
|
1113
|
+
if not box_content:
|
|
1114
|
+
raise RuntimeError("Box does not exist")
|
|
1115
|
+
if start + len(new_content) > len(box_content):
|
|
1116
|
+
raise ValueError("Replacement content exceeds box size")
|
|
1117
|
+
updated_content = (
|
|
1118
|
+
box_content[:start] + new_content + box_content[start + len(new_content) :]
|
|
1119
|
+
)
|
|
1120
|
+
context.set_box(name_bytes, updated_content)
|
|
1121
|
+
|
|
1122
|
+
@staticmethod
|
|
1123
|
+
def resize(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> None:
|
|
1124
|
+
import algopy
|
|
1125
|
+
|
|
1126
|
+
context = get_test_context()
|
|
1127
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1128
|
+
new_size = int(b)
|
|
1129
|
+
if not name_bytes or new_size > MAX_BOX_SIZE:
|
|
1130
|
+
raise ValueError("Invalid box name or size")
|
|
1131
|
+
box_content = context.get_box(name_bytes)
|
|
1132
|
+
if not box_content:
|
|
1133
|
+
raise RuntimeError("Box does not exist")
|
|
1134
|
+
if new_size > len(box_content):
|
|
1135
|
+
updated_content = box_content + b"\x00" * (new_size - len(box_content))
|
|
1136
|
+
else:
|
|
1137
|
+
updated_content = box_content[:new_size]
|
|
1138
|
+
context.set_box(name_bytes, updated_content)
|
|
1139
|
+
|
|
1140
|
+
@staticmethod
|
|
1141
|
+
def splice(
|
|
1142
|
+
a: algopy.Bytes | bytes,
|
|
1143
|
+
b: algopy.UInt64 | int,
|
|
1144
|
+
c: algopy.UInt64 | int,
|
|
1145
|
+
d: algopy.Bytes | bytes,
|
|
1146
|
+
/,
|
|
1147
|
+
) -> None:
|
|
1148
|
+
import algopy
|
|
1149
|
+
|
|
1150
|
+
context = get_test_context()
|
|
1151
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
1152
|
+
start = int(b)
|
|
1153
|
+
delete_count = int(c)
|
|
1154
|
+
insert_content = d.value if isinstance(d, algopy.Bytes) else d
|
|
1155
|
+
box_content = context.get_box(name_bytes)
|
|
1156
|
+
|
|
1157
|
+
if not box_content:
|
|
1158
|
+
raise RuntimeError("Box does not exist")
|
|
1159
|
+
|
|
1160
|
+
if start > len(box_content):
|
|
1161
|
+
raise ValueError("Start index exceeds box size")
|
|
1162
|
+
|
|
1163
|
+
# Calculate the end index for deletion
|
|
1164
|
+
end = min(start + delete_count, len(box_content))
|
|
1165
|
+
|
|
1166
|
+
# Construct the new content
|
|
1167
|
+
new_content = box_content[:start] + insert_content + box_content[end:]
|
|
1168
|
+
|
|
1169
|
+
# Adjust the size if necessary
|
|
1170
|
+
if len(new_content) > len(box_content):
|
|
1171
|
+
# Truncate if the new content is too long
|
|
1172
|
+
new_content = new_content[: len(box_content)]
|
|
1173
|
+
elif len(new_content) < len(box_content):
|
|
1174
|
+
# Pad with zeros if the new content is too short
|
|
1175
|
+
new_content += b"\x00" * (len(box_content) - len(new_content))
|
|
1176
|
+
|
|
1177
|
+
# Update the box with the new content
|
|
1178
|
+
context.set_box(name_bytes, new_content)
|
|
1179
|
+
|
|
1180
|
+
|
|
1026
1181
|
__all__ = [
|
|
1027
1182
|
"AcctParamsGet",
|
|
1028
1183
|
"AppGlobal",
|
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import typing
|
|
4
4
|
from typing import cast, overload
|
|
5
5
|
|
|
6
|
+
if typing.TYPE_CHECKING:
|
|
7
|
+
import algopy
|
|
8
|
+
|
|
6
9
|
_T = typing.TypeVar("_T")
|
|
7
10
|
|
|
8
11
|
|
|
@@ -35,6 +38,8 @@ class GlobalState(typing.Generic[_T]):
|
|
|
35
38
|
key: bytes | str = "",
|
|
36
39
|
description: str = "",
|
|
37
40
|
) -> None:
|
|
41
|
+
import algopy
|
|
42
|
+
|
|
38
43
|
if isinstance(type_or_value, type):
|
|
39
44
|
self.type_ = type_or_value
|
|
40
45
|
self._value: _T | None = None
|
|
@@ -42,9 +47,21 @@ class GlobalState(typing.Generic[_T]):
|
|
|
42
47
|
self.type_ = type(type_or_value)
|
|
43
48
|
self._value = type_or_value
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
match key:
|
|
51
|
+
case bytes(key):
|
|
52
|
+
self._key = algopy.Bytes(key)
|
|
53
|
+
case str(key):
|
|
54
|
+
self._key = algopy.String(key).bytes
|
|
55
|
+
case _:
|
|
56
|
+
raise ValueError("Key must be bytes or str")
|
|
57
|
+
|
|
46
58
|
self.description = description
|
|
47
59
|
|
|
60
|
+
@property
|
|
61
|
+
def key(self) -> algopy.Bytes:
|
|
62
|
+
"""Provides access to the raw storage key"""
|
|
63
|
+
return self._key
|
|
64
|
+
|
|
48
65
|
@property
|
|
49
66
|
def value(self) -> _T:
|
|
50
67
|
if self._value is None:
|
|
@@ -18,11 +18,24 @@ class LocalState(typing.Generic[_T]):
|
|
|
18
18
|
key: bytes | str = "",
|
|
19
19
|
description: str = "",
|
|
20
20
|
) -> None:
|
|
21
|
+
import algopy
|
|
22
|
+
|
|
21
23
|
self.type_ = type_
|
|
22
|
-
|
|
24
|
+
match key:
|
|
25
|
+
case bytes(key):
|
|
26
|
+
self._key = algopy.Bytes(key)
|
|
27
|
+
case str(key):
|
|
28
|
+
self._key = algopy.String(key).bytes
|
|
29
|
+
case _:
|
|
30
|
+
raise ValueError("Key must be bytes or str")
|
|
23
31
|
self.description = description
|
|
24
32
|
self._state: dict[object, _T] = {}
|
|
25
33
|
|
|
34
|
+
@property
|
|
35
|
+
def key(self) -> algopy.Bytes:
|
|
36
|
+
"""Provides access to the raw storage key"""
|
|
37
|
+
return self._key
|
|
38
|
+
|
|
26
39
|
def _validate_local_state_key(self, key: algopy.Account | algopy.UInt64 | int) -> None:
|
|
27
40
|
from algopy import Account, UInt64
|
|
28
41
|
|
{algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: algorand-python-testing
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2b2
|
|
4
4
|
Summary: Algorand Python testing library
|
|
5
5
|
Project-URL: Documentation, https://github.com/algorandfoundation/puya/tree/main/algopy_testing#README.md
|
|
6
6
|
Project-URL: Issues, https://github.com/algorandfoundation/puya/issues
|
|
@@ -14,14 +14,25 @@ Classifier: Programming Language :: Python
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Topic :: Software Development :: Testing
|
|
16
16
|
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: algorand-python>=1.2
|
|
17
18
|
Requires-Dist: coincurve>=19.0.1
|
|
18
19
|
Requires-Dist: ecdsa>=0.17.0
|
|
19
20
|
Requires-Dist: pycryptodomex<4,>=3.6.0
|
|
20
21
|
Requires-Dist: pynacl<2,>=1.4.0
|
|
21
|
-
Requires-Dist: algorand-python>=1,<1.3.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
<div align="center">
|
|
25
|
+
<a href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://bafybeiaibjaf6zy6hvef2rrysaacsfsyb3hw4qqtgn657gw7k5tdzqdxzi.ipfs.nftstorage.link/" width=60%></a>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a target="_blank" href="https://algorandfoundation.github.io/algorand-python-testing/"><img src="https://img.shields.io/badge/docs-repository-74dfdc?logo=github&style=flat.svg" /></a>
|
|
30
|
+
<a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://img.shields.io/badge/learn-AlgoKit-74dfdc?logo=algorand&mac=flat.svg" /></a>
|
|
31
|
+
<a target="_blank" href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://img.shields.io/github/stars/algorandfoundation/algorand-python-testing?color=74dfdc&logo=star&style=flat" /></a>
|
|
32
|
+
<a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2Falgorandfoundation%2Falgorand-python-testing&countColor=%2374dfdc&style=flat" /></a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
---
|
|
25
36
|
|
|
26
37
|
Algorand Python Testing is a companion package to [Algorand Python](https://github.com/algorandfoundation/puya) that enables efficient unit testing of Algorand Python smart contracts in an offline environment. It emulates key AVM behaviors without requiring a network connection, offering fast and reliable testing capabilities with a familiar Pythonic interface.
|
|
27
38
|
|
{algorand_python_testing-0.2.1.dist-info → algorand_python_testing-0.2.2b2.dist-info}/RECORD
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
algopy/__init__.py,sha256=
|
|
1
|
+
algopy/__init__.py,sha256=aAz8sf_DjKyyEAhpMQNGs-l4NCVROrZgKblsqWIqs7A,1273
|
|
2
2
|
algopy/arc4.py,sha256=zE-cwwmeEBA0m22QiHIujeY0S5rdyf3gDqTyGgROAqI,48
|
|
3
3
|
algopy/gtxn.py,sha256=tS2waKIr3g79T1xcmE2iVcvu524xANsR8awXduUVoT0,48
|
|
4
4
|
algopy/itxn.py,sha256=gXida1ee8xMfRufZ24G4xPGGSe26Iwm9nqfHpQCxbpE,48
|
|
@@ -7,11 +7,11 @@ algopy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
7
7
|
algopy_testing/__init__.py,sha256=x7WpDet7utqy6M9MdE5OJeKLqNd1uAIPg_U2ZpDOenI,1150
|
|
8
8
|
algopy_testing/arc4.py,sha256=3rCZGKs4-mH3SymHOrp190edGS9MV_v3L5oP_5_GBsA,52015
|
|
9
9
|
algopy_testing/constants.py,sha256=L1-Vod3_k6NAmeHJ9d8qIIovSMlakudvd4VeR6BjPeo,733
|
|
10
|
-
algopy_testing/context.py,sha256=
|
|
11
|
-
algopy_testing/enums.py,sha256=
|
|
10
|
+
algopy_testing/context.py,sha256=BFcMfIZvpO4n2LXr1n3chc5vN30qoPfPuBaYNQDFjUw,39570
|
|
11
|
+
algopy_testing/enums.py,sha256=6ed2GvC-yRw6cYS06uwkgbFbfQ1C1H2qDqkWXgwul5g,1531
|
|
12
12
|
algopy_testing/gtxn.py,sha256=ul1iBYt8Kwc9Xob_pWVwa6dj9ueQ0iIz2DHkC72Aga4,7954
|
|
13
13
|
algopy_testing/itxn.py,sha256=lLz6dPBwTSvU5zmuKPQd0LkCLG1VJN2Q8Dmb6HRNb3s,23363
|
|
14
|
-
algopy_testing/op.py,sha256=
|
|
14
|
+
algopy_testing/op.py,sha256=2Z9GETVphRAe4kRWRbS1L77yEhKTmN55dqfXVoqMEfU,38650
|
|
15
15
|
algopy_testing/protocols.py,sha256=fPvuYAWKVVYM8oZE9hJtmRKW24Ba8VSzrBGz-7mDnIw,448
|
|
16
16
|
algopy_testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
algopy_testing/utils.py,sha256=PkDkHPAKJxyq6Jc0kWSVhF9MtIyNZmNv-hY2bPtwQ40,8393
|
|
@@ -24,8 +24,8 @@ algopy_testing/models/account.py,sha256=vR_furAVjNGkT1x8vJ8BQeOuCyjg3Cwc1pIGUkwX
|
|
|
24
24
|
algopy_testing/models/application.py,sha256=leMvgKkYt7Kct4ayUwkL7rQy65mpqp52oGnuoNO-1xA,2203
|
|
25
25
|
algopy_testing/models/asset.py,sha256=qsvC7vGDKTH_PHqybc0SAgzPLAjPgzgwK0aDliUxoLk,3471
|
|
26
26
|
algopy_testing/models/block.py,sha256=JwzoQ3Z7EIvOaSjbMlzQTQ7DztYejw43rQeRptolADs,876
|
|
27
|
-
algopy_testing/models/box.py,sha256=
|
|
28
|
-
algopy_testing/models/contract.py,sha256=
|
|
27
|
+
algopy_testing/models/box.py,sha256=cJrKHtwBs0EVvnfAHEZylmbywrg-Vx_-uVaQAsfIxP0,18820
|
|
28
|
+
algopy_testing/models/contract.py,sha256=jNFWwa_wxhpTJwP4LKQDLEA7wQWxlK8xfVu-uxFccQE,2714
|
|
29
29
|
algopy_testing/models/gitxn.py,sha256=MEWkVoK81Hoqz6Bb7uFAuGCZTb_G5lH9Vl1Dx_2nO7w,1594
|
|
30
30
|
algopy_testing/models/global_values.py,sha256=cT5JKbcSY2_VenF6Pn3BkSj_iaV2D5o4uJZYyKXs2xQ,2215
|
|
31
31
|
algopy_testing/models/gtxn.py,sha256=RJCwFyJvIbxwbF27RpWJaS04x7ZACKr1IPP3iXRS6Rk,1993
|
|
@@ -41,12 +41,12 @@ algopy_testing/primitives/bytes.py,sha256=TI-M5hA4NNTdiNwZg_70EH08VFHOW0f9Wm7RwV
|
|
|
41
41
|
algopy_testing/primitives/string.py,sha256=7eWrbFxu74nQ7ZdDg6GVsZ73VF_p24os7SnIoXKZuFk,2189
|
|
42
42
|
algopy_testing/primitives/uint64.py,sha256=-vQHLYza-K7Y_Pms-49pQQ44B78CNLEYBb6X6Ny-G6c,7110
|
|
43
43
|
algopy_testing/state/__init__.py,sha256=uRFuGSn7RMnhjVKWWdHnVtPNhNfTlQIoQaDPbcmkr3U,155
|
|
44
|
-
algopy_testing/state/global_state.py,sha256=
|
|
45
|
-
algopy_testing/state/local_state.py,sha256=
|
|
46
|
-
algopy_testing/utilities/__init__.py,sha256=
|
|
44
|
+
algopy_testing/state/global_state.py,sha256=Kp_Utr2YLQmMYL_Y-94gp5EIhxephbqKUWDaZEGnGBA,2090
|
|
45
|
+
algopy_testing/state/local_state.py,sha256=8dtmyIQXKyonLRHxNykAFqmcodTn30QEGt1PR-eJjVM,2171
|
|
46
|
+
algopy_testing/utilities/__init__.py,sha256=juZqPBATNsIUYDvZJf2KSVwt21anHloc4bgI5KWfLFg,171
|
|
47
47
|
algopy_testing/utilities/budget.py,sha256=P1pOKWCyya4wsqAwKgvp7y0p97hn2Kr2qFVRKm-sJZo,540
|
|
48
48
|
algopy_testing/utilities/log.py,sha256=5MDJY8Oh_COt5OLZXWqkvAJk2qdfBpfSGIpidV-HJ4k,1870
|
|
49
|
-
algorand_python_testing-0.2.
|
|
50
|
-
algorand_python_testing-0.2.
|
|
51
|
-
algorand_python_testing-0.2.
|
|
52
|
-
algorand_python_testing-0.2.
|
|
49
|
+
algorand_python_testing-0.2.2b2.dist-info/METADATA,sha256=YUTScpM6LeXfKQfMhFCiExhIp0WNJATWO653RTQMQMY,4172
|
|
50
|
+
algorand_python_testing-0.2.2b2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
51
|
+
algorand_python_testing-0.2.2b2.dist-info/licenses/LICENSE,sha256=jRyAzzkz3HPr-knS8XWyDY8CyuU-L4RtydPP8uGWsUw,657
|
|
52
|
+
algorand_python_testing-0.2.2b2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|