algokit-utils 2.1.3b1__py3-none-any.whl → 2.2.0__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.
Potentially problematic release.
This version of algokit-utils might be problematic. Click here for more details.
- algokit_utils/__init__.py +6 -6
- algokit_utils/_debugging.py +280 -0
- algokit_utils/application_client.py +66 -44
- algokit_utils/common.py +28 -0
- algokit_utils/config.py +94 -7
- {algokit_utils-2.1.3b1.dist-info → algokit_utils-2.2.0.dist-info}/METADATA +1 -1
- {algokit_utils-2.1.3b1.dist-info → algokit_utils-2.2.0.dist-info}/RECORD +9 -7
- {algokit_utils-2.1.3b1.dist-info → algokit_utils-2.2.0.dist-info}/LICENSE +0 -0
- {algokit_utils-2.1.3b1.dist-info → algokit_utils-2.2.0.dist-info}/WHEEL +0 -0
algokit_utils/__init__.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
from algokit_utils.
|
|
2
|
-
|
|
3
|
-
EnsureFundedResponse,
|
|
4
|
-
ensure_funded,
|
|
5
|
-
)
|
|
1
|
+
from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response
|
|
2
|
+
from algokit_utils._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded
|
|
6
3
|
from algokit_utils._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset
|
|
7
4
|
from algokit_utils.account import (
|
|
8
5
|
create_kmd_wallet_account,
|
|
@@ -15,7 +12,6 @@ from algokit_utils.account import (
|
|
|
15
12
|
)
|
|
16
13
|
from algokit_utils.application_client import (
|
|
17
14
|
ApplicationClient,
|
|
18
|
-
Program,
|
|
19
15
|
execute_atc_with_logic_error,
|
|
20
16
|
get_next_version,
|
|
21
17
|
get_sender_from_signer,
|
|
@@ -32,6 +28,7 @@ from algokit_utils.application_specification import (
|
|
|
32
28
|
OnCompleteActionName,
|
|
33
29
|
)
|
|
34
30
|
from algokit_utils.asset import opt_in, opt_out
|
|
31
|
+
from algokit_utils.common import Program
|
|
35
32
|
from algokit_utils.deploy import (
|
|
36
33
|
DELETABLE_TEMPLATE_NAME,
|
|
37
34
|
NOTE_PREFIX,
|
|
@@ -181,4 +178,7 @@ __all__ = [
|
|
|
181
178
|
"transfer_asset",
|
|
182
179
|
"opt_in",
|
|
183
180
|
"opt_out",
|
|
181
|
+
"persist_sourcemaps",
|
|
182
|
+
"PersistSourceMapInput",
|
|
183
|
+
"simulate_and_persist_response",
|
|
184
184
|
]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from algosdk.atomic_transaction_composer import (
|
|
10
|
+
AtomicTransactionComposer,
|
|
11
|
+
EmptySigner,
|
|
12
|
+
SimulateAtomicTransactionResponse,
|
|
13
|
+
)
|
|
14
|
+
from algosdk.encoding import checksum
|
|
15
|
+
from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig
|
|
16
|
+
|
|
17
|
+
from algokit_utils.common import Program
|
|
18
|
+
|
|
19
|
+
if typing.TYPE_CHECKING:
|
|
20
|
+
from algosdk.v2client.algod import AlgodClient
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
ALGOKIT_DIR = ".algokit"
|
|
25
|
+
SOURCES_DIR = "sources"
|
|
26
|
+
SOURCES_FILE = "sources.avm.json"
|
|
27
|
+
TRACES_FILE_EXT = ".trace.avm.json"
|
|
28
|
+
DEBUG_TRACES_DIR = "debug_traces"
|
|
29
|
+
TEAL_FILE_EXT = ".teal"
|
|
30
|
+
TEAL_SOURCEMAP_EXT = ".teal.tok.map"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AVMDebuggerSourceMapEntry:
|
|
35
|
+
location: str = field(metadata={"json": "sourcemap-location"})
|
|
36
|
+
program_hash: str = field(metadata={"json": "hash"})
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other: object) -> bool:
|
|
39
|
+
if isinstance(other, AVMDebuggerSourceMapEntry):
|
|
40
|
+
return self.location == other.location and self.program_hash == other.program_hash
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return json.dumps({"sourcemap-location": self.location, "hash": self.program_hash})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AVMDebuggerSourceMap:
|
|
49
|
+
txn_group_sources: list[AVMDebuggerSourceMapEntry] = field(metadata={"json": "txn-group-sources"})
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, data: dict) -> "AVMDebuggerSourceMap":
|
|
53
|
+
return cls(
|
|
54
|
+
txn_group_sources=[
|
|
55
|
+
AVMDebuggerSourceMapEntry(location=item["sourcemap-location"], program_hash=item["hash"])
|
|
56
|
+
for item in data.get("txn-group-sources", [])
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
return {"txn-group-sources": [json.loads(str(item)) for item in self.txn_group_sources]}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class PersistSourceMapInput:
|
|
66
|
+
def __init__(
|
|
67
|
+
self, app_name: str, file_name: str, raw_teal: str | None = None, compiled_teal: Program | None = None
|
|
68
|
+
):
|
|
69
|
+
self.compiled_teal = compiled_teal
|
|
70
|
+
self.app_name = app_name
|
|
71
|
+
self._raw_teal = raw_teal
|
|
72
|
+
self._file_name = self.strip_teal_extension(file_name)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_raw_teal(cls, raw_teal: str, app_name: str, file_name: str) -> "PersistSourceMapInput":
|
|
76
|
+
return cls(app_name, file_name, raw_teal=raw_teal)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_compiled_teal(cls, compiled_teal: Program, app_name: str, file_name: str) -> "PersistSourceMapInput":
|
|
80
|
+
return cls(app_name, file_name, compiled_teal=compiled_teal)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def raw_teal(self) -> str:
|
|
84
|
+
if self._raw_teal:
|
|
85
|
+
return self._raw_teal
|
|
86
|
+
elif self.compiled_teal:
|
|
87
|
+
return self.compiled_teal.teal
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError("No teal content found")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def file_name(self) -> str:
|
|
93
|
+
return self._file_name
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def strip_teal_extension(file_name: str) -> str:
|
|
97
|
+
if file_name.endswith(".teal"):
|
|
98
|
+
return file_name[:-5]
|
|
99
|
+
return file_name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_or_create_sources(sources_path: Path) -> AVMDebuggerSourceMap:
|
|
103
|
+
if not sources_path.exists():
|
|
104
|
+
return AVMDebuggerSourceMap(txn_group_sources=[])
|
|
105
|
+
|
|
106
|
+
with sources_path.open() as f:
|
|
107
|
+
return AVMDebuggerSourceMap.from_dict(json.load(f))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _upsert_debug_sourcemaps(sourcemaps: list[AVMDebuggerSourceMapEntry], project_root: Path) -> None:
|
|
111
|
+
"""
|
|
112
|
+
This function updates or inserts debug sourcemaps. If path in the sourcemap during iteration leads to non
|
|
113
|
+
existing file, removes it. Otherwise upserts.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
sourcemaps (list[AVMDebuggerSourceMapEntry]): A list of AVMDebuggerSourceMapEntry objects.
|
|
117
|
+
project_root (Path): The root directory of the project.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
None
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
sources_path = project_root / ALGOKIT_DIR / SOURCES_DIR / SOURCES_FILE
|
|
124
|
+
sources = _load_or_create_sources(sources_path)
|
|
125
|
+
|
|
126
|
+
for sourcemap in sourcemaps:
|
|
127
|
+
source_file_path = Path(sourcemap.location)
|
|
128
|
+
if not source_file_path.exists() and sourcemap in sources.txn_group_sources:
|
|
129
|
+
sources.txn_group_sources.remove(sourcemap)
|
|
130
|
+
elif source_file_path.exists():
|
|
131
|
+
if sourcemap not in sources.txn_group_sources:
|
|
132
|
+
sources.txn_group_sources.append(sourcemap)
|
|
133
|
+
else:
|
|
134
|
+
index = sources.txn_group_sources.index(sourcemap)
|
|
135
|
+
sources.txn_group_sources[index] = sourcemap
|
|
136
|
+
|
|
137
|
+
with sources_path.open("w") as f:
|
|
138
|
+
json.dump(sources.to_dict(), f)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _write_to_file(path: Path, content: str) -> None:
|
|
142
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
path.write_text(content)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_avm_sourcemap( # noqa: PLR0913
|
|
147
|
+
*,
|
|
148
|
+
app_name: str,
|
|
149
|
+
file_name: str,
|
|
150
|
+
output_path: Path,
|
|
151
|
+
client: "AlgodClient",
|
|
152
|
+
raw_teal: str | None = None,
|
|
153
|
+
compiled_teal: Program | None = None,
|
|
154
|
+
with_sources: bool = True,
|
|
155
|
+
) -> AVMDebuggerSourceMapEntry:
|
|
156
|
+
if not raw_teal and not compiled_teal:
|
|
157
|
+
raise ValueError("Either raw teal or compiled teal must be provided")
|
|
158
|
+
|
|
159
|
+
result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client)
|
|
160
|
+
program_hash = base64.b64encode(
|
|
161
|
+
checksum(result.raw_binary) # type: ignore[no-untyped-call]
|
|
162
|
+
).decode()
|
|
163
|
+
source_map = result.source_map.__dict__
|
|
164
|
+
source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else []
|
|
165
|
+
|
|
166
|
+
output_dir_path = output_path / ALGOKIT_DIR / SOURCES_DIR / app_name
|
|
167
|
+
source_map_output_path = output_dir_path / f"{file_name}{TEAL_SOURCEMAP_EXT}"
|
|
168
|
+
teal_output_path = output_dir_path / f"{file_name}{TEAL_FILE_EXT}"
|
|
169
|
+
_write_to_file(source_map_output_path, json.dumps(source_map))
|
|
170
|
+
|
|
171
|
+
if with_sources:
|
|
172
|
+
_write_to_file(teal_output_path, result.teal)
|
|
173
|
+
|
|
174
|
+
return AVMDebuggerSourceMapEntry(str(source_map_output_path), program_hash)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def persist_sourcemaps(
|
|
178
|
+
*, sources: list[PersistSourceMapInput], project_root: Path, client: "AlgodClient", with_sources: bool = True
|
|
179
|
+
) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts.
|
|
182
|
+
Args:
|
|
183
|
+
sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects.
|
|
184
|
+
project_root (Path): The root directory of the project.
|
|
185
|
+
client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain.
|
|
186
|
+
with_sources (bool): If True, it will dump teal source files along with sourcemaps.
|
|
187
|
+
Default is True, as needed by an AlgoKit AVM debugger.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
sourcemaps = [
|
|
191
|
+
_build_avm_sourcemap(
|
|
192
|
+
raw_teal=source.raw_teal,
|
|
193
|
+
compiled_teal=source.compiled_teal,
|
|
194
|
+
app_name=source.app_name,
|
|
195
|
+
file_name=source.file_name,
|
|
196
|
+
output_path=project_root,
|
|
197
|
+
client=client,
|
|
198
|
+
with_sources=with_sources,
|
|
199
|
+
)
|
|
200
|
+
for source in sources
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
_upsert_debug_sourcemaps(sourcemaps, project_root)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse:
|
|
207
|
+
"""
|
|
208
|
+
Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
atc (AtomicTransactionComposer): An AtomicTransactionComposer object.
|
|
212
|
+
algod_client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
SimulateAtomicTransactionResponse: The simulated response.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
unsigned_txn_groups = atc.build_group()
|
|
219
|
+
empty_signer = EmptySigner()
|
|
220
|
+
txn_list = [txn_group.txn for txn_group in unsigned_txn_groups]
|
|
221
|
+
fake_signed_transactions = empty_signer.sign_transactions(txn_list, [])
|
|
222
|
+
txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)]
|
|
223
|
+
trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True)
|
|
224
|
+
|
|
225
|
+
simulate_request = SimulateRequest(
|
|
226
|
+
txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config
|
|
227
|
+
)
|
|
228
|
+
return atc.simulate(algod_client, simulate_request)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def simulate_and_persist_response(
|
|
232
|
+
atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256
|
|
233
|
+
) -> SimulateAtomicTransactionResponse:
|
|
234
|
+
"""
|
|
235
|
+
Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object,
|
|
236
|
+
and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file.
|
|
237
|
+
|
|
238
|
+
:param atc: An `AtomicTransactionComposer` object representing the atomic transactions to be
|
|
239
|
+
simulated and persisted.
|
|
240
|
+
:param project_root: A `Path` object representing the root directory of the project.
|
|
241
|
+
:param algod_client: An `AlgodClient` object representing the Algorand client.
|
|
242
|
+
:param buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256mb.
|
|
243
|
+
:return: None
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
SimulateAtomicTransactionResponse: The simulated response after persisting it
|
|
247
|
+
for AlgoKit AVM Debugger consumption.
|
|
248
|
+
"""
|
|
249
|
+
atc_to_simulate = atc.clone()
|
|
250
|
+
sp = algod_client.suggested_params()
|
|
251
|
+
|
|
252
|
+
for txn_with_sign in atc_to_simulate.txn_list:
|
|
253
|
+
txn_with_sign.txn.first_valid_round = sp.first
|
|
254
|
+
txn_with_sign.txn.last_valid_round = sp.last
|
|
255
|
+
txn_with_sign.txn.genesis_hash = sp.gh
|
|
256
|
+
|
|
257
|
+
response = simulate_response(atc_to_simulate, algod_client)
|
|
258
|
+
txn_results = response.simulate_response["txn-groups"]
|
|
259
|
+
|
|
260
|
+
txn_types = [txn_result["txn-results"][0]["txn-result"]["txn"]["txn"]["type"] for txn_result in txn_results]
|
|
261
|
+
txn_types_count = {txn_type: txn_types.count(txn_type) for txn_type in set(txn_types)}
|
|
262
|
+
txn_types_str = "_".join([f"{count}#{txn_type}" for txn_type, count in txn_types_count.items()])
|
|
263
|
+
|
|
264
|
+
last_round = response.simulate_response["last-round"]
|
|
265
|
+
timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
266
|
+
output_file = project_root / DEBUG_TRACES_DIR / f"{timestamp}_lr{last_round}_{txn_types_str}{TRACES_FILE_EXT}"
|
|
267
|
+
|
|
268
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
|
|
270
|
+
# cleanup old files if buffer size is exceeded
|
|
271
|
+
total_size = sum(f.stat().st_size for f in output_file.parent.glob("*") if f.is_file())
|
|
272
|
+
if total_size > buffer_size_mb * 1024 * 1024:
|
|
273
|
+
sorted_files = sorted(output_file.parent.glob("*"), key=lambda p: p.stat().st_mtime)
|
|
274
|
+
while total_size > buffer_size_mb * 1024 * 1024:
|
|
275
|
+
oldest_file = sorted_files.pop(0)
|
|
276
|
+
total_size -= oldest_file.stat().st_size
|
|
277
|
+
oldest_file.unlink()
|
|
278
|
+
|
|
279
|
+
output_file.write_text(json.dumps(response.simulate_response, indent=2))
|
|
280
|
+
return response
|
|
@@ -18,7 +18,6 @@ from algosdk.atomic_transaction_composer import (
|
|
|
18
18
|
AccountTransactionSigner,
|
|
19
19
|
AtomicTransactionComposer,
|
|
20
20
|
AtomicTransactionResponse,
|
|
21
|
-
EmptySigner,
|
|
22
21
|
LogicSigTransactionSigner,
|
|
23
22
|
MultisigTransactionSigner,
|
|
24
23
|
SimulateAtomicTransactionResponse,
|
|
@@ -28,10 +27,16 @@ from algosdk.atomic_transaction_composer import (
|
|
|
28
27
|
from algosdk.constants import APP_PAGE_MAX_SIZE
|
|
29
28
|
from algosdk.logic import get_application_address
|
|
30
29
|
from algosdk.source_map import SourceMap
|
|
31
|
-
from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig
|
|
32
30
|
|
|
33
31
|
import algokit_utils.application_specification as au_spec
|
|
34
32
|
import algokit_utils.deploy as au_deploy
|
|
33
|
+
from algokit_utils._debugging import (
|
|
34
|
+
PersistSourceMapInput,
|
|
35
|
+
persist_sourcemaps,
|
|
36
|
+
simulate_and_persist_response,
|
|
37
|
+
simulate_response,
|
|
38
|
+
)
|
|
39
|
+
from algokit_utils.common import Program
|
|
35
40
|
from algokit_utils.config import config
|
|
36
41
|
from algokit_utils.logic_error import LogicError, parse_logic_error
|
|
37
42
|
from algokit_utils.models import (
|
|
@@ -61,7 +66,6 @@ logger = logging.getLogger(__name__)
|
|
|
61
66
|
|
|
62
67
|
__all__ = [
|
|
63
68
|
"ApplicationClient",
|
|
64
|
-
"Program",
|
|
65
69
|
"execute_atc_with_logic_error",
|
|
66
70
|
"get_next_version",
|
|
67
71
|
"get_sender_from_signer",
|
|
@@ -72,21 +76,6 @@ __all__ = [
|
|
|
72
76
|
representing an ABI method name or signature"""
|
|
73
77
|
|
|
74
78
|
|
|
75
|
-
class Program:
|
|
76
|
-
"""A compiled TEAL program"""
|
|
77
|
-
|
|
78
|
-
def __init__(self, program: str, client: "AlgodClient"):
|
|
79
|
-
"""
|
|
80
|
-
Fully compile the program source to binary and generate a
|
|
81
|
-
source map for matching pc to line number
|
|
82
|
-
"""
|
|
83
|
-
self.teal = program
|
|
84
|
-
result: dict = client.compile(au_deploy.strip_comments(self.teal), source_map=True)
|
|
85
|
-
self.raw_binary = base64.b64decode(result["result"])
|
|
86
|
-
self.binary_hash: str = result["hash"]
|
|
87
|
-
self.source_map = SourceMap(result["sourcemap"])
|
|
88
|
-
|
|
89
|
-
|
|
90
79
|
def num_extra_program_pages(approval: bytes, clear: bytes) -> int:
|
|
91
80
|
"""Calculate minimum number of extra_pages required for provided approval and clear programs"""
|
|
92
81
|
|
|
@@ -346,6 +335,22 @@ class ApplicationClient:
|
|
|
346
335
|
self._approval_program, self._clear_program = substitute_template_and_compile(
|
|
347
336
|
self.algod_client, self.app_spec, template_values
|
|
348
337
|
)
|
|
338
|
+
|
|
339
|
+
if config.debug and config.project_root:
|
|
340
|
+
persist_sourcemaps(
|
|
341
|
+
sources=[
|
|
342
|
+
PersistSourceMapInput(
|
|
343
|
+
compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal"
|
|
344
|
+
),
|
|
345
|
+
PersistSourceMapInput(
|
|
346
|
+
compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal"
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
project_root=config.project_root,
|
|
350
|
+
client=self.algod_client,
|
|
351
|
+
with_sources=True,
|
|
352
|
+
)
|
|
353
|
+
|
|
349
354
|
deployer = au_deploy.Deployer(
|
|
350
355
|
app_client=self,
|
|
351
356
|
creator=self._creator,
|
|
@@ -630,6 +635,11 @@ class ApplicationClient:
|
|
|
630
635
|
if method:
|
|
631
636
|
hints = self._method_hints(method)
|
|
632
637
|
if hints and hints.read_only:
|
|
638
|
+
if config.debug and config.project_root and config.trace_all:
|
|
639
|
+
simulate_and_persist_response(
|
|
640
|
+
atc, config.project_root, self.algod_client, config.trace_buffer_size_mb
|
|
641
|
+
)
|
|
642
|
+
|
|
633
643
|
return self._simulate_readonly_call(method, atc)
|
|
634
644
|
|
|
635
645
|
return self._execute_atc_tr(atc)
|
|
@@ -865,26 +875,40 @@ class ApplicationClient:
|
|
|
865
875
|
self._approval_program, self._clear_program = substitute_template_and_compile(
|
|
866
876
|
self.algod_client, self.app_spec, self.template_values
|
|
867
877
|
)
|
|
878
|
+
|
|
879
|
+
if config.debug and config.project_root:
|
|
880
|
+
persist_sourcemaps(
|
|
881
|
+
sources=[
|
|
882
|
+
PersistSourceMapInput(
|
|
883
|
+
compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal"
|
|
884
|
+
),
|
|
885
|
+
PersistSourceMapInput(
|
|
886
|
+
compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal"
|
|
887
|
+
),
|
|
888
|
+
],
|
|
889
|
+
project_root=config.project_root,
|
|
890
|
+
client=self.algod_client,
|
|
891
|
+
with_sources=True,
|
|
892
|
+
)
|
|
893
|
+
|
|
868
894
|
return self._approval_program, self._clear_program
|
|
869
895
|
|
|
870
896
|
def _simulate_readonly_call(
|
|
871
897
|
self, method: Method, atc: AtomicTransactionComposer
|
|
872
898
|
) -> ABITransactionResponse | TransactionResponse:
|
|
873
|
-
|
|
899
|
+
response = simulate_response(atc, self.algod_client)
|
|
874
900
|
traces = None
|
|
875
901
|
if config.debug:
|
|
876
|
-
traces = _create_simulate_traces(
|
|
877
|
-
if
|
|
902
|
+
traces = _create_simulate_traces(response)
|
|
903
|
+
if response.failure_message:
|
|
878
904
|
raise _try_convert_to_logic_error(
|
|
879
|
-
|
|
905
|
+
response.failure_message,
|
|
880
906
|
self.app_spec.approval_program,
|
|
881
907
|
self._get_approval_source_map,
|
|
882
908
|
traces,
|
|
883
|
-
) or Exception(
|
|
884
|
-
f"Simulate failed for readonly method {method.get_signature()}: {simulate_response.failure_message}"
|
|
885
|
-
)
|
|
909
|
+
) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}")
|
|
886
910
|
|
|
887
|
-
return TransactionResponse.from_atr(
|
|
911
|
+
return TransactionResponse.from_atr(response)
|
|
888
912
|
|
|
889
913
|
def _load_reference_and_check_app_id(self) -> None:
|
|
890
914
|
self._load_app_reference()
|
|
@@ -1192,7 +1216,9 @@ def substitute_template_and_compile(
|
|
|
1192
1216
|
au_deploy.check_template_variables(app_spec.approval_program, template_values)
|
|
1193
1217
|
approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values)
|
|
1194
1218
|
|
|
1195
|
-
|
|
1219
|
+
approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client)
|
|
1220
|
+
|
|
1221
|
+
return approval_app, clear_app
|
|
1196
1222
|
|
|
1197
1223
|
|
|
1198
1224
|
def get_next_version(current_version: str) -> str:
|
|
@@ -1263,10 +1289,22 @@ def execute_atc_with_logic_error(
|
|
|
1263
1289
|
```
|
|
1264
1290
|
"""
|
|
1265
1291
|
try:
|
|
1292
|
+
if config.debug and config.project_root and config.trace_all:
|
|
1293
|
+
simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb)
|
|
1294
|
+
|
|
1266
1295
|
return atc.execute(algod_client, wait_rounds=wait_rounds)
|
|
1267
1296
|
except Exception as ex:
|
|
1268
1297
|
if config.debug:
|
|
1269
|
-
simulate =
|
|
1298
|
+
simulate = None
|
|
1299
|
+
if config.project_root and not config.trace_all:
|
|
1300
|
+
# if trace_all is enabled, we already have the traces executed above
|
|
1301
|
+
# hence we only need to simulate if trace_all is disabled and
|
|
1302
|
+
# project_root is set
|
|
1303
|
+
simulate = simulate_and_persist_response(
|
|
1304
|
+
atc, config.project_root, algod_client, config.trace_buffer_size_mb
|
|
1305
|
+
)
|
|
1306
|
+
else:
|
|
1307
|
+
simulate = simulate_response(atc, algod_client)
|
|
1270
1308
|
traces = _create_simulate_traces(simulate)
|
|
1271
1309
|
else:
|
|
1272
1310
|
traces = None
|
|
@@ -1299,22 +1337,6 @@ def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list
|
|
|
1299
1337
|
return traces
|
|
1300
1338
|
|
|
1301
1339
|
|
|
1302
|
-
def _simulate_response(
|
|
1303
|
-
atc: AtomicTransactionComposer, algod_client: "AlgodClient"
|
|
1304
|
-
) -> SimulateAtomicTransactionResponse:
|
|
1305
|
-
unsigned_txn_groups = atc.build_group()
|
|
1306
|
-
empty_signer = EmptySigner()
|
|
1307
|
-
txn_list = [txn_group.txn for txn_group in unsigned_txn_groups]
|
|
1308
|
-
fake_signed_transactions = empty_signer.sign_transactions(txn_list, [])
|
|
1309
|
-
txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)]
|
|
1310
|
-
trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True)
|
|
1311
|
-
|
|
1312
|
-
simulate_request = SimulateRequest(
|
|
1313
|
-
txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config
|
|
1314
|
-
)
|
|
1315
|
-
return atc.simulate(algod_client, simulate_request)
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
1340
|
def _convert_transaction_parameters(
|
|
1319
1341
|
args: TransactionParameters | TransactionParametersDict | None,
|
|
1320
1342
|
) -> CreateCallParameters:
|
algokit_utils/common.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains common classes and methods that are reused in more than one file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from algosdk.source_map import SourceMap
|
|
9
|
+
|
|
10
|
+
from algokit_utils import deploy
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from algosdk.v2client.algod import AlgodClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Program:
|
|
17
|
+
"""A compiled TEAL program"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, program: str, client: "AlgodClient"):
|
|
20
|
+
"""
|
|
21
|
+
Fully compile the program source to binary and generate a
|
|
22
|
+
source map for matching pc to line number
|
|
23
|
+
"""
|
|
24
|
+
self.teal = program
|
|
25
|
+
result: dict = client.compile(deploy.strip_comments(self.teal), source_map=True)
|
|
26
|
+
self.raw_binary = base64.b64decode(result["result"])
|
|
27
|
+
self.binary_hash: str = result["hash"]
|
|
28
|
+
self.source_map = SourceMap(result["sourcemap"])
|
algokit_utils/config.py
CHANGED
|
@@ -1,25 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
1
3
|
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
# Environment variable to override the project root
|
|
9
|
+
ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT")
|
|
10
|
+
ALGOKIT_CONFIG_FILENAME = ".algokit.toml"
|
|
2
11
|
|
|
3
12
|
|
|
4
13
|
class UpdatableConfig:
|
|
14
|
+
"""Class to manage and update configuration settings for the AlgoKit project.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
debug (bool): Indicates whether debug mode is enabled.
|
|
18
|
+
project_root (Path | None): The path to the project root directory.
|
|
19
|
+
trace_all (bool): Indicates whether to trace all operations.
|
|
20
|
+
trace_buffer_size_mb (int): The size of the trace buffer in megabytes.
|
|
21
|
+
max_search_depth (int): The maximum depth to search for a specific file.
|
|
22
|
+
"""
|
|
23
|
+
|
|
5
24
|
def __init__(self) -> None:
|
|
6
25
|
self._debug: bool = False
|
|
26
|
+
self._project_root: Path | None = None
|
|
27
|
+
self._trace_all: bool = False
|
|
28
|
+
self._trace_buffer_size_mb: int | float = 256 # megabytes
|
|
29
|
+
self._max_search_depth: int = 10
|
|
30
|
+
self._configure_project_root()
|
|
31
|
+
|
|
32
|
+
def _configure_project_root(self) -> None:
|
|
33
|
+
"""Configures the project root by searching for a specific file within a depth limit."""
|
|
34
|
+
current_path = Path(__file__).resolve()
|
|
35
|
+
for _ in range(self._max_search_depth):
|
|
36
|
+
logger.info(f"Searching in: {current_path}")
|
|
37
|
+
if (current_path / ALGOKIT_CONFIG_FILENAME).exists():
|
|
38
|
+
self._project_root = current_path
|
|
39
|
+
break
|
|
40
|
+
current_path = current_path.parent
|
|
7
41
|
|
|
8
42
|
@property
|
|
9
43
|
def debug(self) -> bool:
|
|
44
|
+
"""Returns the debug status."""
|
|
10
45
|
return self._debug
|
|
11
46
|
|
|
12
|
-
|
|
13
|
-
|
|
47
|
+
@property
|
|
48
|
+
def project_root(self) -> Path | None:
|
|
49
|
+
"""Returns the project root path."""
|
|
50
|
+
return self._project_root
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def trace_all(self) -> bool:
|
|
54
|
+
"""Indicates whether to store simulation traces for all operations."""
|
|
55
|
+
return self._trace_all
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def trace_buffer_size_mb(self) -> int | float:
|
|
59
|
+
"""Returns the size of the trace buffer in megabytes."""
|
|
60
|
+
return self._trace_buffer_size_mb
|
|
61
|
+
|
|
62
|
+
def with_debug(self, func: Callable[[], str | None]) -> None:
|
|
63
|
+
"""Executes a function with debug mode temporarily enabled."""
|
|
64
|
+
original_debug = self._debug
|
|
14
65
|
try:
|
|
15
66
|
self._debug = True
|
|
16
|
-
|
|
67
|
+
func()
|
|
17
68
|
finally:
|
|
18
|
-
self._debug =
|
|
69
|
+
self._debug = original_debug
|
|
70
|
+
|
|
71
|
+
def configure( # noqa: PLR0913
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
debug: bool,
|
|
75
|
+
project_root: Path | None = None,
|
|
76
|
+
trace_all: bool = False,
|
|
77
|
+
trace_buffer_size_mb: float = 256,
|
|
78
|
+
max_search_depth: int = 10,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Configures various settings for the application.
|
|
82
|
+
Please note, when `project_root` is not specified, by default config will attempt to find the `algokit.toml` by
|
|
83
|
+
scanning the parent directories according to the `max_search_depth` parameter.
|
|
84
|
+
Alternatively value can also be set via the `ALGOKIT_PROJECT_ROOT` environment variable.
|
|
85
|
+
If you are executing the config from an algokit compliant project, you can simply call
|
|
86
|
+
`config.configure(debug=True)`.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
debug (bool): Indicates whether debug mode is enabled.
|
|
90
|
+
project_root (Path | None, optional): The path to the project root directory. Defaults to None.
|
|
91
|
+
trace_all (bool, optional): Indicates whether to trace all operations. Defaults to False. Which implies that
|
|
92
|
+
only the operations that are failed will be traced by default.
|
|
93
|
+
trace_buffer_size_mb (float, optional): The size of the trace buffer in megabytes. Defaults to 512mb.
|
|
94
|
+
max_search_depth (int, optional): The maximum depth to search for a specific file. Defaults to 10.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
None
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
self._debug = debug
|
|
101
|
+
|
|
102
|
+
if project_root:
|
|
103
|
+
self._project_root = project_root.resolve(strict=True)
|
|
104
|
+
elif debug and ALGOKIT_PROJECT_ROOT:
|
|
105
|
+
self._project_root = Path(ALGOKIT_PROJECT_ROOT).resolve(strict=True)
|
|
19
106
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
107
|
+
self._trace_all = trace_all
|
|
108
|
+
self._trace_buffer_size_mb = trace_buffer_size_mb
|
|
109
|
+
self._max_search_depth = max_search_depth
|
|
23
110
|
|
|
24
111
|
|
|
25
112
|
config = UpdatableConfig()
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
algokit_utils/__init__.py,sha256=
|
|
1
|
+
algokit_utils/__init__.py,sha256=yeufbE_5wjRILZs_10UD5B3_sjdwAfPXskdFKskzXhg,4963
|
|
2
|
+
algokit_utils/_debugging.py,sha256=4UC5NZGqxF32y742TUB34rX9kWaObXCCPOs-lbkQjGQ,10732
|
|
2
3
|
algokit_utils/_ensure_funded.py,sha256=ZdEdUB43QGIQrg7cSSgNrDmWaLSUhli9x9I6juwKfgo,6786
|
|
3
4
|
algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
|
|
4
5
|
algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
|
|
5
|
-
algokit_utils/application_client.py,sha256=
|
|
6
|
+
algokit_utils/application_client.py,sha256=2EjOPLEur8xsz31nJKzsElm_DEUI3vbts5d4OxjKxfs,58870
|
|
6
7
|
algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
|
|
7
8
|
algokit_utils/asset.py,sha256=jsc7T1dH9HZA3Yve2gRLObwUlK6xLDoQz0NxLLnqaGs,7216
|
|
8
|
-
algokit_utils/
|
|
9
|
+
algokit_utils/common.py,sha256=K6-3_9dv2clDn0WMYb8AWE_N46kWWIXglZIPfHIowDs,812
|
|
10
|
+
algokit_utils/config.py,sha256=Ag6Wu2iZDN2CwdmjYA3mJPQ50GInkfXyaWMqb2ZHd6g,4278
|
|
9
11
|
algokit_utils/deploy.py,sha256=ydE3QSq1lRkjXQC9zdFclywx8q1UgV9l-l3Mx-shbHg,34668
|
|
10
12
|
algokit_utils/dispenser_api.py,sha256=BpwEhKDig6qz54wbO-htG8hmLxFIrvdzXpESUb7Y1zw,5584
|
|
11
13
|
algokit_utils/logic_error.py,sha256=bta0YsZF6ggmrspc7tIs0itBOY2jk-AS62EZrKfiHLI,2632
|
|
12
14
|
algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
|
|
13
15
|
algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
|
|
14
16
|
algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
algokit_utils-2.
|
|
16
|
-
algokit_utils-2.
|
|
17
|
-
algokit_utils-2.
|
|
18
|
-
algokit_utils-2.
|
|
17
|
+
algokit_utils-2.2.0.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
|
|
18
|
+
algokit_utils-2.2.0.dist-info/METADATA,sha256=TeDdZ1Z4Eg2fwYi1sKrAANWoYo0lRpP4TYanIFL45hg,2205
|
|
19
|
+
algokit_utils-2.2.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
20
|
+
algokit_utils-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|