algokit-utils 2.1.2b1__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 CHANGED
@@ -1,8 +1,5 @@
1
- from algokit_utils._ensure_funded import (
2
- EnsureBalanceParameters,
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,
@@ -72,14 +69,14 @@ from algokit_utils.models import (
72
69
  ABIMethod,
73
70
  ABITransactionResponse,
74
71
  Account,
75
- CommonCallParameters, # noqa: ignore[F401]
76
- CommonCallParametersDict, # noqa: ignore[F401]
72
+ CommonCallParameters, # noqa: F401
73
+ CommonCallParametersDict, # noqa: F401
77
74
  CreateCallParameters,
78
75
  CreateCallParametersDict,
79
76
  CreateTransactionParameters,
80
77
  OnCompleteCallParameters,
81
78
  OnCompleteCallParametersDict,
82
- RawTransactionParameters, # noqa: ignore[F401]
79
+ RawTransactionParameters, # noqa: F401
83
80
  TransactionParameters,
84
81
  TransactionParametersDict,
85
82
  TransactionResponse,
@@ -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
 
@@ -97,7 +86,7 @@ class ApplicationClient:
97
86
  """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app"""
98
87
 
99
88
  @overload
100
- def __init__(
89
+ def __init__( # noqa: PLR0913
101
90
  self,
102
91
  algod_client: "AlgodClient",
103
92
  app_spec: au_spec.ApplicationSpecification | Path,
@@ -111,7 +100,7 @@ class ApplicationClient:
111
100
  ...
112
101
 
113
102
  @overload
114
- def __init__(
103
+ def __init__( # noqa: PLR0913
115
104
  self,
116
105
  algod_client: "AlgodClient",
117
106
  app_spec: au_spec.ApplicationSpecification | Path,
@@ -127,7 +116,7 @@ class ApplicationClient:
127
116
  ):
128
117
  ...
129
118
 
130
- def __init__(
119
+ def __init__( # noqa: PLR0913
131
120
  self,
132
121
  algod_client: "AlgodClient",
133
122
  app_spec: au_spec.ApplicationSpecification | Path,
@@ -251,7 +240,7 @@ class ApplicationClient:
251
240
  )
252
241
  return new_client
253
242
 
254
- def _prepare(
243
+ def _prepare( # noqa: PLR0913
255
244
  self,
256
245
  target: "ApplicationClient",
257
246
  *,
@@ -266,7 +255,7 @@ class ApplicationClient:
266
255
  )
267
256
  target.template_values = self.template_values | (template_values or {})
268
257
 
269
- def deploy(
258
+ def deploy( # noqa: PLR0913
270
259
  self,
271
260
  version: str | None = None,
272
261
  *,
@@ -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
- simulate_response = _simulate_response(atc, self.algod_client)
899
+ response = simulate_response(atc, self.algod_client)
874
900
  traces = None
875
901
  if config.debug:
876
- traces = _create_simulate_traces(simulate_response)
877
- if simulate_response.failure_message:
902
+ traces = _create_simulate_traces(response)
903
+ if response.failure_message:
878
904
  raise _try_convert_to_logic_error(
879
- simulate_response.failure_message,
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(simulate_response)
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()
@@ -978,7 +1002,7 @@ class ApplicationClient:
978
1002
  source_map = json.loads(source_map_json)
979
1003
  self._approval_source_map = SourceMap(source_map)
980
1004
 
981
- def add_method_call(
1005
+ def add_method_call( # noqa: PLR0913
982
1006
  self,
983
1007
  atc: AtomicTransactionComposer,
984
1008
  abi_method: ABIMethod | bool | None = None,
@@ -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
- return Program(approval, algod_client), Program(clear, algod_client)
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 = _simulate_response(atc, algod_client)
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:
@@ -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
- def with_debug(self, lambda_func: Callable[[], None | str]) -> None:
13
- original = self._debug
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
- lambda_func()
67
+ func()
17
68
  finally:
18
- self._debug = original
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
- def configure(self, *, debug: bool) -> None:
21
- if debug is not None:
22
- self._debug = debug
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()
algokit_utils/deploy.py CHANGED
@@ -253,7 +253,7 @@ class AppChanges:
253
253
  schema_change_description: str | None
254
254
 
255
255
 
256
- def check_for_app_changes(
256
+ def check_for_app_changes( # noqa: PLR0913
257
257
  algod_client: "AlgodClient",
258
258
  *,
259
259
  new_approval: bytes,
@@ -37,7 +37,7 @@ def parse_logic_error(
37
37
 
38
38
 
39
39
  class LogicError(Exception):
40
- def __init__(
40
+ def __init__( # noqa: PLR0913
41
41
  self,
42
42
  *,
43
43
  logic_error_str: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: algokit-utils
3
- Version: 2.1.2b1
3
+ Version: 2.2.0
4
4
  Summary: Utilities for Algorand development for use by AlgoKit
5
5
  License: MIT
6
6
  Author: Algorand Foundation
@@ -0,0 +1,20 @@
1
+ algokit_utils/__init__.py,sha256=yeufbE_5wjRILZs_10UD5B3_sjdwAfPXskdFKskzXhg,4963
2
+ algokit_utils/_debugging.py,sha256=4UC5NZGqxF32y742TUB34rX9kWaObXCCPOs-lbkQjGQ,10732
3
+ algokit_utils/_ensure_funded.py,sha256=ZdEdUB43QGIQrg7cSSgNrDmWaLSUhli9x9I6juwKfgo,6786
4
+ algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
5
+ algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
6
+ algokit_utils/application_client.py,sha256=2EjOPLEur8xsz31nJKzsElm_DEUI3vbts5d4OxjKxfs,58870
7
+ algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
8
+ algokit_utils/asset.py,sha256=jsc7T1dH9HZA3Yve2gRLObwUlK6xLDoQz0NxLLnqaGs,7216
9
+ algokit_utils/common.py,sha256=K6-3_9dv2clDn0WMYb8AWE_N46kWWIXglZIPfHIowDs,812
10
+ algokit_utils/config.py,sha256=Ag6Wu2iZDN2CwdmjYA3mJPQ50GInkfXyaWMqb2ZHd6g,4278
11
+ algokit_utils/deploy.py,sha256=ydE3QSq1lRkjXQC9zdFclywx8q1UgV9l-l3Mx-shbHg,34668
12
+ algokit_utils/dispenser_api.py,sha256=BpwEhKDig6qz54wbO-htG8hmLxFIrvdzXpESUb7Y1zw,5584
13
+ algokit_utils/logic_error.py,sha256=bta0YsZF6ggmrspc7tIs0itBOY2jk-AS62EZrKfiHLI,2632
14
+ algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
15
+ algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
16
+ algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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,,
@@ -1,18 +0,0 @@
1
- algokit_utils/__init__.py,sha256=NokdZ-V2JKOWipN2TMvcBxgcEKzJ_3F4rSnwsittqC4,4774
2
- algokit_utils/_ensure_funded.py,sha256=ZdEdUB43QGIQrg7cSSgNrDmWaLSUhli9x9I6juwKfgo,6786
3
- algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
4
- algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
5
- algokit_utils/application_client.py,sha256=9YH4ecHsn0aXmDeaApraT5WeHaUlN4FPa27QdtN9tew,57878
6
- algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
7
- algokit_utils/asset.py,sha256=jsc7T1dH9HZA3Yve2gRLObwUlK6xLDoQz0NxLLnqaGs,7216
8
- algokit_utils/config.py,sha256=V8010eUkbfcoB0bHtxsGQOymq1cqNRc1lDWnwcumQpM,567
9
- algokit_utils/deploy.py,sha256=klvgxwbS78pOXCLdD0wZB2Q2MSVHOhb66LIs4Nbbt4E,34651
10
- algokit_utils/dispenser_api.py,sha256=BpwEhKDig6qz54wbO-htG8hmLxFIrvdzXpESUb7Y1zw,5584
11
- algokit_utils/logic_error.py,sha256=vkLVdxv-XPnwRiIP4CWedgtIZcOPr_5wr7XVH02If9w,2615
12
- algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
13
- algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
14
- algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- algokit_utils-2.1.2b1.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
16
- algokit_utils-2.1.2b1.dist-info/METADATA,sha256=RRdb7yXQ4J6vR-2WcvmIOETayHq1S9bEJWbVjHHIK2E,2207
17
- algokit_utils-2.1.2b1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
18
- algokit_utils-2.1.2b1.dist-info/RECORD,,