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,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Self, TypedDict, TypeVar
|
|
5
|
+
|
|
6
|
+
import algosdk
|
|
7
|
+
|
|
8
|
+
from algopy_testing.primitives.bytes import Bytes
|
|
9
|
+
from algopy_testing.utils import as_bytes
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import algopy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AccountFields(TypedDict, total=False):
|
|
19
|
+
balance: algopy.UInt64
|
|
20
|
+
min_balance: algopy.UInt64
|
|
21
|
+
auth_address: algopy.Account
|
|
22
|
+
total_num_uint: algopy.UInt64
|
|
23
|
+
total_num_byte_slice: algopy.Bytes
|
|
24
|
+
total_extra_app_pages: algopy.UInt64
|
|
25
|
+
total_apps_created: algopy.UInt64
|
|
26
|
+
total_apps_opted_in: algopy.UInt64
|
|
27
|
+
total_assets_created: algopy.UInt64
|
|
28
|
+
total_assets: algopy.UInt64
|
|
29
|
+
total_boxes: algopy.UInt64
|
|
30
|
+
total_box_bytes: algopy.UInt64
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass()
|
|
34
|
+
class Account:
|
|
35
|
+
_public_key: bytes
|
|
36
|
+
|
|
37
|
+
def __init__(self, value: str | Bytes = algosdk.constants.ZERO_ADDRESS, /):
|
|
38
|
+
if not isinstance(value, (str | Bytes)):
|
|
39
|
+
raise TypeError("Invalid value for Account")
|
|
40
|
+
|
|
41
|
+
public_key = (
|
|
42
|
+
algosdk.encoding.decode_address(value) if isinstance(value, str) else value.value
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._public_key = public_key
|
|
46
|
+
|
|
47
|
+
def is_opted_in(self, asset_or_app: algopy.Asset | algopy.Application, /) -> bool:
|
|
48
|
+
from algopy import Application, Asset
|
|
49
|
+
|
|
50
|
+
from algopy_testing import get_test_context
|
|
51
|
+
|
|
52
|
+
context = get_test_context()
|
|
53
|
+
opted_apps = context._account_data[str(self)].opted_apps
|
|
54
|
+
opted_asset_balances = context._account_data[str(self)].opted_asset_balances
|
|
55
|
+
|
|
56
|
+
if not context:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
59
|
+
"the context manager."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if isinstance(asset_or_app, Asset):
|
|
63
|
+
return asset_or_app.id in opted_asset_balances
|
|
64
|
+
elif isinstance(asset_or_app, Application):
|
|
65
|
+
return asset_or_app.id in opted_apps
|
|
66
|
+
|
|
67
|
+
raise TypeError(
|
|
68
|
+
"Invalid `asset_or_app` argument type. Must be an `algopy.Asset` or "
|
|
69
|
+
"`algopy.Application` instance."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_bytes(cls, value: algopy.Bytes | bytes) -> Self:
|
|
74
|
+
# NOTE: AVM does not perform any validation beyond type.
|
|
75
|
+
validated_value = as_bytes(value)
|
|
76
|
+
return cls(Bytes(validated_value))
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def bytes(self) -> Bytes:
|
|
80
|
+
return Bytes(self._public_key)
|
|
81
|
+
|
|
82
|
+
def __getattr__(self, name: str) -> object:
|
|
83
|
+
from algopy_testing.context import get_test_context
|
|
84
|
+
|
|
85
|
+
context = get_test_context()
|
|
86
|
+
if not context:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
89
|
+
"the context manager."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if str(self) not in context._account_data:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"`algopy.Account` is not present in the test context! "
|
|
95
|
+
"Use `context.add_account()` or `context.any_account()` to add the account "
|
|
96
|
+
"to your test setup."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return_value = context._account_data[str(self)].fields.get(name)
|
|
100
|
+
if return_value is None:
|
|
101
|
+
raise AttributeError(
|
|
102
|
+
f"The value for '{name}' in the test context is None. "
|
|
103
|
+
f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
|
|
104
|
+
"instance."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return return_value
|
|
108
|
+
|
|
109
|
+
def __repr__(self) -> str:
|
|
110
|
+
return str(algosdk.encoding.encode_address(self._public_key))
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return str(algosdk.encoding.encode_address(self._public_key))
|
|
114
|
+
|
|
115
|
+
def __eq__(self, other: object) -> bool:
|
|
116
|
+
if not isinstance(other, Account | str):
|
|
117
|
+
raise TypeError("Invalid value for Account")
|
|
118
|
+
if isinstance(other, Account):
|
|
119
|
+
return self._public_key == other._public_key
|
|
120
|
+
return self._public_key == as_bytes(other)
|
|
121
|
+
|
|
122
|
+
def __bool__(self) -> bool:
|
|
123
|
+
return bool(self._public_key) and self._public_key != algosdk.encoding.decode_address(
|
|
124
|
+
algosdk.constants.ZERO_ADDRESS
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def __hash__(self) -> int:
|
|
128
|
+
return hash(self._public_key)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, TypedDict, TypeVar
|
|
6
|
+
|
|
7
|
+
from algopy_testing.utils import as_int64
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import algopy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApplicationFields(TypedDict, total=False):
|
|
17
|
+
approval_program: algopy.Bytes
|
|
18
|
+
clear_state_program: algopy.Bytes
|
|
19
|
+
global_num_uint: algopy.UInt64
|
|
20
|
+
global_num_bytes: algopy.UInt64
|
|
21
|
+
local_num_uint: algopy.UInt64
|
|
22
|
+
local_num_bytes: algopy.UInt64
|
|
23
|
+
extra_program_pages: algopy.UInt64
|
|
24
|
+
creator: algopy.Account
|
|
25
|
+
address: algopy.Account
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass()
|
|
29
|
+
class Application:
|
|
30
|
+
id: algopy.UInt64
|
|
31
|
+
|
|
32
|
+
def __init__(self, application_id: algopy.UInt64 | int = 0, /):
|
|
33
|
+
from algopy import UInt64
|
|
34
|
+
|
|
35
|
+
self.id = application_id if isinstance(application_id, UInt64) else UInt64(application_id)
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, name: str) -> typing.Any:
|
|
38
|
+
from algopy_testing.context import get_test_context
|
|
39
|
+
|
|
40
|
+
context = get_test_context()
|
|
41
|
+
if not context:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
44
|
+
"the context manager."
|
|
45
|
+
)
|
|
46
|
+
if int(self.id) not in context._application_data:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"`algopy.Application` is not present in the test context! "
|
|
49
|
+
"Use `context.add_application()` or `context.any_application()` to add the "
|
|
50
|
+
"application to your test setup."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return_value = context._application_data[int(self.id)].get(name)
|
|
54
|
+
if return_value is None:
|
|
55
|
+
raise AttributeError(
|
|
56
|
+
f"The value for '{name}' in the test context is None. "
|
|
57
|
+
f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
|
|
58
|
+
"instance."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return return_value
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other: object) -> bool:
|
|
64
|
+
if isinstance(other, Application):
|
|
65
|
+
return self.id == other.id
|
|
66
|
+
return self.id == as_int64(other)
|
|
67
|
+
|
|
68
|
+
def __bool__(self) -> bool:
|
|
69
|
+
return self.id != 0
|
|
70
|
+
|
|
71
|
+
def __hash__(self) -> int:
|
|
72
|
+
return hash(self.id)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, TypedDict, TypeVar
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import algopy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AssetFields(TypedDict, total=False):
|
|
14
|
+
total: algopy.UInt64
|
|
15
|
+
decimals: algopy.UInt64
|
|
16
|
+
default_frozen: bool
|
|
17
|
+
unit_name: algopy.Bytes
|
|
18
|
+
name: algopy.Bytes
|
|
19
|
+
url: algopy.Bytes
|
|
20
|
+
metadata_hash: algopy.Bytes
|
|
21
|
+
manager: algopy.Account
|
|
22
|
+
reserve: algopy.Account
|
|
23
|
+
freeze: algopy.Account
|
|
24
|
+
clawback: algopy.Account
|
|
25
|
+
creator: algopy.Account
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Asset:
|
|
30
|
+
id: algopy.UInt64
|
|
31
|
+
|
|
32
|
+
def __init__(self, asset_id: algopy.UInt64 | int = 0):
|
|
33
|
+
from algopy import UInt64
|
|
34
|
+
|
|
35
|
+
self.id = asset_id if isinstance(asset_id, UInt64) else UInt64(asset_id)
|
|
36
|
+
|
|
37
|
+
def balance(self, account: algopy.Account) -> algopy.UInt64:
|
|
38
|
+
from algopy_testing.context import get_test_context
|
|
39
|
+
|
|
40
|
+
context = get_test_context()
|
|
41
|
+
if not context:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
44
|
+
"the context manager."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if account not in context._account_data:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"The account is not present in the test context! "
|
|
50
|
+
"Use `context.add_account()` or `context.any_account()` to add the account to "
|
|
51
|
+
"your test setup."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
account_data = context._account_data.get(str(account), None)
|
|
55
|
+
|
|
56
|
+
if not account_data:
|
|
57
|
+
raise ValueError("Account not found in testing context!")
|
|
58
|
+
|
|
59
|
+
if int(self.id) not in account_data.opted_asset_balances:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"The asset is not opted into the account! "
|
|
62
|
+
"Use `account.opt_in()` to opt the asset into the account."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return account_data.opted_asset_balances[self.id]
|
|
66
|
+
|
|
67
|
+
def frozen(self, _account: algopy.Account) -> bool:
|
|
68
|
+
raise NotImplementedError(
|
|
69
|
+
"The 'frozen' method is being executed in a python testing context. "
|
|
70
|
+
"Please mock this method using your python testing framework of choice."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def __getattr__(self, name: str) -> object:
|
|
74
|
+
from algopy_testing.context import get_test_context
|
|
75
|
+
|
|
76
|
+
context = get_test_context()
|
|
77
|
+
if not context:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
80
|
+
"the context manager."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if int(self.id) not in context._asset_data:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"`algopy.Asset` is not present in the test context! "
|
|
86
|
+
"Use `context.add_asset()` or `context.any_asset()` to add the asset to "
|
|
87
|
+
"your test setup."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return_value = context._asset_data[int(self.id)].get(name)
|
|
91
|
+
if return_value is None:
|
|
92
|
+
raise AttributeError(
|
|
93
|
+
f"The value for '{name}' in the test context is None. "
|
|
94
|
+
f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
|
|
95
|
+
"instance."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return return_value
|
|
99
|
+
|
|
100
|
+
def __eq__(self, other: object) -> bool:
|
|
101
|
+
if isinstance(other, Asset):
|
|
102
|
+
return self.id == other.id
|
|
103
|
+
return self.id == other
|
|
104
|
+
|
|
105
|
+
def __bool__(self) -> bool:
|
|
106
|
+
return self.id != 0
|
|
107
|
+
|
|
108
|
+
def __hash__(self) -> int:
|
|
109
|
+
return hash(self.id)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import algopy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Block:
|
|
10
|
+
@staticmethod
|
|
11
|
+
def blk_seed(a: algopy.UInt64 | int, /) -> algopy.Bytes:
|
|
12
|
+
from algopy_testing import get_test_context, op
|
|
13
|
+
|
|
14
|
+
context = get_test_context()
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
index = int(a)
|
|
18
|
+
return op.itob(context._blocks[index]["seed"])
|
|
19
|
+
except KeyError as e:
|
|
20
|
+
raise KeyError(f"Block {a} not set") from e
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def blk_timestamp(a: algopy.UInt64 | int, /) -> algopy.UInt64:
|
|
24
|
+
import algopy
|
|
25
|
+
|
|
26
|
+
from algopy_testing import get_test_context
|
|
27
|
+
|
|
28
|
+
context = get_test_context()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
index = int(a)
|
|
32
|
+
return algopy.UInt64(context._blocks[index]["timestamp"])
|
|
33
|
+
except KeyError as e:
|
|
34
|
+
raise KeyError(f"Block {a} not set") from e
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from algopy_testing.context import get_test_context
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import algopy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Box:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def create(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> bool:
|
|
14
|
+
import algopy
|
|
15
|
+
|
|
16
|
+
context = get_test_context()
|
|
17
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
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
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def delete(a: algopy.Bytes | bytes, /) -> bool:
|
|
28
|
+
import algopy
|
|
29
|
+
|
|
30
|
+
context = get_test_context()
|
|
31
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
32
|
+
if context.get_box(name_bytes):
|
|
33
|
+
context.clear_box(name_bytes)
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def extract(
|
|
39
|
+
a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.UInt64 | int, /
|
|
40
|
+
) -> algopy.Bytes:
|
|
41
|
+
import algopy
|
|
42
|
+
|
|
43
|
+
context = get_test_context()
|
|
44
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
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]
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def get(a: algopy.Bytes | bytes, /) -> tuple[algopy.Bytes, bool]:
|
|
54
|
+
import algopy
|
|
55
|
+
|
|
56
|
+
context = get_test_context()
|
|
57
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
58
|
+
box_content = context.get_box(name_bytes)
|
|
59
|
+
return box_content, bool(box_content)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def length(a: algopy.Bytes | bytes, /) -> tuple[algopy.UInt64, bool]:
|
|
63
|
+
import algopy
|
|
64
|
+
|
|
65
|
+
context = get_test_context()
|
|
66
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
67
|
+
box_content = context.get_box(name_bytes)
|
|
68
|
+
return algopy.UInt64(len(box_content)), bool(box_content)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def put(a: algopy.Bytes | bytes, b: algopy.Bytes | bytes, /) -> None:
|
|
72
|
+
import algopy
|
|
73
|
+
|
|
74
|
+
context = get_test_context()
|
|
75
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
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)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def replace(
|
|
84
|
+
a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.Bytes | bytes, /
|
|
85
|
+
) -> None:
|
|
86
|
+
import algopy
|
|
87
|
+
|
|
88
|
+
context = get_test_context()
|
|
89
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
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)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def resize(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> None:
|
|
104
|
+
import algopy
|
|
105
|
+
|
|
106
|
+
context = get_test_context()
|
|
107
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
108
|
+
new_size = int(b)
|
|
109
|
+
if not name_bytes or new_size > 32768:
|
|
110
|
+
raise ValueError("Invalid box name or size")
|
|
111
|
+
box_content = context.get_box(name_bytes)
|
|
112
|
+
if not box_content:
|
|
113
|
+
raise ValueError("Box does not exist")
|
|
114
|
+
if new_size > len(box_content):
|
|
115
|
+
updated_content = box_content + b"\x00" * (new_size - len(box_content))
|
|
116
|
+
else:
|
|
117
|
+
updated_content = box_content[:new_size]
|
|
118
|
+
context.set_box(name_bytes, updated_content)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def splice(
|
|
122
|
+
a: algopy.Bytes | bytes,
|
|
123
|
+
b: algopy.UInt64 | int,
|
|
124
|
+
c: algopy.UInt64 | int,
|
|
125
|
+
d: algopy.Bytes | bytes,
|
|
126
|
+
/,
|
|
127
|
+
) -> None:
|
|
128
|
+
import algopy
|
|
129
|
+
|
|
130
|
+
context = get_test_context()
|
|
131
|
+
name_bytes = a.value if isinstance(a, algopy.Bytes) else a
|
|
132
|
+
start = int(b)
|
|
133
|
+
delete_count = int(c)
|
|
134
|
+
insert_content = d.value if isinstance(d, algopy.Bytes) else d
|
|
135
|
+
box_content = context.get_box(name_bytes)
|
|
136
|
+
|
|
137
|
+
if not box_content:
|
|
138
|
+
raise ValueError("Box does not exist")
|
|
139
|
+
|
|
140
|
+
if start > len(box_content):
|
|
141
|
+
raise ValueError("Start index exceeds box size")
|
|
142
|
+
|
|
143
|
+
# Calculate the end index for deletion
|
|
144
|
+
end = min(start + delete_count, len(box_content))
|
|
145
|
+
|
|
146
|
+
# Construct the new content
|
|
147
|
+
new_content = box_content[:start] + insert_content + box_content[end:]
|
|
148
|
+
|
|
149
|
+
# Adjust the size if necessary
|
|
150
|
+
if len(new_content) > len(box_content):
|
|
151
|
+
# Truncate if the new content is too long
|
|
152
|
+
new_content = new_content[: len(box_content)]
|
|
153
|
+
elif len(new_content) < len(box_content):
|
|
154
|
+
# Pad with zeros if the new content is too short
|
|
155
|
+
new_content += b"\x00" * (len(box_content) - len(new_content))
|
|
156
|
+
|
|
157
|
+
# Update the box with the new content
|
|
158
|
+
context.set_box(name_bytes, new_content)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, final
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import algopy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class StateTotals:
|
|
12
|
+
global_uints: int | None = None
|
|
13
|
+
global_bytes: int | None = None
|
|
14
|
+
local_uints: int | None = None
|
|
15
|
+
local_bytes: int | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ContractMeta(type):
|
|
19
|
+
def __call__(cls, *args: Any, **kwargs: dict[str, Any]) -> object:
|
|
20
|
+
from algopy import Contract
|
|
21
|
+
|
|
22
|
+
from algopy_testing.context import get_test_context
|
|
23
|
+
|
|
24
|
+
context = get_test_context()
|
|
25
|
+
instance = super().__call__(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
if context and isinstance(instance, Contract):
|
|
28
|
+
context._add_contract(instance)
|
|
29
|
+
|
|
30
|
+
return instance
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Contract(metaclass=_ContractMeta):
|
|
34
|
+
"""Base class for an Algorand Smart Contract"""
|
|
35
|
+
|
|
36
|
+
_name: str
|
|
37
|
+
_scratch_slots: Any | None
|
|
38
|
+
_state_totals: StateTotals | None
|
|
39
|
+
|
|
40
|
+
def __init_subclass__(
|
|
41
|
+
cls,
|
|
42
|
+
*,
|
|
43
|
+
name: str | None = None,
|
|
44
|
+
scratch_slots: (
|
|
45
|
+
algopy.UInt64 | tuple[int | algopy.UInt64, ...] | list[int | algopy.UInt64] | None
|
|
46
|
+
) = None,
|
|
47
|
+
state_totals: StateTotals | None = None,
|
|
48
|
+
):
|
|
49
|
+
cls._name = name or cls.__name__
|
|
50
|
+
cls._scratch_slots = scratch_slots
|
|
51
|
+
cls._state_totals = state_totals
|
|
52
|
+
|
|
53
|
+
def approval_program(self) -> algopy.UInt64 | bool:
|
|
54
|
+
raise NotImplementedError("`approval_program` is not implemented.")
|
|
55
|
+
|
|
56
|
+
def clear_state_program(self) -> algopy.UInt64 | bool:
|
|
57
|
+
raise NotImplementedError("`clear_state_program` is not implemented.")
|
|
58
|
+
|
|
59
|
+
def __hash__(self) -> int:
|
|
60
|
+
return hash(self._name)
|
|
61
|
+
|
|
62
|
+
def __getattribute__(self, name: str) -> Any:
|
|
63
|
+
attr = super().__getattribute__(name)
|
|
64
|
+
if callable(attr):
|
|
65
|
+
|
|
66
|
+
def wrapper(*args: Any, **kwargs: dict[str, Any]) -> Any:
|
|
67
|
+
return attr(*args, **kwargs)
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
return attr
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ARC4Contract(Contract):
|
|
74
|
+
@final
|
|
75
|
+
def approval_program(self) -> algopy.UInt64 | bool:
|
|
76
|
+
raise NotImplementedError(
|
|
77
|
+
"`approval_program` is not implemented. To test ARC4 specific logic, "
|
|
78
|
+
"refer to direct calls to ARC4 methods."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def clear_state_program(self) -> algopy.UInt64 | bool:
|
|
82
|
+
return True
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _GITxn:
|
|
6
|
+
def __getattr__(self, name: str) -> Callable[[int], typing.Any]:
|
|
7
|
+
from algopy_testing.context import get_test_context
|
|
8
|
+
|
|
9
|
+
context = get_test_context()
|
|
10
|
+
if not context:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
13
|
+
"the context manager."
|
|
14
|
+
)
|
|
15
|
+
if not context._inner_transaction_groups:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
"No inner transaction found in the context! Use `with algopy_testing_context()` "
|
|
18
|
+
"to access the context manager."
|
|
19
|
+
)
|
|
20
|
+
last_itxn_group = context._inner_transaction_groups[-1]
|
|
21
|
+
|
|
22
|
+
if not last_itxn_group:
|
|
23
|
+
raise ValueError("No inner transaction found in the testing context!")
|
|
24
|
+
|
|
25
|
+
return lambda index: self._get_value(last_itxn_group, name, index)
|
|
26
|
+
|
|
27
|
+
# TODO: refine mapping
|
|
28
|
+
def _map_fields(self, name: str) -> str:
|
|
29
|
+
field_mapping = {"type": "type_bytes", "type_enum": "type", "application_args": "app_args"}
|
|
30
|
+
return field_mapping.get(name, name)
|
|
31
|
+
|
|
32
|
+
def _get_value(self, itxn_group: Sequence[typing.Any], name: str, index: int) -> object:
|
|
33
|
+
if index >= len(itxn_group):
|
|
34
|
+
raise IndexError("Transaction index out of range")
|
|
35
|
+
itxn = itxn_group[index]
|
|
36
|
+
value = getattr(itxn, self._map_fields(name))
|
|
37
|
+
if value is None:
|
|
38
|
+
raise ValueError(f"'{name}' is not defined for {type(itxn).__name__}")
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
GITxn = _GITxn()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import typing
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TypedDict, TypeVar
|
|
7
|
+
|
|
8
|
+
import algosdk
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
import algopy
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GlobalFields(TypedDict, total=False):
|
|
19
|
+
min_txn_fee: algopy.UInt64
|
|
20
|
+
min_balance: algopy.UInt64
|
|
21
|
+
max_txn_life: algopy.UInt64
|
|
22
|
+
zero_address: algopy.Account
|
|
23
|
+
group_size: algopy.UInt64
|
|
24
|
+
logic_sig_version: algopy.UInt64
|
|
25
|
+
round: algopy.UInt64
|
|
26
|
+
latest_timestamp: algopy.UInt64
|
|
27
|
+
current_application_id: algopy.Application
|
|
28
|
+
creator_address: algopy.Account
|
|
29
|
+
current_application_address: algopy.Account
|
|
30
|
+
group_id: algopy.Bytes
|
|
31
|
+
caller_application_id: algopy.Application
|
|
32
|
+
caller_application_address: algopy.Account
|
|
33
|
+
asset_create_min_balance: algopy.UInt64
|
|
34
|
+
asset_opt_in_min_balance: algopy.UInt64
|
|
35
|
+
genesis_hash: algopy.Bytes
|
|
36
|
+
opcode_budget: Callable[[], int]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _Global:
|
|
41
|
+
def __getattr__(self, name: str) -> typing.Any:
|
|
42
|
+
import algopy
|
|
43
|
+
|
|
44
|
+
from algopy_testing.context import get_test_context
|
|
45
|
+
|
|
46
|
+
context = get_test_context()
|
|
47
|
+
if not context:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Test context is not initialized! Use `with algopy_testing_context()` to access "
|
|
50
|
+
"the context manager."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if name == "latest_timestamp" and context._global_fields.get(name) is None:
|
|
54
|
+
return algopy.UInt64(int(time.time()))
|
|
55
|
+
|
|
56
|
+
if name == "group_size" and context._global_fields.get(name) is None:
|
|
57
|
+
return algopy.UInt64(len(context.get_transaction_group()))
|
|
58
|
+
|
|
59
|
+
if name == "zero_address" and context._global_fields.get(name) is None:
|
|
60
|
+
return algopy.Account(algosdk.constants.ZERO_ADDRESS)
|
|
61
|
+
|
|
62
|
+
if name not in context._global_fields:
|
|
63
|
+
raise AttributeError(
|
|
64
|
+
f"'algopy.Global' object has no value set for attribute named '{name}'. "
|
|
65
|
+
f"Use `context.patch_global_fields({name}=your_value)` to set the value "
|
|
66
|
+
"in your test setup."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return context._global_fields[name] # type: ignore[literal-required]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Global = _Global()
|