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
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
# Define the union type
|
|
10
|
+
from typing import TYPE_CHECKING, Any, TypeVar, Unpack, cast, overload
|
|
11
|
+
|
|
12
|
+
import algosdk
|
|
13
|
+
|
|
14
|
+
from algopy_testing.constants import (
|
|
15
|
+
ARC4_RETURN_PREFIX,
|
|
16
|
+
DEFAULT_ACCOUNT_MIN_BALANCE,
|
|
17
|
+
DEFAULT_ASSET_CREATE_MIN_BALANCE,
|
|
18
|
+
DEFAULT_ASSET_OPT_IN_MIN_BALANCE,
|
|
19
|
+
DEFAULT_GLOBAL_GENESIS_HASH,
|
|
20
|
+
DEFAULT_MAX_TXN_LIFE,
|
|
21
|
+
MAX_BYTES_SIZE,
|
|
22
|
+
MAX_UINT64,
|
|
23
|
+
)
|
|
24
|
+
from algopy_testing.gtxn import NULL_GTXN_GROUP_INDEX
|
|
25
|
+
from algopy_testing.models.account import AccountFields
|
|
26
|
+
from algopy_testing.models.application import ApplicationFields
|
|
27
|
+
from algopy_testing.models.asset import AssetFields
|
|
28
|
+
from algopy_testing.models.global_values import GlobalFields
|
|
29
|
+
from algopy_testing.models.txn import TxnFields
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Callable, Generator, Sequence
|
|
33
|
+
|
|
34
|
+
import algopy
|
|
35
|
+
|
|
36
|
+
from algopy_testing.gtxn import (
|
|
37
|
+
AssetTransferFields,
|
|
38
|
+
PaymentFields,
|
|
39
|
+
TransactionFields,
|
|
40
|
+
)
|
|
41
|
+
from algopy_testing.models.transactions import (
|
|
42
|
+
AssetConfigFields,
|
|
43
|
+
AssetFreezeFields,
|
|
44
|
+
KeyRegistrationFields,
|
|
45
|
+
_ApplicationCallFields,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
InnerTransactionResultType = (
|
|
49
|
+
algopy.itxn.InnerTransactionResult
|
|
50
|
+
| algopy.itxn.PaymentInnerTransaction
|
|
51
|
+
| algopy.itxn.KeyRegistrationInnerTransaction
|
|
52
|
+
| algopy.itxn.AssetConfigInnerTransaction
|
|
53
|
+
| algopy.itxn.AssetTransferInnerTransaction
|
|
54
|
+
| algopy.itxn.AssetFreezeInnerTransaction
|
|
55
|
+
| algopy.itxn.ApplicationCallInnerTransaction
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
T = TypeVar("T")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class AccountContextData:
|
|
64
|
+
"""
|
|
65
|
+
Stores account-related information.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
opted_asset_balances (dict[int, algopy.UInt64]): Mapping of asset IDs to balances.
|
|
69
|
+
opted_apps (dict[int, algopy.UInt64]): Mapping of application IDs to instances.
|
|
70
|
+
fields (AccountFields): Additional account fields.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
opted_asset_balances: dict[algopy.UInt64, algopy.UInt64]
|
|
74
|
+
opted_apps: dict[algopy.UInt64, algopy.Application]
|
|
75
|
+
fields: AccountFields
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class ContractContextData:
|
|
80
|
+
"""
|
|
81
|
+
Stores contract-related information.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
contract (algopy.Contract | algopy.ARC4Contract): Contract instance.
|
|
85
|
+
app_id (algopy.UInt64): Application ID.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
contract: algopy.Contract | algopy.ARC4Contract
|
|
89
|
+
app_id: algopy.UInt64
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ITxnLoader:
|
|
93
|
+
"""
|
|
94
|
+
Stores inner transaction references.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, inner_txn: InnerTransactionResultType):
|
|
98
|
+
self._inner_txn = inner_txn
|
|
99
|
+
|
|
100
|
+
def _get_itxn(self, txn_type: type[T]) -> T:
|
|
101
|
+
txn = self._inner_txn
|
|
102
|
+
|
|
103
|
+
if not isinstance(txn, txn_type):
|
|
104
|
+
raise TypeError(f"Last transaction is not of type {txn_type.__name__}!")
|
|
105
|
+
|
|
106
|
+
return txn
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def payment(self) -> algopy.itxn.PaymentInnerTransaction:
|
|
110
|
+
"""
|
|
111
|
+
Retrieve the last PaymentInnerTransaction.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
115
|
+
"""
|
|
116
|
+
import algopy
|
|
117
|
+
|
|
118
|
+
return self._get_itxn(algopy.itxn.PaymentInnerTransaction)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def asset_config(self) -> algopy.itxn.AssetConfigInnerTransaction:
|
|
122
|
+
"""
|
|
123
|
+
Retrieve the last AssetConfigInnerTransaction.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
127
|
+
"""
|
|
128
|
+
import algopy
|
|
129
|
+
|
|
130
|
+
return self._get_itxn(algopy.itxn.AssetConfigInnerTransaction)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def asset_transfer(self) -> algopy.itxn.AssetTransferInnerTransaction:
|
|
134
|
+
"""
|
|
135
|
+
Retrieve the last AssetTransferInnerTransaction.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
139
|
+
"""
|
|
140
|
+
import algopy
|
|
141
|
+
|
|
142
|
+
return self._get_itxn(algopy.itxn.AssetTransferInnerTransaction)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def asset_freeze(self) -> algopy.itxn.AssetFreezeInnerTransaction:
|
|
146
|
+
"""
|
|
147
|
+
Retrieve the last AssetFreezeInnerTransaction.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
151
|
+
"""
|
|
152
|
+
import algopy
|
|
153
|
+
|
|
154
|
+
return self._get_itxn(algopy.itxn.AssetFreezeInnerTransaction)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def application_call(self) -> algopy.itxn.ApplicationCallInnerTransaction:
|
|
158
|
+
"""
|
|
159
|
+
Retrieve the last ApplicationCallInnerTransaction.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
163
|
+
"""
|
|
164
|
+
import algopy
|
|
165
|
+
|
|
166
|
+
return self._get_itxn(algopy.itxn.ApplicationCallInnerTransaction)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def key_registration(self) -> algopy.itxn.KeyRegistrationInnerTransaction:
|
|
170
|
+
"""
|
|
171
|
+
Retrieve the last KeyRegistrationInnerTransaction.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
175
|
+
"""
|
|
176
|
+
import algopy
|
|
177
|
+
|
|
178
|
+
return self._get_itxn(algopy.itxn.KeyRegistrationInnerTransaction)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def transaction(self) -> algopy.itxn.InnerTransactionResult:
|
|
182
|
+
"""
|
|
183
|
+
Retrieve the last InnerTransactionResult.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: If the transaction is not found or not of the expected type.
|
|
187
|
+
"""
|
|
188
|
+
import algopy
|
|
189
|
+
|
|
190
|
+
return self._get_itxn(algopy.itxn.InnerTransactionResult)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ITxnGroupLoader:
|
|
194
|
+
@overload
|
|
195
|
+
def __getitem__(self, index: int) -> ITxnLoader: ...
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def __getitem__(self, index: slice) -> list[ITxnLoader]: ...
|
|
199
|
+
|
|
200
|
+
def __getitem__(self, index: int | slice) -> ITxnLoader | list[ITxnLoader]:
|
|
201
|
+
if isinstance(index, int):
|
|
202
|
+
return ITxnLoader(self._inner_txn_group[index])
|
|
203
|
+
elif isinstance(index, slice):
|
|
204
|
+
return [ITxnLoader(self._inner_txn_group[i]) for i in range(*index.indices(len(self)))]
|
|
205
|
+
else:
|
|
206
|
+
raise TypeError("Index must be int or slice")
|
|
207
|
+
|
|
208
|
+
def __len__(self) -> int:
|
|
209
|
+
return len(self._inner_txn_group)
|
|
210
|
+
|
|
211
|
+
def __init__(self, inner_txn_group: Sequence[InnerTransactionResultType]):
|
|
212
|
+
self._inner_txn_group = inner_txn_group
|
|
213
|
+
|
|
214
|
+
def _get_itxn(self, index: int, txn_type: type[T]) -> T:
|
|
215
|
+
try:
|
|
216
|
+
txn = self._inner_txn_group[index]
|
|
217
|
+
except IndexError as err:
|
|
218
|
+
raise ValueError(f"No inner transaction available at index {index}!") from err
|
|
219
|
+
|
|
220
|
+
if not isinstance(txn, txn_type):
|
|
221
|
+
raise TypeError(f"Last transaction is not of type {txn_type.__name__}!")
|
|
222
|
+
|
|
223
|
+
return txn
|
|
224
|
+
|
|
225
|
+
def payment(self, index: int) -> algopy.itxn.PaymentInnerTransaction:
|
|
226
|
+
import algopy
|
|
227
|
+
|
|
228
|
+
return ITxnLoader(self._get_itxn(index, algopy.itxn.PaymentInnerTransaction)).payment
|
|
229
|
+
|
|
230
|
+
def asset_config(self, index: int) -> algopy.itxn.AssetConfigInnerTransaction:
|
|
231
|
+
import algopy
|
|
232
|
+
|
|
233
|
+
return self._get_itxn(index, algopy.itxn.AssetConfigInnerTransaction)
|
|
234
|
+
|
|
235
|
+
def asset_transfer(self, index: int) -> algopy.itxn.AssetTransferInnerTransaction:
|
|
236
|
+
import algopy
|
|
237
|
+
|
|
238
|
+
return self._get_itxn(index, algopy.itxn.AssetTransferInnerTransaction)
|
|
239
|
+
|
|
240
|
+
def asset_freeze(self, index: int) -> algopy.itxn.AssetFreezeInnerTransaction:
|
|
241
|
+
import algopy
|
|
242
|
+
|
|
243
|
+
return self._get_itxn(index, algopy.itxn.AssetFreezeInnerTransaction)
|
|
244
|
+
|
|
245
|
+
def application_call(self, index: int) -> algopy.itxn.ApplicationCallInnerTransaction:
|
|
246
|
+
import algopy
|
|
247
|
+
|
|
248
|
+
return self._get_itxn(index, algopy.itxn.ApplicationCallInnerTransaction)
|
|
249
|
+
|
|
250
|
+
def key_registration(self, index: int) -> algopy.itxn.KeyRegistrationInnerTransaction:
|
|
251
|
+
import algopy
|
|
252
|
+
|
|
253
|
+
return self._get_itxn(index, algopy.itxn.KeyRegistrationInnerTransaction)
|
|
254
|
+
|
|
255
|
+
def transaction(self, index: int) -> algopy.itxn.InnerTransactionResult:
|
|
256
|
+
import algopy
|
|
257
|
+
|
|
258
|
+
return self._get_itxn(index, algopy.itxn.InnerTransactionResult)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class AlgopyTestContext:
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
default_creator: algopy.Account | None = None,
|
|
267
|
+
default_application: algopy.Application | None = None,
|
|
268
|
+
template_vars: dict[str, Any] | None = None,
|
|
269
|
+
) -> None:
|
|
270
|
+
import algopy
|
|
271
|
+
|
|
272
|
+
self._asset_id = iter(range(1, 2**64))
|
|
273
|
+
self._app_id = iter(range(1, 2**64))
|
|
274
|
+
|
|
275
|
+
self._contracts: list[algopy.Contract | algopy.ARC4Contract] = []
|
|
276
|
+
self._txn_fields: TxnFields = {}
|
|
277
|
+
self._gtxns: list[algopy.gtxn.TransactionBase] = []
|
|
278
|
+
self._active_transaction_index: int | None = None
|
|
279
|
+
self._application_data: dict[int, ApplicationFields] = {}
|
|
280
|
+
self._application_logs: dict[int, list[bytes]] = {}
|
|
281
|
+
self._asset_data: dict[int, AssetFields] = {}
|
|
282
|
+
self._inner_transaction_groups: list[Sequence[InnerTransactionResultType]] = []
|
|
283
|
+
self._constructing_inner_transaction_group: list[InnerTransactionResultType] = []
|
|
284
|
+
self._constructing_inner_transaction: InnerTransactionResultType | None = None
|
|
285
|
+
self._scratch_spaces: dict[str, list[algopy.Bytes | algopy.UInt64 | bytes | int]] = {}
|
|
286
|
+
self._template_vars: dict[str, Any] = template_vars or {}
|
|
287
|
+
self._blocks: dict[int, dict[str, int]] = {}
|
|
288
|
+
self._boxes: dict[bytes, algopy.Bytes] = {}
|
|
289
|
+
self._lsigs: dict[algopy.LogicSig, Callable[[], algopy.UInt64 | bool]] = {}
|
|
290
|
+
self._active_lsig_args: Sequence[algopy.Bytes] = []
|
|
291
|
+
|
|
292
|
+
self.default_creator = default_creator or algopy.Account(
|
|
293
|
+
algosdk.account.generate_account()[1]
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
default_application_id = next(self._app_id)
|
|
297
|
+
default_application_address = algosdk.logic.get_application_address(default_application_id)
|
|
298
|
+
|
|
299
|
+
self.default_application = default_application or self.any_application(
|
|
300
|
+
id=default_application_id,
|
|
301
|
+
address=algopy.Account(default_application_address),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
self._global_fields: GlobalFields = {
|
|
305
|
+
"min_txn_fee": algopy.UInt64(algosdk.constants.MIN_TXN_FEE),
|
|
306
|
+
"min_balance": algopy.UInt64(DEFAULT_ACCOUNT_MIN_BALANCE),
|
|
307
|
+
"max_txn_life": algopy.UInt64(DEFAULT_MAX_TXN_LIFE),
|
|
308
|
+
"zero_address": algopy.Account(algosdk.constants.ZERO_ADDRESS),
|
|
309
|
+
"creator_address": self.default_creator,
|
|
310
|
+
"current_application_address": algopy.Account(default_application_address),
|
|
311
|
+
"current_application_id": self.default_application,
|
|
312
|
+
"asset_create_min_balance": algopy.UInt64(DEFAULT_ASSET_CREATE_MIN_BALANCE),
|
|
313
|
+
"asset_opt_in_min_balance": algopy.UInt64(DEFAULT_ASSET_OPT_IN_MIN_BALANCE),
|
|
314
|
+
"genesis_hash": algopy.Bytes(DEFAULT_GLOBAL_GENESIS_HASH),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
self._account_data: dict[str, AccountContextData] = {
|
|
318
|
+
str(self.default_creator): AccountContextData(
|
|
319
|
+
fields=AccountFields(), opted_asset_balances={}, opted_apps={}
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def patch_global_fields(self, **global_fields: Unpack[GlobalFields]) -> None:
|
|
324
|
+
"""
|
|
325
|
+
Patch 'Global' fields in the test context.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
**global_fields: Key-value pairs for global fields.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
AttributeError: If a key is invalid.
|
|
332
|
+
"""
|
|
333
|
+
invalid_keys = global_fields.keys() - GlobalFields.__annotations__.keys()
|
|
334
|
+
|
|
335
|
+
if invalid_keys:
|
|
336
|
+
raise AttributeError(
|
|
337
|
+
f"Invalid field(s) found during patch for `Global`: {', '.join(invalid_keys)}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
self._global_fields.update(global_fields)
|
|
341
|
+
|
|
342
|
+
def patch_txn_fields(self, **txn_fields: Unpack[TxnFields]) -> None:
|
|
343
|
+
"""
|
|
344
|
+
Patch 'algopy.Txn' fields in the test context.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
**txn_fields: Key-value pairs for transaction fields.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
AttributeError: If a key is invalid.
|
|
351
|
+
"""
|
|
352
|
+
invalid_keys = txn_fields.keys() - TxnFields.__annotations__.keys()
|
|
353
|
+
if invalid_keys:
|
|
354
|
+
raise AttributeError(
|
|
355
|
+
f"Invalid field(s) found during patch for `Txn`: {', '.join(invalid_keys)}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
self._txn_fields.update(txn_fields)
|
|
359
|
+
|
|
360
|
+
def set_scratch_space(
|
|
361
|
+
self, txn: str, scratch_space: dict[int, algopy.Bytes | algopy.UInt64 | bytes | int]
|
|
362
|
+
) -> None:
|
|
363
|
+
new_scratch_space: list[algopy.Bytes | algopy.UInt64 | bytes | int] = [0] * 256
|
|
364
|
+
# insert values to list at specific indexes, use key as index and value as value to set
|
|
365
|
+
for index, value in scratch_space.items():
|
|
366
|
+
new_scratch_space[index] = value
|
|
367
|
+
|
|
368
|
+
self._scratch_spaces[str(txn)] = new_scratch_space
|
|
369
|
+
|
|
370
|
+
def set_template_var(self, name: str, value: Any) -> None:
|
|
371
|
+
self._template_vars[name] = value
|
|
372
|
+
|
|
373
|
+
def get_account(self, address: str) -> algopy.Account:
|
|
374
|
+
"""
|
|
375
|
+
Retrieve an account by address.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
address (str): Account address.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
algopy.Account: The account associated with the address.
|
|
382
|
+
"""
|
|
383
|
+
import algopy
|
|
384
|
+
|
|
385
|
+
if address not in self._account_data:
|
|
386
|
+
raise ValueError("Account not found in testing context!")
|
|
387
|
+
|
|
388
|
+
return algopy.Account(address)
|
|
389
|
+
|
|
390
|
+
def get_account_data(self) -> dict[str, AccountContextData]:
|
|
391
|
+
"""
|
|
392
|
+
Retrieve all account data.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
dict[str, AccountContextData]: The account data.
|
|
396
|
+
"""
|
|
397
|
+
return self._account_data
|
|
398
|
+
|
|
399
|
+
def get_asset_data(self) -> dict[int, AssetFields]:
|
|
400
|
+
"""
|
|
401
|
+
Retrieve all asset data.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
dict[int, AssetFields]: The asset data.
|
|
405
|
+
"""
|
|
406
|
+
return self._asset_data
|
|
407
|
+
|
|
408
|
+
def get_application_data(self) -> dict[int, ApplicationFields]:
|
|
409
|
+
"""
|
|
410
|
+
Retrieve all application data.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
dict[int, ApplicationFields]: The application data.
|
|
414
|
+
"""
|
|
415
|
+
return self._application_data
|
|
416
|
+
|
|
417
|
+
def get_contracts(self) -> list[algopy.Contract | algopy.ARC4Contract]:
|
|
418
|
+
"""
|
|
419
|
+
Retrieve all contracts.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
list[algopy.Contract | algopy.ARC4Contract]: The contracts.
|
|
423
|
+
"""
|
|
424
|
+
return self._contracts
|
|
425
|
+
|
|
426
|
+
def update_account(self, address: str, **account_fields: Unpack[AccountFields]) -> None:
|
|
427
|
+
"""
|
|
428
|
+
Update an existing account.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
address (str): Account address.
|
|
432
|
+
**account_fields: New account data.
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
TypeError: If the provided object is not an instance of `Account`.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
if address not in self._account_data:
|
|
439
|
+
raise ValueError("Account not found")
|
|
440
|
+
|
|
441
|
+
self._account_data[address].fields.update(account_fields)
|
|
442
|
+
|
|
443
|
+
def get_opted_asset_balance(
|
|
444
|
+
self, account: algopy.Account, asset_id: algopy.UInt64
|
|
445
|
+
) -> algopy.UInt64 | None:
|
|
446
|
+
"""
|
|
447
|
+
Retrieve the opted asset balance for an account and asset ID.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
account (algopy.Account): Account to retrieve the balance for.
|
|
451
|
+
asset_id (algopy.UInt64): Asset ID.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
algopy.UInt64 | None: The opted asset balance or None if not opted in.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
response = self._account_data.get(
|
|
458
|
+
str(account),
|
|
459
|
+
AccountContextData(fields=AccountFields(), opted_asset_balances={}, opted_apps={}),
|
|
460
|
+
).opted_asset_balances.get(asset_id, None)
|
|
461
|
+
|
|
462
|
+
return response
|
|
463
|
+
|
|
464
|
+
def get_asset(self, asset_id: algopy.UInt64 | int) -> algopy.Asset:
|
|
465
|
+
"""
|
|
466
|
+
Retrieve an asset by ID.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
asset_id (int): Asset ID.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
algopy.Asset: The asset associated with the ID.
|
|
473
|
+
"""
|
|
474
|
+
import algopy
|
|
475
|
+
|
|
476
|
+
asset_id = int(asset_id) if isinstance(asset_id, algopy.UInt64) else asset_id
|
|
477
|
+
|
|
478
|
+
if asset_id not in self._asset_data:
|
|
479
|
+
raise ValueError("Asset not found in testing context!")
|
|
480
|
+
|
|
481
|
+
return algopy.Asset(asset_id)
|
|
482
|
+
|
|
483
|
+
def update_asset(self, asset_id: int, **asset_fields: Unpack[AssetFields]) -> None:
|
|
484
|
+
"""
|
|
485
|
+
Update an existing asset.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
asset_id (int): Asset ID.
|
|
489
|
+
**asset_fields: New asset data.
|
|
490
|
+
"""
|
|
491
|
+
if asset_id not in self._asset_data:
|
|
492
|
+
raise ValueError("Asset not found in testing context!")
|
|
493
|
+
|
|
494
|
+
self._asset_data[asset_id].update(asset_fields)
|
|
495
|
+
|
|
496
|
+
def get_application(self, app_id: algopy.UInt64 | int) -> algopy.Application:
|
|
497
|
+
"""
|
|
498
|
+
Retrieve an application by ID.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
app_id (int): Application ID.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
algopy.Application: The application associated with the ID.
|
|
505
|
+
"""
|
|
506
|
+
import algopy
|
|
507
|
+
|
|
508
|
+
app_id = int(app_id) if isinstance(app_id, algopy.UInt64) else app_id
|
|
509
|
+
|
|
510
|
+
if app_id not in self._application_data:
|
|
511
|
+
raise ValueError("Application not found in testing context!")
|
|
512
|
+
|
|
513
|
+
return algopy.Application(app_id)
|
|
514
|
+
|
|
515
|
+
def update_application(
|
|
516
|
+
self, app_id: int, **application_fields: Unpack[ApplicationFields]
|
|
517
|
+
) -> None:
|
|
518
|
+
"""
|
|
519
|
+
Update an existing application.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
app_id (int): Application ID.
|
|
523
|
+
**application_fields: New application data.
|
|
524
|
+
"""
|
|
525
|
+
if app_id not in self._application_data:
|
|
526
|
+
raise ValueError("Application not found in testing context!")
|
|
527
|
+
|
|
528
|
+
self._application_data[app_id].update(application_fields)
|
|
529
|
+
|
|
530
|
+
def _add_contract(
|
|
531
|
+
self,
|
|
532
|
+
contract: algopy.Contract | algopy.ARC4Contract,
|
|
533
|
+
) -> None:
|
|
534
|
+
"""
|
|
535
|
+
Add a contract to the context.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
contract (algopy.Contract | algopy.ARC4Contract): The contract to add.
|
|
539
|
+
"""
|
|
540
|
+
self._contracts.append(contract)
|
|
541
|
+
|
|
542
|
+
def _append_inner_transaction_group(
|
|
543
|
+
self,
|
|
544
|
+
itxn: Sequence[InnerTransactionResultType],
|
|
545
|
+
) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Append a group of inner transactions to the context.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
itxn (Sequence[InnerTransactionResultType]): The group of inner transactions to append.
|
|
551
|
+
"""
|
|
552
|
+
import algopy.itxn
|
|
553
|
+
|
|
554
|
+
self._inner_transaction_groups.append(cast(list[algopy.itxn.InnerTransactionResult], itxn))
|
|
555
|
+
|
|
556
|
+
def get_submitted_itxn_groups(self) -> list[Sequence[InnerTransactionResultType]]:
|
|
557
|
+
"""
|
|
558
|
+
Retrieve the number of inner transaction groups.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
int: The number of inner transaction groups.
|
|
562
|
+
"""
|
|
563
|
+
return self._inner_transaction_groups
|
|
564
|
+
|
|
565
|
+
def get_submitted_itxn_group(self, index: int) -> ITxnGroupLoader:
|
|
566
|
+
"""
|
|
567
|
+
Retrieve the last group of inner transactions.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Sequence[algopy.itxn.InnerTransactionResult]: The last group of inner transactions.
|
|
571
|
+
|
|
572
|
+
Raises:
|
|
573
|
+
ValueError: If no inner transaction groups have been submitted yet.
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
if not self._inner_transaction_groups:
|
|
577
|
+
raise ValueError("No inner transaction groups submitted yet!")
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
return ITxnGroupLoader(self._inner_transaction_groups[index])
|
|
581
|
+
except IndexError as err:
|
|
582
|
+
raise ValueError(f"No inner transaction group available at index {index}!") from err
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def last_submitted_itxn(self) -> ITxnLoader:
|
|
586
|
+
"""
|
|
587
|
+
Retrieve the last submitted inner transaction from the
|
|
588
|
+
last inner transaction group (if both exist).
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
ITxnLoader: The last submitted inner transaction loader.
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
ValueError: If no inner transactions exist in the last inner transaction group.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
if not self._inner_transaction_groups or not self._inner_transaction_groups[-1]:
|
|
598
|
+
raise ValueError("No inner transactions in the last inner transaction group!")
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
last_itxn = self._inner_transaction_groups[-1][-1]
|
|
602
|
+
return ITxnLoader(last_itxn)
|
|
603
|
+
except IndexError as err:
|
|
604
|
+
raise ValueError("No inner transactions in the last inner transaction group!") from err
|
|
605
|
+
|
|
606
|
+
def any_account(
|
|
607
|
+
self,
|
|
608
|
+
address: str | None = None,
|
|
609
|
+
opted_asset_balances: dict[algopy.UInt64, algopy.UInt64] | None = None,
|
|
610
|
+
opted_apps: dict[algopy.UInt64, algopy.Application] | None = None,
|
|
611
|
+
**account_fields: Unpack[AccountFields],
|
|
612
|
+
) -> algopy.Account:
|
|
613
|
+
"""
|
|
614
|
+
Generate and add a new account with a random address.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
algopy.Account: The newly generated account.
|
|
618
|
+
"""
|
|
619
|
+
import algopy
|
|
620
|
+
|
|
621
|
+
if address and not algosdk.encoding.is_valid_address(address):
|
|
622
|
+
raise ValueError("Invalid Algorand address supplied!")
|
|
623
|
+
|
|
624
|
+
if address in self._account_data:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
"Account with such address already exists in testing context! "
|
|
627
|
+
"Use `context.get_account(address)` to retrieve the existing account."
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
for key in account_fields:
|
|
631
|
+
if key not in AccountFields.__annotations__:
|
|
632
|
+
raise AttributeError(f"Invalid field '{key}' for Account")
|
|
633
|
+
|
|
634
|
+
new_account_address = address or algosdk.account.generate_account()[1]
|
|
635
|
+
new_account = algopy.Account(new_account_address)
|
|
636
|
+
new_account_fields = AccountFields(**account_fields)
|
|
637
|
+
new_account_data = AccountContextData(
|
|
638
|
+
fields=new_account_fields,
|
|
639
|
+
opted_asset_balances=opted_asset_balances or {},
|
|
640
|
+
opted_apps=opted_apps or {},
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
self._account_data[new_account_address] = new_account_data
|
|
644
|
+
|
|
645
|
+
return new_account
|
|
646
|
+
|
|
647
|
+
def any_asset(
|
|
648
|
+
self, asset_id: int | None = None, **asset_fields: Unpack[AssetFields]
|
|
649
|
+
) -> algopy.Asset:
|
|
650
|
+
"""
|
|
651
|
+
Generate and add a new asset with a unique ID.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
algopy.Asset: The newly generated asset.
|
|
655
|
+
"""
|
|
656
|
+
import algopy
|
|
657
|
+
|
|
658
|
+
if asset_id and asset_id in self._asset_data:
|
|
659
|
+
raise ValueError("Asset with such ID already exists in testing context!")
|
|
660
|
+
|
|
661
|
+
new_asset = algopy.Asset(asset_id or next(self._asset_id))
|
|
662
|
+
self._asset_data[int(new_asset.id)] = AssetFields(**asset_fields)
|
|
663
|
+
return new_asset
|
|
664
|
+
|
|
665
|
+
def any_application( # type: ignore[misc]
|
|
666
|
+
self,
|
|
667
|
+
id: int | None = None,
|
|
668
|
+
address: algopy.Account | None = None,
|
|
669
|
+
**application_fields: Unpack[ApplicationFields],
|
|
670
|
+
) -> algopy.Application:
|
|
671
|
+
"""
|
|
672
|
+
Generate and add a new application with a unique ID.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
id (int | None): Optional application ID. If not provided, a new ID will be generated.
|
|
676
|
+
address (algopy.Account | None): Optional application address. If not provided,
|
|
677
|
+
it will be generated.
|
|
678
|
+
**application_fields: Additional application fields.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
algopy.Application: The newly generated application.
|
|
682
|
+
"""
|
|
683
|
+
import algopy
|
|
684
|
+
|
|
685
|
+
new_app_id = id if id is not None else next(self._app_id)
|
|
686
|
+
new_app = algopy.Application(new_app_id)
|
|
687
|
+
|
|
688
|
+
if address is None:
|
|
689
|
+
address = algopy.Account(algosdk.logic.get_application_address(new_app_id))
|
|
690
|
+
|
|
691
|
+
app_fields = ApplicationFields(address=address, **application_fields) # type: ignore[typeddict-item]
|
|
692
|
+
self._application_data[int(new_app.id)] = app_fields
|
|
693
|
+
return new_app
|
|
694
|
+
|
|
695
|
+
def add_application_logs(
|
|
696
|
+
self,
|
|
697
|
+
*,
|
|
698
|
+
app_id: algopy.UInt64 | algopy.Application | int,
|
|
699
|
+
logs: bytes | list[bytes],
|
|
700
|
+
prepend_arc4_prefix: bool = False,
|
|
701
|
+
) -> None:
|
|
702
|
+
"""
|
|
703
|
+
Add logs for an application.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
app_id (int): The ID of the application.
|
|
707
|
+
logs (bytes | list[bytes]): A single log entry or a list of log entries.
|
|
708
|
+
"""
|
|
709
|
+
import algopy
|
|
710
|
+
|
|
711
|
+
raw_app_id = (
|
|
712
|
+
int(app_id)
|
|
713
|
+
if isinstance(app_id, algopy.UInt64)
|
|
714
|
+
else int(app_id.id) if isinstance(app_id, algopy.Application) else app_id
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
if isinstance(logs, bytes):
|
|
718
|
+
logs = [logs]
|
|
719
|
+
|
|
720
|
+
if prepend_arc4_prefix:
|
|
721
|
+
logs = [ARC4_RETURN_PREFIX + log for log in logs]
|
|
722
|
+
|
|
723
|
+
if raw_app_id in self._application_logs:
|
|
724
|
+
self._application_logs[raw_app_id].extend(logs)
|
|
725
|
+
else:
|
|
726
|
+
self._application_logs[raw_app_id] = logs
|
|
727
|
+
|
|
728
|
+
def get_application_logs(self, app_id: algopy.UInt64 | int) -> list[bytes]:
|
|
729
|
+
"""
|
|
730
|
+
Retrieve the application logs for a given app ID.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
app_id (int): The ID of the application.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
list[bytes]: The application logs for the given app ID.
|
|
737
|
+
"""
|
|
738
|
+
import algopy
|
|
739
|
+
|
|
740
|
+
app_id = int(app_id) if isinstance(app_id, algopy.UInt64) else app_id
|
|
741
|
+
|
|
742
|
+
if app_id not in self._application_logs:
|
|
743
|
+
raise ValueError(
|
|
744
|
+
f"No application logs available for app ID {app_id} in testing context!"
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
return self._application_logs[app_id]
|
|
748
|
+
|
|
749
|
+
def set_block(
|
|
750
|
+
self, index: int, seed: algopy.UInt64 | int, timestamp: algopy.UInt64 | int
|
|
751
|
+
) -> None:
|
|
752
|
+
"""
|
|
753
|
+
Set the block seed and timestamp for block at index `index`.
|
|
754
|
+
"""
|
|
755
|
+
self._blocks[index] = {"seed": int(seed), "timestamp": int(timestamp)}
|
|
756
|
+
|
|
757
|
+
def set_transaction_group(
|
|
758
|
+
self, gtxn: list[algopy.gtxn.TransactionBase], active_transaction_index: int | None = None
|
|
759
|
+
) -> None:
|
|
760
|
+
"""
|
|
761
|
+
Set the transaction group using a list of transactions.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
gtxn (list[algopy.gtxn.TransactionBase]): List of transactions.
|
|
765
|
+
active_transaction_index (int, optional): Index of the active transaction.
|
|
766
|
+
Defaults to None.
|
|
767
|
+
"""
|
|
768
|
+
self._gtxns = gtxn
|
|
769
|
+
|
|
770
|
+
if active_transaction_index is not None:
|
|
771
|
+
self.set_active_transaction_index(active_transaction_index)
|
|
772
|
+
|
|
773
|
+
def add_transactions(
|
|
774
|
+
self,
|
|
775
|
+
gtxns: list[algopy.gtxn.TransactionBase],
|
|
776
|
+
) -> None:
|
|
777
|
+
"""
|
|
778
|
+
Add transactions to the current transaction group.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
gtxns (list[algopy.gtxn.TransactionBase]): List of transactions to add.
|
|
782
|
+
|
|
783
|
+
Raises:
|
|
784
|
+
ValueError: If any transaction is not an instance of TransactionBase or if the total
|
|
785
|
+
number of transactions exceeds the group limit.
|
|
786
|
+
"""
|
|
787
|
+
# ensure that new len after append is at most 16 txns in a group
|
|
788
|
+
import algopy.gtxn
|
|
789
|
+
|
|
790
|
+
if not all(isinstance(txn, algopy.gtxn.TransactionBase) for txn in gtxns): # type: ignore[arg-type, unused-ignore]
|
|
791
|
+
raise ValueError("All transactions must be instances of TransactionBase")
|
|
792
|
+
|
|
793
|
+
if len(self._gtxns) + len(gtxns) > algosdk.constants.TX_GROUP_LIMIT:
|
|
794
|
+
raise ValueError(
|
|
795
|
+
f"Transaction group can have at most {algosdk.constants.TX_GROUP_LIMIT} "
|
|
796
|
+
"transactions, as per AVM limits."
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
self._gtxns.extend(gtxns)
|
|
800
|
+
|
|
801
|
+
# iterate and refresh group_index to match the order of transactions
|
|
802
|
+
for i, txn in enumerate(self._gtxns):
|
|
803
|
+
txn._fields["group_index"] = i
|
|
804
|
+
|
|
805
|
+
def get_transaction_group(self) -> list[algopy.gtxn.TransactionBase]:
|
|
806
|
+
"""
|
|
807
|
+
Retrieve the current transaction group.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
list[algopy.gtxn.TransactionBase]: The current transaction group.
|
|
811
|
+
"""
|
|
812
|
+
return self._gtxns
|
|
813
|
+
|
|
814
|
+
def set_active_transaction_index(self, index: int) -> None:
|
|
815
|
+
"""
|
|
816
|
+
Set the index of the active transaction.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
index (int): The index of the active transaction.
|
|
820
|
+
"""
|
|
821
|
+
self._active_transaction_index = index
|
|
822
|
+
|
|
823
|
+
def get_active_transaction(
|
|
824
|
+
self,
|
|
825
|
+
) -> algopy.gtxn.Transaction | None:
|
|
826
|
+
"""
|
|
827
|
+
Retrieve the active transaction of a specified type.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
_txn_type (type[T]): The type of the active transaction.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
T | None: The active transaction if it exists, otherwise None.
|
|
834
|
+
"""
|
|
835
|
+
import algopy
|
|
836
|
+
|
|
837
|
+
if self._active_transaction_index is None:
|
|
838
|
+
return None
|
|
839
|
+
active_txn = self._gtxns[self._active_transaction_index]
|
|
840
|
+
return cast(algopy.gtxn.Transaction, active_txn) if active_txn else None
|
|
841
|
+
|
|
842
|
+
def any_uint64(self, min_value: int = 0, max_value: int = MAX_UINT64) -> algopy.UInt64:
|
|
843
|
+
"""
|
|
844
|
+
Generate a random UInt64 value within a specified range.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
min_value (int, optional): Minimum value. Defaults to 0.
|
|
848
|
+
max_value (int, optional): Maximum value. Defaults to MAX_UINT64.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
algopy.UInt64: The randomly generated UInt64 value.
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
ValueError: If `max_value` exceeds MAX_UINT64 or `min_value` exceeds `max_value`.
|
|
855
|
+
"""
|
|
856
|
+
import algopy
|
|
857
|
+
|
|
858
|
+
if max_value > MAX_UINT64:
|
|
859
|
+
raise ValueError("max_value must be less than or equal to MAX_UINT64")
|
|
860
|
+
if min_value > max_value:
|
|
861
|
+
raise ValueError("min_value must be less than or equal to max_value")
|
|
862
|
+
|
|
863
|
+
random_value = secrets.randbelow(max_value - min_value) + min_value
|
|
864
|
+
return algopy.UInt64(random_value)
|
|
865
|
+
|
|
866
|
+
def any_bytes(self, length: int = MAX_BYTES_SIZE) -> algopy.Bytes:
|
|
867
|
+
"""
|
|
868
|
+
Generate a random byte sequence of a specified length.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
length (int, optional): Length of the byte sequence. Defaults to MAX_BYTES_SIZE.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
algopy.Bytes: The randomly generated byte sequence.
|
|
875
|
+
"""
|
|
876
|
+
import algopy
|
|
877
|
+
|
|
878
|
+
return algopy.Bytes(secrets.token_bytes(length))
|
|
879
|
+
|
|
880
|
+
def any_string(self, length: int = MAX_BYTES_SIZE) -> algopy.String:
|
|
881
|
+
"""
|
|
882
|
+
Generate a random string of a specified length.
|
|
883
|
+
"""
|
|
884
|
+
import algopy
|
|
885
|
+
|
|
886
|
+
return algopy.String(
|
|
887
|
+
"".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
def any_application_call_transaction( # type: ignore[misc] # noqa: PLR0913
|
|
891
|
+
self,
|
|
892
|
+
app_id: algopy.Application,
|
|
893
|
+
app_args: Sequence[algopy.Bytes] = [],
|
|
894
|
+
accounts: Sequence[algopy.Account] = [],
|
|
895
|
+
assets: Sequence[algopy.Asset] = [],
|
|
896
|
+
apps: Sequence[algopy.Application] = [],
|
|
897
|
+
approval_program_pages: Sequence[algopy.Bytes] = [],
|
|
898
|
+
clear_state_program_pages: Sequence[algopy.Bytes] = [],
|
|
899
|
+
scratch_space: dict[int, algopy.Bytes | algopy.UInt64 | int | bytes] | None = None,
|
|
900
|
+
**kwargs: Unpack[_ApplicationCallFields],
|
|
901
|
+
) -> algopy.gtxn.ApplicationCallTransaction:
|
|
902
|
+
"""
|
|
903
|
+
Generate a new application call transaction with specified fields.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
**kwargs (Unpack[ApplicationCallFields]): Fields to be set in the transaction.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
algopy.gtxn.ApplicationCallTransaction: The newly generated application
|
|
910
|
+
call transaction.
|
|
911
|
+
"""
|
|
912
|
+
import algopy.gtxn
|
|
913
|
+
|
|
914
|
+
if not isinstance(app_id, algopy.Application):
|
|
915
|
+
raise TypeError("`app_id` must be an instance of algopy.Application")
|
|
916
|
+
if int(app_id.id) not in self._application_data:
|
|
917
|
+
raise ValueError(
|
|
918
|
+
f"algopy.Application with ID {app_id.id} not found in testing context!"
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
dynamic_params = {
|
|
922
|
+
"app_id": lambda: app_id,
|
|
923
|
+
"app_args": lambda index: app_args[index],
|
|
924
|
+
"accounts": lambda index: accounts[index],
|
|
925
|
+
"assets": lambda index: assets[index],
|
|
926
|
+
"apps": lambda index: apps[index],
|
|
927
|
+
"approval_program_pages": lambda index: approval_program_pages[index],
|
|
928
|
+
"clear_state_program_pages": lambda index: clear_state_program_pages[index],
|
|
929
|
+
}
|
|
930
|
+
new_txn = algopy.gtxn.ApplicationCallTransaction(NULL_GTXN_GROUP_INDEX)
|
|
931
|
+
|
|
932
|
+
for key, value in {**kwargs, **dynamic_params}.items():
|
|
933
|
+
setattr(new_txn, key, value)
|
|
934
|
+
|
|
935
|
+
self.set_scratch_space(new_txn.txn_id, scratch_space or {})
|
|
936
|
+
|
|
937
|
+
return new_txn
|
|
938
|
+
|
|
939
|
+
def any_asset_transfer_transaction(
|
|
940
|
+
self, **kwargs: Unpack[AssetTransferFields]
|
|
941
|
+
) -> algopy.gtxn.AssetTransferTransaction:
|
|
942
|
+
"""
|
|
943
|
+
Generate a new asset transfer transaction with specified fields.
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
**kwargs (Unpack[AssetTransferFields]): Fields to be set in the transaction.
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
algopy.gtxn.AssetTransferTransaction: The newly generated asset transfer transaction.
|
|
950
|
+
"""
|
|
951
|
+
import algopy.gtxn
|
|
952
|
+
|
|
953
|
+
new_txn = algopy.gtxn.AssetTransferTransaction(NULL_GTXN_GROUP_INDEX)
|
|
954
|
+
|
|
955
|
+
for key, value in kwargs.items():
|
|
956
|
+
setattr(new_txn, key, value)
|
|
957
|
+
|
|
958
|
+
return new_txn
|
|
959
|
+
|
|
960
|
+
def any_payment_transaction(
|
|
961
|
+
self, **kwargs: Unpack[PaymentFields]
|
|
962
|
+
) -> algopy.gtxn.PaymentTransaction:
|
|
963
|
+
"""
|
|
964
|
+
Generate a new payment transaction with specified fields.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
**kwargs (Unpack[PaymentFields]): Fields to be set in the transaction.
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
algopy.gtxn.PaymentTransaction: The newly generated payment transaction.
|
|
971
|
+
"""
|
|
972
|
+
import algopy.gtxn
|
|
973
|
+
|
|
974
|
+
new_txn = algopy.gtxn.PaymentTransaction(NULL_GTXN_GROUP_INDEX)
|
|
975
|
+
|
|
976
|
+
for key, value in kwargs.items():
|
|
977
|
+
setattr(new_txn, key, value)
|
|
978
|
+
|
|
979
|
+
return new_txn
|
|
980
|
+
|
|
981
|
+
def any_asset_config_transaction(
|
|
982
|
+
self, **kwargs: Unpack[AssetConfigFields]
|
|
983
|
+
) -> algopy.gtxn.AssetConfigTransaction:
|
|
984
|
+
"""
|
|
985
|
+
Generate a new ACFG transaction with specified fields.
|
|
986
|
+
"""
|
|
987
|
+
import algopy.gtxn
|
|
988
|
+
|
|
989
|
+
new_txn = algopy.gtxn.AssetConfigTransaction(NULL_GTXN_GROUP_INDEX)
|
|
990
|
+
|
|
991
|
+
for key, value in kwargs.items():
|
|
992
|
+
setattr(new_txn, key, value)
|
|
993
|
+
|
|
994
|
+
return new_txn
|
|
995
|
+
|
|
996
|
+
def any_key_registration_transaction(
|
|
997
|
+
self, **kwargs: Unpack[KeyRegistrationFields]
|
|
998
|
+
) -> algopy.gtxn.KeyRegistrationTransaction:
|
|
999
|
+
"""
|
|
1000
|
+
Generate a new key registration transaction with specified fields.
|
|
1001
|
+
"""
|
|
1002
|
+
import algopy.gtxn
|
|
1003
|
+
|
|
1004
|
+
new_txn = algopy.gtxn.KeyRegistrationTransaction(NULL_GTXN_GROUP_INDEX)
|
|
1005
|
+
|
|
1006
|
+
for key, value in kwargs.items():
|
|
1007
|
+
setattr(new_txn, key, value)
|
|
1008
|
+
|
|
1009
|
+
return new_txn
|
|
1010
|
+
|
|
1011
|
+
def any_asset_freeze_transaction(
|
|
1012
|
+
self, **kwargs: Unpack[AssetFreezeFields]
|
|
1013
|
+
) -> algopy.gtxn.AssetFreezeTransaction:
|
|
1014
|
+
"""
|
|
1015
|
+
Generate a new asset freeze transaction with specified fields.
|
|
1016
|
+
"""
|
|
1017
|
+
import algopy.gtxn
|
|
1018
|
+
|
|
1019
|
+
new_txn = algopy.gtxn.AssetFreezeTransaction(NULL_GTXN_GROUP_INDEX)
|
|
1020
|
+
|
|
1021
|
+
for key, value in kwargs.items():
|
|
1022
|
+
setattr(new_txn, key, value)
|
|
1023
|
+
|
|
1024
|
+
return new_txn
|
|
1025
|
+
|
|
1026
|
+
def any_transaction( # type: ignore[misc]
|
|
1027
|
+
self,
|
|
1028
|
+
type: algopy.TransactionType, # noqa: A002
|
|
1029
|
+
**kwargs: Unpack[TransactionFields],
|
|
1030
|
+
) -> algopy.gtxn.Transaction:
|
|
1031
|
+
"""
|
|
1032
|
+
Generate a new transaction with specified fields.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
type (algopy.TransactionType): Transaction type.
|
|
1036
|
+
**kwargs (Unpack[TransactionFields]): Fields to be set in the transaction.
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
algopy.gtxn.Transaction: The newly generated transaction.
|
|
1040
|
+
"""
|
|
1041
|
+
import algopy.gtxn
|
|
1042
|
+
|
|
1043
|
+
new_txn = algopy.gtxn.Transaction(NULL_GTXN_GROUP_INDEX, type=type) # type: ignore[arg-type, unused-ignore]
|
|
1044
|
+
|
|
1045
|
+
for key, value in kwargs.items():
|
|
1046
|
+
setattr(new_txn, key, value)
|
|
1047
|
+
|
|
1048
|
+
return new_txn
|
|
1049
|
+
|
|
1050
|
+
def get_box(self, name: algopy.Bytes | bytes) -> algopy.Bytes:
|
|
1051
|
+
"""Get the content of a box."""
|
|
1052
|
+
import algopy
|
|
1053
|
+
|
|
1054
|
+
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1055
|
+
return self._boxes.get(name_bytes, algopy.Bytes(b""))
|
|
1056
|
+
|
|
1057
|
+
def set_box(self, name: algopy.Bytes | bytes, content: algopy.Bytes | bytes) -> None:
|
|
1058
|
+
"""Set the content of a box."""
|
|
1059
|
+
import algopy
|
|
1060
|
+
|
|
1061
|
+
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1062
|
+
content_bytes = content if isinstance(content, bytes) else content.value
|
|
1063
|
+
self._boxes[name_bytes] = algopy.Bytes(content_bytes)
|
|
1064
|
+
|
|
1065
|
+
def execute_logicsig(
|
|
1066
|
+
self, lsig: algopy.LogicSig, lsig_args: Sequence[algopy.Bytes] | None = None
|
|
1067
|
+
) -> bool | algopy.UInt64:
|
|
1068
|
+
self._active_lsig_args = lsig_args or []
|
|
1069
|
+
# TODO: refine LogicSig class to handle injects into context
|
|
1070
|
+
if lsig not in self._lsigs:
|
|
1071
|
+
self._lsigs[lsig] = lsig.func
|
|
1072
|
+
return lsig.func()
|
|
1073
|
+
|
|
1074
|
+
def clear_box(self, name: algopy.Bytes | bytes) -> None:
|
|
1075
|
+
"""Clear the content of a box."""
|
|
1076
|
+
|
|
1077
|
+
name_bytes = name if isinstance(name, bytes) else name.value
|
|
1078
|
+
if name_bytes in self._boxes:
|
|
1079
|
+
del self._boxes[name_bytes]
|
|
1080
|
+
|
|
1081
|
+
def clear_all_boxes(self) -> None:
|
|
1082
|
+
"""Clear all boxes."""
|
|
1083
|
+
self._boxes.clear()
|
|
1084
|
+
|
|
1085
|
+
def clear_inner_transaction_groups(self) -> None:
|
|
1086
|
+
"""
|
|
1087
|
+
Clear all inner transactions.
|
|
1088
|
+
"""
|
|
1089
|
+
self._inner_transaction_groups.clear()
|
|
1090
|
+
|
|
1091
|
+
def clear_transaction_group(self) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Clear the transaction group.
|
|
1094
|
+
"""
|
|
1095
|
+
self._gtxns.clear()
|
|
1096
|
+
|
|
1097
|
+
def clear_accounts(self) -> None:
|
|
1098
|
+
"""
|
|
1099
|
+
Clear all accounts.
|
|
1100
|
+
"""
|
|
1101
|
+
self._account_data.clear()
|
|
1102
|
+
|
|
1103
|
+
def clear_applications(self) -> None:
|
|
1104
|
+
"""
|
|
1105
|
+
Clear all applications.
|
|
1106
|
+
"""
|
|
1107
|
+
self._application_data.clear()
|
|
1108
|
+
|
|
1109
|
+
def clear_assets(self) -> None:
|
|
1110
|
+
"""
|
|
1111
|
+
Clear all assets.
|
|
1112
|
+
"""
|
|
1113
|
+
self._asset_data.clear()
|
|
1114
|
+
|
|
1115
|
+
def clear_application_logs(self) -> None:
|
|
1116
|
+
"""
|
|
1117
|
+
Clear all application logs.
|
|
1118
|
+
"""
|
|
1119
|
+
self._application_logs.clear()
|
|
1120
|
+
|
|
1121
|
+
def clear_contracts(self) -> None:
|
|
1122
|
+
"""
|
|
1123
|
+
Clear all contracts.
|
|
1124
|
+
"""
|
|
1125
|
+
self._contracts.clear()
|
|
1126
|
+
|
|
1127
|
+
def clear_scratch_spaces(self) -> None:
|
|
1128
|
+
"""
|
|
1129
|
+
Clear all scratch spaces.
|
|
1130
|
+
"""
|
|
1131
|
+
self._scratch_spaces.clear()
|
|
1132
|
+
|
|
1133
|
+
def clear_active_transaction_index(self) -> None:
|
|
1134
|
+
"""
|
|
1135
|
+
Clear the active transaction index.
|
|
1136
|
+
"""
|
|
1137
|
+
self._active_transaction_index = None
|
|
1138
|
+
|
|
1139
|
+
def clear(self) -> None:
|
|
1140
|
+
"""
|
|
1141
|
+
Clear all data, including accounts, applications, assets, inner transactions,
|
|
1142
|
+
transaction groups, and application_logs.
|
|
1143
|
+
"""
|
|
1144
|
+
self.clear_accounts()
|
|
1145
|
+
self.clear_applications()
|
|
1146
|
+
self.clear_assets()
|
|
1147
|
+
self.clear_inner_transaction_groups()
|
|
1148
|
+
self.clear_transaction_group()
|
|
1149
|
+
self.clear_application_logs()
|
|
1150
|
+
self.clear_contracts()
|
|
1151
|
+
self.clear_scratch_spaces()
|
|
1152
|
+
self.clear_active_transaction_index()
|
|
1153
|
+
|
|
1154
|
+
def reset(self) -> None:
|
|
1155
|
+
"""
|
|
1156
|
+
Reset the test context to its initial state, clearing all data and resetting ID counters.
|
|
1157
|
+
"""
|
|
1158
|
+
self._account_data = {}
|
|
1159
|
+
self._application_data = {}
|
|
1160
|
+
self._asset_data = {}
|
|
1161
|
+
self._contracts = []
|
|
1162
|
+
self._active_transaction_index = None
|
|
1163
|
+
self._scratch_spaces = {}
|
|
1164
|
+
self._inner_transaction_groups = []
|
|
1165
|
+
self._gtxns = []
|
|
1166
|
+
self._global_fields = {}
|
|
1167
|
+
self._txn_fields = {}
|
|
1168
|
+
self._application_logs = {}
|
|
1169
|
+
self._asset_id = iter(range(1, 2**64))
|
|
1170
|
+
self._app_id = iter(range(1, 2**64))
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
#
|
|
1174
|
+
_var: ContextVar[AlgopyTestContext] = ContextVar("_var")
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def get_test_context() -> AlgopyTestContext:
|
|
1178
|
+
return _var.get()
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
@contextmanager
|
|
1182
|
+
def algopy_testing_context(
|
|
1183
|
+
*,
|
|
1184
|
+
default_creator: algopy.Account | None = None,
|
|
1185
|
+
) -> Generator[AlgopyTestContext, None, None]:
|
|
1186
|
+
token = _var.set(
|
|
1187
|
+
AlgopyTestContext(
|
|
1188
|
+
default_creator=default_creator,
|
|
1189
|
+
)
|
|
1190
|
+
)
|
|
1191
|
+
try:
|
|
1192
|
+
yield _var.get()
|
|
1193
|
+
finally:
|
|
1194
|
+
_var.reset(token)
|