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.
Files changed (52) hide show
  1. algopy/__init__.py +58 -0
  2. algopy/arc4.py +1 -0
  3. algopy/gtxn.py +1 -0
  4. algopy/itxn.py +1 -0
  5. algopy/op.py +1 -0
  6. algopy/py.typed +0 -0
  7. algopy_testing/__init__.py +55 -0
  8. algopy_testing/arc4.py +1533 -0
  9. algopy_testing/constants.py +22 -0
  10. algopy_testing/context.py +1194 -0
  11. algopy_testing/decorators/__init__.py +0 -0
  12. algopy_testing/decorators/abimethod.py +204 -0
  13. algopy_testing/decorators/baremethod.py +83 -0
  14. algopy_testing/decorators/subroutine.py +9 -0
  15. algopy_testing/enums.py +42 -0
  16. algopy_testing/gtxn.py +261 -0
  17. algopy_testing/itxn.py +665 -0
  18. algopy_testing/models/__init__.py +31 -0
  19. algopy_testing/models/account.py +128 -0
  20. algopy_testing/models/application.py +72 -0
  21. algopy_testing/models/asset.py +109 -0
  22. algopy_testing/models/block.py +34 -0
  23. algopy_testing/models/box.py +158 -0
  24. algopy_testing/models/contract.py +82 -0
  25. algopy_testing/models/gitxn.py +42 -0
  26. algopy_testing/models/global_values.py +72 -0
  27. algopy_testing/models/gtxn.py +56 -0
  28. algopy_testing/models/itxn.py +85 -0
  29. algopy_testing/models/logicsig.py +44 -0
  30. algopy_testing/models/template_variable.py +23 -0
  31. algopy_testing/models/transactions.py +158 -0
  32. algopy_testing/models/txn.py +113 -0
  33. algopy_testing/models/unsigned_builtins.py +36 -0
  34. algopy_testing/op.py +1098 -0
  35. algopy_testing/primitives/__init__.py +6 -0
  36. algopy_testing/primitives/biguint.py +148 -0
  37. algopy_testing/primitives/bytes.py +174 -0
  38. algopy_testing/primitives/string.py +68 -0
  39. algopy_testing/primitives/uint64.py +213 -0
  40. algopy_testing/protocols.py +18 -0
  41. algopy_testing/py.typed +0 -0
  42. algopy_testing/state/__init__.py +4 -0
  43. algopy_testing/state/global_state.py +73 -0
  44. algopy_testing/state/local_state.py +54 -0
  45. algopy_testing/utilities/__init__.py +3 -0
  46. algopy_testing/utilities/budget.py +23 -0
  47. algopy_testing/utilities/log.py +55 -0
  48. algopy_testing/utils.py +249 -0
  49. algorand_python_testing-0.0.0b1.dist-info/METADATA +81 -0
  50. algorand_python_testing-0.0.0b1.dist-info/RECORD +52 -0
  51. algorand_python_testing-0.0.0b1.dist-info/WHEEL +4 -0
  52. algorand_python_testing-0.0.0b1.dist-info/licenses/LICENSE +14 -0
algopy_testing/op.py ADDED
@@ -0,0 +1,1098 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import math
7
+ import typing
8
+ from typing import TYPE_CHECKING, Any, Literal, cast
9
+
10
+ import algosdk
11
+ import coincurve
12
+ import nacl.exceptions
13
+ import nacl.signing
14
+ from Cryptodome.Hash import SHA512, keccak
15
+ from ecdsa import ( # type: ignore # noqa: PGH003
16
+ BadSignatureError,
17
+ NIST256p,
18
+ SECP256k1,
19
+ VerifyingKey,
20
+ )
21
+
22
+ from algopy_testing.constants import (
23
+ BITS_IN_BYTE,
24
+ DEFAULT_ACCOUNT_MIN_BALANCE,
25
+ MAX_BYTES_SIZE,
26
+ MAX_UINT64,
27
+ )
28
+ from algopy_testing.enums import EC, ECDSA, Base64, VrfVerify
29
+ from algopy_testing.models.block import Block
30
+ from algopy_testing.models.box import Box
31
+ from algopy_testing.models.gitxn import GITxn
32
+ from algopy_testing.models.global_values import Global
33
+ from algopy_testing.models.gtxn import GTxn
34
+ from algopy_testing.models.itxn import ITxn, ITxnCreate
35
+ from algopy_testing.models.txn import Txn
36
+ from algopy_testing.primitives.biguint import BigUInt
37
+ from algopy_testing.primitives.bytes import Bytes
38
+ from algopy_testing.primitives.uint64 import UInt64
39
+ from algopy_testing.utils import as_bytes, as_int, as_int8, as_int64, as_int512, int_to_bytes
40
+
41
+ if TYPE_CHECKING:
42
+ import algopy
43
+
44
+
45
+ def sha256(a: Bytes | bytes, /) -> Bytes:
46
+ input_value = as_bytes(a)
47
+ return Bytes(hashlib.sha256(input_value).digest())
48
+
49
+
50
+ def sha3_256(a: Bytes | bytes, /) -> Bytes:
51
+ input_value = as_bytes(a)
52
+ return Bytes(hashlib.sha3_256(input_value).digest())
53
+
54
+
55
+ def keccak256(a: Bytes | bytes, /) -> Bytes:
56
+ input_value = as_bytes(a)
57
+ hashed_value = keccak.new(data=input_value, digest_bits=256)
58
+ return Bytes(hashed_value.digest())
59
+
60
+
61
+ def sha512_256(a: Bytes | bytes, /) -> Bytes:
62
+ input_value = as_bytes(a)
63
+ hash_object = SHA512.new(input_value, truncate="256")
64
+ return Bytes(hash_object.digest())
65
+
66
+
67
+ def ed25519verify_bare(a: Bytes | bytes, b: Bytes | bytes, c: Bytes | bytes, /) -> bool:
68
+ a, b, c = (as_bytes(x) for x in [a, b, c])
69
+
70
+ try:
71
+ verify_key = nacl.signing.VerifyKey(c)
72
+ verify_key.verify(a, b)
73
+ except nacl.exceptions.BadSignatureError:
74
+ return False
75
+ else:
76
+ return True
77
+
78
+
79
+ def ed25519verify(a: Bytes | bytes, b: Bytes | bytes, c: Bytes | bytes, /) -> bool:
80
+ from algopy_testing.context import get_test_context
81
+ from algopy_testing.utils import as_bytes
82
+
83
+ try:
84
+ ctx = get_test_context()
85
+ except LookupError as e:
86
+ raise RuntimeError(
87
+ "function must be run within an active context"
88
+ " using `with algopy_testing.context.new_context():`"
89
+ ) from e
90
+
91
+ # TODO: Decide on whether to pick clear or approval depending on OnComplete state
92
+ if not ctx._txn_fields:
93
+ raise RuntimeError("`txn_fields` must be set in the context")
94
+
95
+ program_bytes = as_bytes(ctx._txn_fields.get("approval_program", None))
96
+ if not program_bytes:
97
+ raise RuntimeError("`program_bytes` must be set in the context")
98
+
99
+ decoded_address = algosdk.encoding.decode_address(algosdk.logic.address(program_bytes))
100
+ address_bytes = as_bytes(decoded_address)
101
+ a = algosdk.constants.logic_data_prefix + address_bytes + a
102
+ return ed25519verify_bare(a, b, c)
103
+
104
+
105
+ def ecdsa_verify( # noqa: PLR0913
106
+ v: ECDSA,
107
+ a: Bytes | bytes,
108
+ b: Bytes | bytes,
109
+ c: Bytes | bytes,
110
+ d: Bytes | bytes,
111
+ e: Bytes | bytes,
112
+ /,
113
+ ) -> bool:
114
+ data_bytes, sig_r_bytes, sig_s_bytes, pubkey_x_bytes, pubkey_y_bytes = map(
115
+ as_bytes, [a, b, c, d, e]
116
+ )
117
+
118
+ curve_map = {
119
+ ECDSA.Secp256k1: SECP256k1,
120
+ ECDSA.Secp256r1: NIST256p,
121
+ }
122
+
123
+ curve = curve_map.get(v)
124
+ if curve is None:
125
+ raise ValueError(f"Unsupported ECDSA curve: {v}")
126
+
127
+ public_key = b"\x04" + pubkey_x_bytes + pubkey_y_bytes
128
+ vk = VerifyingKey.from_string(public_key, curve=curve)
129
+
130
+ # Concatenate R and S components to form the signature
131
+ signature = sig_r_bytes + sig_s_bytes
132
+ try:
133
+ vk.verify_digest(signature, data_bytes)
134
+ except BadSignatureError:
135
+ return False
136
+ return True
137
+
138
+
139
+ def ecdsa_pk_recover(
140
+ v: ECDSA, a: Bytes | bytes, b: UInt64 | int, c: Bytes | bytes, d: Bytes | bytes, /
141
+ ) -> tuple[Bytes, Bytes]:
142
+ if v is not ECDSA.Secp256k1:
143
+ raise ValueError(f"Unsupported ECDSA curve: {v}")
144
+
145
+ data_bytes = as_bytes(a)
146
+ r_bytes = as_bytes(c)
147
+ s_bytes = as_bytes(d)
148
+ recovery_id = int(b)
149
+
150
+ if len(r_bytes) != 32 or len(s_bytes) != 32:
151
+ raise ValueError("Invalid length for r or s bytes.")
152
+
153
+ signature_rs = r_bytes + s_bytes + bytes([recovery_id])
154
+
155
+ try:
156
+ public_key = coincurve.PublicKey.from_signature_and_message(
157
+ signature_rs, data_bytes, hasher=None
158
+ )
159
+ pubkey_x, pubkey_y = public_key.point()
160
+ except Exception as e:
161
+ raise ValueError(f"Failed to recover public key: {e}") from e
162
+ else:
163
+ return Bytes(pubkey_x.to_bytes(32, byteorder="big")), Bytes(
164
+ pubkey_y.to_bytes(32, byteorder="big")
165
+ )
166
+
167
+
168
+ def ecdsa_pk_decompress(v: ECDSA, a: Bytes | bytes, /) -> tuple[Bytes, Bytes]:
169
+ if v not in [ECDSA.Secp256k1, ECDSA.Secp256r1]:
170
+ raise ValueError(f"Unsupported ECDSA curve: {v}")
171
+
172
+ compressed_pubkey = as_bytes(a)
173
+
174
+ try:
175
+ public_key = coincurve.PublicKey(compressed_pubkey)
176
+ pubkey_x, pubkey_y = public_key.point()
177
+ except Exception as e:
178
+ raise ValueError(f"Failed to decompress public key: {e}") from e
179
+ else:
180
+ return Bytes(pubkey_x.to_bytes(32, byteorder="big")), Bytes(
181
+ pubkey_y.to_bytes(32, byteorder="big")
182
+ )
183
+
184
+
185
+ def vrf_verify(
186
+ _s: VrfVerify,
187
+ _a: Bytes | bytes,
188
+ _b: Bytes | bytes,
189
+ _c: Bytes | bytes,
190
+ /,
191
+ ) -> tuple[Bytes, bool]:
192
+ raise NotImplementedError(
193
+ "'op.vrf_verify' is not implemented. Mock using preferred testing tools."
194
+ )
195
+
196
+
197
+ def addw(a: UInt64 | int, b: UInt64 | int, /) -> tuple[UInt64, UInt64]:
198
+ a = as_int64(a)
199
+ b = as_int64(b)
200
+ result = a + b
201
+ return _int_to_uint128(result)
202
+
203
+
204
+ def base64_decode(e: Base64, a: Bytes | bytes, /) -> Bytes:
205
+ a_str = _bytes_to_string(a, "illegal base64 data")
206
+ a_str = a_str + "=" # append padding to ensure there is at least one
207
+
208
+ result = (
209
+ base64.urlsafe_b64decode(a_str) if e == Base64.URLEncoding else base64.b64decode(a_str)
210
+ )
211
+ return Bytes(result)
212
+
213
+
214
+ def bitlen(a: Bytes | UInt64 | bytes | int, /) -> UInt64:
215
+ int_value = int.from_bytes(as_bytes(a)) if (isinstance(a, Bytes | bytes)) else as_int64(a)
216
+ return UInt64(int_value.bit_length())
217
+
218
+
219
+ def bsqrt(a: BigUInt | int, /) -> BigUInt:
220
+ a = as_int512(a)
221
+ return BigUInt(math.isqrt(a))
222
+
223
+
224
+ def btoi(a: Bytes | bytes, /) -> UInt64:
225
+ a_bytes = as_bytes(a)
226
+ if len(a_bytes) > 8:
227
+ raise ValueError(f"btoi arg too long, got [{len(a_bytes)}]bytes")
228
+ return UInt64(int.from_bytes(a_bytes))
229
+
230
+
231
+ def bzero(a: UInt64 | int, /) -> Bytes:
232
+ a = as_int64(a)
233
+ if a > MAX_BYTES_SIZE:
234
+ raise ValueError("bzero attempted to create a too large string")
235
+ return Bytes(b"\x00" * a)
236
+
237
+
238
+ def divmodw(
239
+ a: UInt64 | int, b: UInt64 | int, c: UInt64 | int, d: UInt64 | int, /
240
+ ) -> tuple[UInt64, UInt64, UInt64, UInt64]:
241
+ i = _uint128_to_int(a, b)
242
+ j = _uint128_to_int(c, d)
243
+ d = i // j
244
+ m = i % j
245
+ return _int_to_uint128(d) + _int_to_uint128(m)
246
+
247
+
248
+ def divw(a: UInt64 | int, b: UInt64 | int, c: UInt64 | int, /) -> UInt64:
249
+ i = _uint128_to_int(a, b)
250
+ c = as_int64(c)
251
+ return UInt64(i // c)
252
+
253
+
254
+ def err() -> None:
255
+ raise RuntimeError("err opcode executed")
256
+
257
+
258
+ def exp(a: UInt64 | int, b: UInt64 | int, /) -> UInt64:
259
+ a = as_int64(a)
260
+ b = as_int64(b)
261
+ if a == b and a == 0:
262
+ raise ArithmeticError("0^0 is undefined")
263
+ return UInt64(a**b)
264
+
265
+
266
+ def expw(a: UInt64 | int, b: UInt64 | int, /) -> tuple[UInt64, UInt64]:
267
+ a = as_int64(a)
268
+ b = as_int64(b)
269
+ if a == b and a == 0:
270
+ raise ArithmeticError("0^0 is undefined")
271
+ result = a**b
272
+ return _int_to_uint128(result)
273
+
274
+
275
+ def extract(a: Bytes | bytes, b: UInt64 | int, c: UInt64 | int, /) -> Bytes:
276
+ a = as_bytes(a)
277
+ start = as_int64(b)
278
+ stop = start + as_int64(c)
279
+
280
+ if isinstance(b, int) and isinstance(c, int) and c == 0:
281
+ stop = len(a)
282
+
283
+ if start > len(a):
284
+ raise ValueError(f"extraction start {start} is beyond length")
285
+ if stop > len(a):
286
+ raise ValueError(f"extraction end {stop} is beyond length")
287
+
288
+ return Bytes(a)[slice(start, stop)]
289
+
290
+
291
+ def extract_uint16(a: Bytes | bytes, b: UInt64 | int, /) -> UInt64:
292
+ result = extract(a, b, 2)
293
+ result_int = int.from_bytes(result.value)
294
+ return UInt64(result_int)
295
+
296
+
297
+ def extract_uint32(a: Bytes | bytes, b: UInt64 | int, /) -> UInt64:
298
+ result = extract(a, b, 4)
299
+ result_int = int.from_bytes(result.value)
300
+ return UInt64(result_int)
301
+
302
+
303
+ def extract_uint64(a: Bytes | bytes, b: UInt64 | int, /) -> UInt64:
304
+ result = extract(a, b, 8)
305
+ result_int = int.from_bytes(result.value)
306
+ return UInt64(result_int)
307
+
308
+
309
+ def getbit(a: Bytes | UInt64 | bytes | int, b: UInt64 | int, /) -> UInt64:
310
+ if isinstance(a, Bytes | bytes):
311
+ return _getbit_bytes(a, b)
312
+ if isinstance(a, UInt64 | int):
313
+ a_bytes = _uint64_to_bytes(a)
314
+ return _getbit_bytes(a_bytes, b, "little")
315
+ raise TypeError("Unknown type for argument a")
316
+
317
+
318
+ def getbyte(a: Bytes | bytes, b: UInt64 | int, /) -> UInt64:
319
+ a = as_bytes(a)
320
+ int_list = list(a)
321
+
322
+ max_index = len(int_list) - 1
323
+ b = as_int(b, max=max_index)
324
+
325
+ return UInt64(int_list[b])
326
+
327
+
328
+ def itob(a: UInt64 | int, /) -> Bytes:
329
+ return Bytes(_uint64_to_bytes(a))
330
+
331
+
332
+ def mulw(a: UInt64 | int, b: UInt64 | int, /) -> tuple[UInt64, UInt64]:
333
+ a = as_int64(a)
334
+ b = as_int64(b)
335
+ result = a * b
336
+ return _int_to_uint128(result)
337
+
338
+
339
+ def replace(a: Bytes | bytes, b: UInt64 | int, c: Bytes | bytes, /) -> Bytes:
340
+ a = a if (isinstance(a, Bytes)) else Bytes(a)
341
+ b = as_int64(b)
342
+ c = as_bytes(c)
343
+ if b + len(c) > len(a):
344
+ raise ValueError(f"expected value <= {len(a)}, got: {b + len(c)}")
345
+ return a[slice(0, b)] + c + a[slice(b + len(c), len(a))]
346
+
347
+
348
+ def select_bytes(a: Bytes | bytes, b: Bytes | bytes, c: bool | UInt64 | int, /) -> Bytes:
349
+ a = as_bytes(a)
350
+ b = as_bytes(b)
351
+ c = int(c) if (isinstance(c, bool)) else as_int64(c)
352
+ return Bytes(b if c != 0 else a)
353
+
354
+
355
+ def select_uint64(a: UInt64 | int, b: UInt64 | int, c: bool | UInt64 | int, /) -> UInt64:
356
+ a = as_int64(a)
357
+ b = as_int64(b)
358
+ c = int(c) if (isinstance(c, bool)) else as_int64(c)
359
+ return UInt64(b if c != 0 else a)
360
+
361
+
362
+ def setbit_bytes(a: Bytes | bytes, b: UInt64 | int, c: UInt64 | int, /) -> Bytes:
363
+ return _setbit_bytes(a, b, c)
364
+
365
+
366
+ def setbit_uint64(a: UInt64 | int, b: UInt64 | int, c: UInt64 | int, /) -> UInt64:
367
+ a_bytes = _uint64_to_bytes(a)
368
+ result = _setbit_bytes(a_bytes, b, c, "little")
369
+ return UInt64(int.from_bytes(result.value))
370
+
371
+
372
+ def setbyte(a: Bytes | bytes, b: UInt64 | int, c: UInt64 | int, /) -> Bytes:
373
+ a = as_bytes(a)
374
+ int_list = list(a)
375
+
376
+ max_index = len(int_list) - 1
377
+ b = as_int(b, max=max_index)
378
+ c = as_int8(c)
379
+
380
+ int_list[b] = c
381
+ return Bytes(_int_list_to_bytes(int_list))
382
+
383
+
384
+ def shl(a: UInt64 | int, b: UInt64 | int, /) -> UInt64:
385
+ a = as_int64(a)
386
+ b = as_int(b, max=63)
387
+ result = (a * (2**b)) % (2**64)
388
+ return UInt64(result)
389
+
390
+
391
+ def shr(a: UInt64 | int, b: UInt64 | int, /) -> UInt64:
392
+ a = as_int64(a)
393
+ b = as_int(b, max=63)
394
+ result = a // (2**b)
395
+ return UInt64(result)
396
+
397
+
398
+ def sqrt(a: UInt64 | int, /) -> UInt64:
399
+ a = as_int64(a)
400
+ return UInt64(math.isqrt(a))
401
+
402
+
403
+ def substring(a: Bytes | bytes, b: UInt64 | int, c: UInt64 | int, /) -> Bytes:
404
+ a = as_bytes(a)
405
+ c = as_int(c, max=len(a))
406
+ b = as_int(b, max=c)
407
+ return Bytes(a)[slice(b, c)]
408
+
409
+
410
+ def concat(a: Bytes | bytes, b: Bytes | bytes, /) -> Bytes:
411
+ a = a if (isinstance(a, Bytes)) else Bytes(a)
412
+ b = b if (isinstance(b, Bytes)) else Bytes(b)
413
+ return a + b
414
+
415
+
416
+ def _int_to_uint128(a: int) -> tuple[UInt64, UInt64]:
417
+ cf, rest = a >> 64, a & MAX_UINT64
418
+ return (
419
+ UInt64(cf),
420
+ UInt64(rest),
421
+ )
422
+
423
+
424
+ def _uint128_to_int(a: UInt64 | int, b: UInt64 | int) -> int:
425
+ a = as_int64(a)
426
+ b = as_int64(b)
427
+ return (a << 64) + b
428
+
429
+
430
+ def _uint64_to_bytes(a: UInt64 | int) -> bytes:
431
+ a = as_int64(a)
432
+ return a.to_bytes(8)
433
+
434
+
435
+ def _int_list_to_bytes(a: list[int]) -> bytes:
436
+ return b"".join([b"\x00" if i == 0 else int_to_bytes(i) for i in a])
437
+
438
+
439
+ def _getbit_bytes(
440
+ a: Bytes | bytes, b: UInt64 | int, byteorder: Literal["little", "big"] = "big"
441
+ ) -> UInt64:
442
+ a = as_bytes(a)
443
+ if byteorder != "big": # reverse bytes if NOT big endian
444
+ a = bytes(reversed(a))
445
+
446
+ int_list = list(a)
447
+ max_index = len(int_list) * BITS_IN_BYTE - 1
448
+ b = as_int(b, max=max_index)
449
+
450
+ byte_index = b // BITS_IN_BYTE
451
+ bit_index = b % BITS_IN_BYTE
452
+ if byteorder == "big":
453
+ bit_index = 7 - bit_index
454
+ bit = _get_bit(int_list[byte_index], bit_index)
455
+
456
+ return UInt64(bit)
457
+
458
+
459
+ def _setbit_bytes(
460
+ a: Bytes | bytes, b: UInt64 | int, c: UInt64 | int, byteorder: Literal["little", "big"] = "big"
461
+ ) -> Bytes:
462
+ a = as_bytes(a)
463
+ if byteorder != "big": # reverse bytes if NOT big endian
464
+ a = bytes(reversed(a))
465
+
466
+ int_list = list(a)
467
+ max_index = len(int_list) * BITS_IN_BYTE - 1
468
+ b = as_int(b, max=max_index)
469
+ c = as_int(c, max=1)
470
+
471
+ byte_index = b // BITS_IN_BYTE
472
+ bit_index = b % BITS_IN_BYTE
473
+ if byteorder == "big":
474
+ bit_index = 7 - bit_index
475
+ int_list[byte_index] = _set_bit(int_list[byte_index], bit_index, c)
476
+
477
+ # reverse int array if NOT big endian before casting it to Bytes
478
+ if byteorder != "big":
479
+ int_list = list(reversed(int_list))
480
+
481
+ return Bytes(_int_list_to_bytes(int_list))
482
+
483
+
484
+ def _get_bit(v: int, index: int) -> int:
485
+ return (v >> index) & 1
486
+
487
+
488
+ def _set_bit(v: int, index: int, x: int) -> int:
489
+ """Set the index:th bit of v to 1 if x is truthy, else to 0, and return the new value."""
490
+ mask = 1 << index # Compute mask, an integer with just bit 'index' set.
491
+ v &= ~mask # Clear the bit indicated by the mask (if x is False)
492
+ if x:
493
+ v |= mask # If x was True, set the bit indicated by the mask.
494
+ return v
495
+
496
+
497
+ def _bytes_to_string(a: Bytes | bytes, err_msg: str) -> str:
498
+ a = as_bytes(a)
499
+ try:
500
+ return a.decode()
501
+ except UnicodeDecodeError:
502
+ raise ValueError(err_msg) from None
503
+
504
+
505
+ class JsonRef:
506
+ @staticmethod
507
+ def _load_json(a: Bytes | bytes) -> dict[Any, Any]:
508
+ a = as_bytes(a)
509
+ try:
510
+ # load the whole json payload as an array of key value pairs
511
+ pairs = json.loads(a, object_pairs_hook=lambda x: x)
512
+ except json.JSONDecodeError:
513
+ raise ValueError("error while parsing JSON text, invalid json text") from None
514
+
515
+ # turn the pairs into the dictionay for the top level,
516
+ # all other levels remain as key value pairs
517
+ # e.g.
518
+ # input bytes: b'{"key0": 1,"key1": {"key2":2,"key2":"10"}, "key2": "test"}'
519
+ # output dict: {'key0': 1, 'key1': [('key2', 2), ('key2', '10')], 'key2': 'test'}
520
+ result = dict(pairs)
521
+ if len(pairs) != len(result):
522
+ raise ValueError(
523
+ "error while parsing JSON text, invalid json text, duplicate keys found"
524
+ )
525
+
526
+ return result
527
+
528
+ @staticmethod
529
+ def _raise_key_error(key: str) -> None:
530
+ raise ValueError(f"key {key} not found in JSON text")
531
+
532
+ @staticmethod
533
+ def json_string(a: Bytes | bytes, b: Bytes | bytes, /) -> Bytes:
534
+ b_str = _bytes_to_string(b, "can't decode bytes as string")
535
+ obj = JsonRef._load_json(a)
536
+ result = None
537
+
538
+ try:
539
+ result = obj[b_str]
540
+ except KeyError:
541
+ JsonRef._raise_key_error(b_str)
542
+
543
+ if not isinstance(result, str):
544
+ raise TypeError(f"value must be a string type, not {type(result).__name__!r}")
545
+
546
+ # encode with `surrogatepass` to allow sequences such as `\uD800`
547
+ # decode with `replace` to replace with official replacement character `U+FFFD`
548
+ # encode with default settings to get the final bytes result
549
+ result = result.encode("utf-16", "surrogatepass").decode("utf-16", "replace").encode()
550
+ return Bytes(result)
551
+
552
+ @staticmethod
553
+ def json_uint64(a: Bytes | bytes, b: Bytes | bytes, /) -> UInt64:
554
+ b_str = _bytes_to_string(b, "can't decode bytes as string")
555
+ obj = JsonRef._load_json(a)
556
+ result = None
557
+
558
+ try:
559
+ result = obj[b_str]
560
+ except KeyError:
561
+ JsonRef._raise_key_error(b_str)
562
+
563
+ result = as_int(result, max=MAX_UINT64)
564
+ return UInt64(result)
565
+
566
+ @staticmethod
567
+ def json_object(a: Bytes | bytes, b: Bytes | bytes, /) -> Bytes:
568
+ b_str = _bytes_to_string(b, "can't decode bytes as string")
569
+ obj = JsonRef._load_json(a)
570
+ result = None
571
+ try:
572
+ # using a custom dict object to allow duplicate keys which is essentially a list
573
+ result = obj[b_str]
574
+ except KeyError:
575
+ JsonRef._raise_key_error(b_str)
576
+
577
+ if not isinstance(result, list) or not all(isinstance(i, tuple) for i in result):
578
+ raise TypeError(f"value must be an object type, not {type(result).__name__!r}")
579
+
580
+ result = _MultiKeyDict(result)
581
+ result_string = json.dumps(result, separators=(",", ":"))
582
+ return Bytes(result_string.encode())
583
+
584
+
585
+ class Scratch:
586
+ @staticmethod
587
+ def load_bytes(a: UInt64 | int, /) -> Bytes:
588
+ from algopy_testing import get_test_context
589
+
590
+ context = get_test_context()
591
+ active_txn = context.get_active_transaction()
592
+ if not active_txn:
593
+ raise ValueError("No active transaction found to reference scratch space")
594
+
595
+ slot_content = context._scratch_spaces[str(active_txn.txn_id)][a]
596
+ match slot_content:
597
+ case Bytes():
598
+ return slot_content
599
+ case bytes():
600
+ return Bytes(slot_content)
601
+ case UInt64() | int():
602
+ return itob(slot_content)
603
+ case _:
604
+ raise ValueError(f"Invalid scratch space type: {type(slot_content)}")
605
+
606
+ @staticmethod
607
+ def load_uint64(a: UInt64 | int, /) -> UInt64:
608
+ from algopy_testing import get_test_context
609
+
610
+ context = get_test_context()
611
+ active_txn = context.get_active_transaction()
612
+ if not active_txn:
613
+ raise ValueError("No active transaction found to reference scratch space")
614
+
615
+ slot_content = context._scratch_spaces[str(active_txn.txn_id)][a]
616
+ match slot_content:
617
+ case Bytes() | bytes():
618
+ return btoi(slot_content)
619
+ case UInt64():
620
+ return slot_content
621
+ case int():
622
+ return UInt64(slot_content)
623
+ case _:
624
+ raise ValueError(f"Invalid scratch space type: {type(slot_content)}")
625
+
626
+ @staticmethod
627
+ def store(a: UInt64 | int, b: Bytes | UInt64 | bytes | int, /) -> None:
628
+ from algopy_testing import get_test_context
629
+
630
+ context = get_test_context()
631
+ active_txn = context.get_active_transaction()
632
+ if not active_txn:
633
+ raise ValueError("No active transaction found to reference scratch space")
634
+
635
+ context._scratch_spaces[str(active_txn.txn_id)][a] = b
636
+
637
+
638
+ class _MultiKeyDict(dict[Any, Any]):
639
+ def __init__(self, items: list[Any]):
640
+ self[""] = ""
641
+ items = [
642
+ (
643
+ (i[0], _MultiKeyDict(i[1]))
644
+ if isinstance(i[1], list) and all(isinstance(j, tuple) for j in i[1])
645
+ else i
646
+ )
647
+ for i in items
648
+ ]
649
+ self._items = items
650
+
651
+ def items(self) -> Any:
652
+ return self._items
653
+
654
+
655
+ def gload_uint64(a: UInt64 | int, b: UInt64 | int, /) -> UInt64:
656
+ from algopy_testing import get_test_context
657
+
658
+ context = get_test_context()
659
+ txn_group = context.get_transaction_group()
660
+ if not txn_group:
661
+ raise ValueError("No transaction group found to reference scratch space")
662
+ if a >= len(txn_group):
663
+ raise ValueError(f"Index {a} out of range for transaction group")
664
+ txn = txn_group[a]
665
+ slot_content = context._scratch_spaces[str(txn.txn_id)][int(b)]
666
+ match slot_content:
667
+ case Bytes() | bytes():
668
+ return btoi(slot_content)
669
+ case int():
670
+ return UInt64(slot_content)
671
+ case UInt64():
672
+ return slot_content
673
+ case _:
674
+ raise ValueError(f"Invalid scratch space type: {type(slot_content)}")
675
+
676
+
677
+ def gload_bytes(a: algopy.UInt64 | int, b: algopy.UInt64 | int, /) -> algopy.Bytes:
678
+ import algopy
679
+
680
+ from algopy_testing import get_test_context
681
+
682
+ context = get_test_context()
683
+ txn_group = context.get_transaction_group()
684
+ if not txn_group:
685
+ raise ValueError("No transaction group found to reference scratch space")
686
+ if a >= len(txn_group):
687
+ raise ValueError(f"Index {a} out of range for transaction group")
688
+ txn = txn_group[a]
689
+ slot_content = context._scratch_spaces[str(txn.txn_id)][int(b)]
690
+ match slot_content:
691
+ case algopy.Bytes():
692
+ return slot_content
693
+ case bytes():
694
+ return algopy.Bytes(slot_content)
695
+ case int() | algopy.UInt64():
696
+ return itob(slot_content)
697
+ case _:
698
+ raise ValueError(f"Invalid scratch space type: {type(slot_content)}")
699
+
700
+
701
+ def gaid(a: UInt64 | int, /) -> algopy.Application:
702
+ import algopy
703
+
704
+ from algopy_testing import get_test_context
705
+
706
+ context = get_test_context()
707
+ txn_group = context.get_transaction_group()
708
+
709
+ if not txn_group:
710
+ raise ValueError("No transaction group found to reference gaid")
711
+
712
+ a = int(a)
713
+ if a >= len(txn_group):
714
+ raise ValueError(f"Index {a} out of range for transaction group")
715
+
716
+ txn = txn_group[a]
717
+
718
+ if not txn.type == algopy.TransactionType.ApplicationCall:
719
+ raise TypeError(f"Transaction at index {a} is not an ApplicationCallTransaction")
720
+
721
+ app_id = txn.created_application_id
722
+ if app_id is None:
723
+ raise ValueError(f"Transaction at index {a} did not create an application")
724
+
725
+ return context.get_application(cast(int, app_id))
726
+
727
+
728
+ def balance(a: algopy.Account | algopy.UInt64 | int, /) -> algopy.UInt64:
729
+ import algopy
730
+
731
+ from algopy_testing.context import get_test_context
732
+
733
+ context = get_test_context()
734
+ if not context:
735
+ raise ValueError(
736
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
737
+ "the context manager."
738
+ )
739
+
740
+ active_txn = context.get_active_transaction()
741
+ if not active_txn:
742
+ raise ValueError("No active transaction found to reference account")
743
+
744
+ if isinstance(a, algopy.Account):
745
+ account = a
746
+ elif isinstance(a, (algopy.UInt64 | int)):
747
+ index = int(a)
748
+ if index == 0:
749
+ account = active_txn.sender
750
+ else:
751
+ accounts = getattr(active_txn, "accounts", None)
752
+ if not accounts or index >= len(accounts):
753
+ raise ValueError(f"Invalid account index: {index}")
754
+ account = accounts[index]
755
+ else:
756
+ raise TypeError("Invalid type for account parameter")
757
+
758
+ account_data = context._account_data.get(str(account))
759
+ if not account_data:
760
+ raise ValueError(f"Account {account} not found in testing context!")
761
+
762
+ balance = account_data.fields.get("balance")
763
+ if balance is None:
764
+ raise ValueError(f"Balance not set for account {account}")
765
+
766
+ # Deduct the fee for the current transaction
767
+ if account == active_txn.sender:
768
+ fee = getattr(active_txn, "fee", algopy.UInt64(0))
769
+ balance = algopy.UInt64(int(balance) - int(fee))
770
+
771
+ return balance
772
+
773
+
774
+ def min_balance(a: algopy.Account | algopy.UInt64 | int, /) -> algopy.UInt64:
775
+ import algopy
776
+
777
+ from algopy_testing.context import get_test_context
778
+
779
+ context = get_test_context()
780
+ if not context:
781
+ raise ValueError("Test context is not initialized!")
782
+
783
+ active_txn = context.get_active_transaction()
784
+ if not active_txn:
785
+ raise ValueError("No active transaction found to reference account")
786
+
787
+ if isinstance(a, algopy.Account):
788
+ account = a
789
+ elif isinstance(a, (algopy.UInt64 | int)):
790
+ index = int(a)
791
+ if index == 0:
792
+ account = active_txn.sender
793
+ else:
794
+ accounts = getattr(active_txn, "accounts", None)
795
+ if not accounts or index >= len(accounts):
796
+ raise ValueError(f"Invalid account index: {index}")
797
+ account = accounts[index]
798
+ else:
799
+ raise TypeError("Invalid type for account parameter")
800
+
801
+ account_data = context._account_data.get(str(account))
802
+ if not account_data:
803
+ raise ValueError(f"Account {account} not found in testing context!")
804
+
805
+ # Return the pre-set min_balance if available, otherwise use a default value
806
+ return account_data.fields.get("min_balance", UInt64(DEFAULT_ACCOUNT_MIN_BALANCE))
807
+
808
+
809
+ def exit(a: UInt64 | int, /) -> typing.Never: # noqa: A001
810
+ value = UInt64(a) if isinstance(a, int) else a
811
+ raise SystemExit(int(value))
812
+
813
+
814
+ def app_opted_in(
815
+ a: algopy.Account | algopy.UInt64 | int, b: algopy.Application | algopy.UInt64 | int, /
816
+ ) -> bool:
817
+ import algopy
818
+
819
+ from algopy_testing.context import get_test_context
820
+
821
+ context = get_test_context()
822
+ active_txn = context.get_active_transaction()
823
+
824
+ if not active_txn:
825
+ raise ValueError("No active transaction found to reference account")
826
+
827
+ # Resolve account
828
+ if isinstance(a, (algopy.UInt64 | int)):
829
+ index = int(a)
830
+ account = active_txn.sender if index == 0 else active_txn.accounts[index]
831
+ else:
832
+ account = a
833
+
834
+ # Resolve application
835
+ if isinstance(b, (algopy.UInt64 | int)):
836
+ index = int(b)
837
+ app_id = active_txn.application_id if index == 0 else active_txn.foreign_apps[index]
838
+ else:
839
+ app_id = b.id
840
+
841
+ # Check if account is opted in to the application
842
+ account_data = context._account_data.get(str(account))
843
+ if not account_data:
844
+ return False
845
+
846
+ return app_id in account_data.opted_apps
847
+
848
+
849
+ class _AssetParamsGet:
850
+ def __getattr__(
851
+ self, name: str
852
+ ) -> typing.Callable[[algopy.Asset | algopy.UInt64 | int], tuple[typing.Any, bool]]:
853
+ def get_asset_param(a: algopy.Asset | algopy.UInt64 | int) -> tuple[typing.Any, bool]:
854
+ import algopy
855
+
856
+ from algopy_testing.context import get_test_context
857
+
858
+ context = get_test_context()
859
+ if not context:
860
+ raise ValueError(
861
+ "Test context is not initialized! Use `with algopy_testing_context()` to "
862
+ "access the context manager."
863
+ )
864
+
865
+ active_txn = context.get_active_transaction()
866
+ if not active_txn:
867
+ raise ValueError("No active transaction found to reference asset")
868
+
869
+ asset_id = a.value if isinstance(a, (algopy.Asset)) else int(a)
870
+ asset_data = active_txn.assets[asset_id]
871
+
872
+ if asset_data is None:
873
+ return None, False
874
+
875
+ param = "config_" + name
876
+ value = getattr(asset_data, param, None)
877
+ return value, True
878
+
879
+ if name.startswith("asset_"):
880
+ return get_asset_param
881
+ else:
882
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
883
+
884
+
885
+ AssetParamsGet = _AssetParamsGet()
886
+
887
+
888
+ class AssetHoldingGet:
889
+ @staticmethod
890
+ def _get_asset_holding(
891
+ account: algopy.Account | algopy.UInt64 | int,
892
+ asset: algopy.Asset | algopy.UInt64 | int,
893
+ field: str,
894
+ ) -> tuple[typing.Any, bool]:
895
+ import algopy
896
+
897
+ from algopy_testing.context import get_test_context
898
+
899
+ context = get_test_context()
900
+ if not context:
901
+ raise ValueError(
902
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
903
+ "the context manager."
904
+ )
905
+
906
+ active_txn = context.get_active_transaction()
907
+ if not active_txn:
908
+ raise ValueError("No active transaction found to reference account or asset")
909
+
910
+ # Resolve account
911
+ if isinstance(account, (algopy.UInt64 | int)):
912
+ index = int(account)
913
+ account = active_txn.sender if index == 0 else active_txn.accounts[index]
914
+
915
+ # Resolve asset
916
+ if isinstance(asset, (algopy.UInt64 | int)):
917
+ index = int(asset)
918
+ asset_id = active_txn.assets[index]
919
+ else:
920
+ asset_id = asset.id
921
+
922
+ account_data = context._account_data.get(str(account))
923
+ if not account_data:
924
+ return None, False
925
+
926
+ asset_balance = account_data.opted_asset_balances.get(asset_id)
927
+ if asset_balance is None:
928
+ return None, False
929
+
930
+ if field == "balance":
931
+ return asset_balance, True
932
+ elif field == "frozen":
933
+ asset_data = context._asset_data.get(int(asset_id))
934
+ if not asset_data:
935
+ return None, False
936
+ return asset_data["default_frozen"], True
937
+ else:
938
+ raise ValueError(f"Invalid asset holding field: {field}")
939
+
940
+ @staticmethod
941
+ def asset_balance(
942
+ a: algopy.Account | algopy.UInt64 | int, b: algopy.Asset | algopy.UInt64 | int, /
943
+ ) -> tuple[algopy.UInt64, bool]:
944
+ import algopy
945
+
946
+ balance, exists = AssetHoldingGet._get_asset_holding(a, b, "balance")
947
+ return algopy.UInt64(balance) if exists else algopy.UInt64(0), exists
948
+
949
+ @staticmethod
950
+ def asset_frozen(
951
+ a: algopy.Account | algopy.UInt64 | int, b: algopy.Asset | algopy.UInt64 | int, /
952
+ ) -> tuple[bool, bool]:
953
+ frozen, exists = AssetHoldingGet._get_asset_holding(a, b, "frozen")
954
+ return bool(frozen), exists
955
+
956
+
957
+ class _AppParamsGet:
958
+ def __getattr__(self, name: str) -> Any:
959
+ raise NotImplementedError(
960
+ f"AppParamsGet.{name} is currently not available as a native "
961
+ "`algorand-python-testing` type. Use your own preferred testing "
962
+ "framework of choice to mock the behaviour."
963
+ )
964
+
965
+
966
+ AppParamsGet = _AppParamsGet()
967
+
968
+
969
+ class _AppLocal:
970
+ def __getattr__(self, name: str) -> Any:
971
+ raise NotImplementedError(
972
+ f"AppLocal.{name} is currently not available as a native "
973
+ "`algorand-python-testing` type. Use your own preferred testing "
974
+ "framework of choice to mock the behaviour."
975
+ )
976
+
977
+
978
+ AppLocal = _AppLocal()
979
+
980
+
981
+ class _AppGlobal:
982
+ def __getattr__(self, name: str) -> Any:
983
+ raise NotImplementedError(
984
+ f"AppGlobal.{name} is currently not available as a native "
985
+ "`algorand-python-testing` type. Use your own preferred testing "
986
+ "framework of choice to mock the behaviour."
987
+ )
988
+
989
+
990
+ AppGlobal = _AppGlobal()
991
+
992
+
993
+ class _AcctParamsGet:
994
+ def __getattr__(self, name: str) -> Any:
995
+ raise NotImplementedError(
996
+ f"AcctParamsGet.{name} is currently not available as a native "
997
+ "`algorand-python-testing` type. Use your own preferred testing "
998
+ "framework of choice to mock the behaviour."
999
+ )
1000
+
1001
+
1002
+ AcctParamsGet = _AcctParamsGet()
1003
+
1004
+
1005
+ def arg(a: UInt64 | int, /) -> Bytes:
1006
+ from algopy_testing.context import get_test_context
1007
+
1008
+ context = get_test_context()
1009
+ if not context:
1010
+ raise ValueError("Test context is not initialized!")
1011
+
1012
+ return context._active_lsig_args[int(a)]
1013
+
1014
+
1015
+ class _EllipticCurve:
1016
+ def __getattr__(self, __name: str) -> Any:
1017
+ raise NotImplementedError(
1018
+ f"EllipticCurve.{__name} is currently not available as a native "
1019
+ "`algorand-python-testing` type. Use your own preferred testing "
1020
+ "framework of choice to mock the behaviour."
1021
+ )
1022
+
1023
+
1024
+ EllipticCurve = _EllipticCurve()
1025
+
1026
+ __all__ = [
1027
+ "AcctParamsGet",
1028
+ "AppGlobal",
1029
+ "AppLocal",
1030
+ "AppParamsGet",
1031
+ "AssetHoldingGet",
1032
+ "AssetParamsGet",
1033
+ "Base64",
1034
+ "BigUInt",
1035
+ "Block",
1036
+ "Box",
1037
+ "EC",
1038
+ "ECDSA",
1039
+ "EllipticCurve",
1040
+ "GITxn",
1041
+ "GTxn",
1042
+ "Global",
1043
+ "ITxn",
1044
+ "ITxnCreate",
1045
+ "JsonRef",
1046
+ "Scratch",
1047
+ "Txn",
1048
+ "UInt64",
1049
+ "VrfVerify",
1050
+ "addw",
1051
+ "arg",
1052
+ "app_opted_in",
1053
+ "balance",
1054
+ "base64_decode",
1055
+ "bitlen",
1056
+ "bsqrt",
1057
+ "btoi",
1058
+ "bzero",
1059
+ "concat",
1060
+ "divmodw",
1061
+ "divw",
1062
+ "ecdsa_pk_decompress",
1063
+ "ecdsa_pk_recover",
1064
+ "ecdsa_verify",
1065
+ "ed25519verify",
1066
+ "ed25519verify_bare",
1067
+ "err",
1068
+ "exit",
1069
+ "exp",
1070
+ "expw",
1071
+ "extract",
1072
+ "extract_uint16",
1073
+ "extract_uint32",
1074
+ "extract_uint64",
1075
+ "gaid",
1076
+ "getbit",
1077
+ "getbyte",
1078
+ "gload_bytes",
1079
+ "gload_uint64",
1080
+ "itob",
1081
+ "keccak256",
1082
+ "min_balance",
1083
+ "mulw",
1084
+ "replace",
1085
+ "select_bytes",
1086
+ "select_uint64",
1087
+ "setbit_bytes",
1088
+ "setbit_uint64",
1089
+ "setbyte",
1090
+ "sha256",
1091
+ "sha3_256",
1092
+ "sha512_256",
1093
+ "shl",
1094
+ "shr",
1095
+ "sqrt",
1096
+ "substring",
1097
+ "vrf_verify",
1098
+ ]