diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 6db51d97..d76f27b7 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -136,6 +136,7 @@ When an error is thrown then the resulting error that is re-thrown will be a `Lo * `transaction_id`: Transaction ID of failing transaction * `message`: The error message * `line_no`: The line number in the TEAL program that +* `traces`: A list of Trace objects providing additional insights on simulation when debug mode is active. The function `trace()` will provide a formatted output of the surrounding TEAL where the error occurred. @@ -147,3 +148,10 @@ The extended information will only show if the Application Client has a source m 3.) `approval_source_map` on `ApplicationClient` has been set from a previously compiled approval program OR 4.) A source map has been exported/imported using `export_source_map`/`import_source_map`""" ``` + +### Debug Mode and traces Field +When debug mode is active, the LogicError will contain a field named traces. This field will include raw simulate execution traces, providing a detailed account of the transaction simulation. These traces are crucial for diagnosing complex issues and are automatically included in all application client calls when debug mode is active. + +```{note} +Remember to enable debug mode (`config.debug = True`) to include raw simulate execution traces in the `LogicError`. +``` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 48d06df2..82c74f00 100644 --- a/poetry.lock +++ b/poetry.lock @@ -838,20 +838,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.36" +version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"}, - {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"}, + {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, + {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] [[package]] name = "html5lib" @@ -1138,16 +1138,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1991,7 +1981,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1999,15 +1988,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2024,7 +2006,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2032,7 +2013,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, diff --git a/src/algokit_utils/_simulate_315_compat.py b/src/algokit_utils/_simulate_315_compat.py deleted file mode 100644 index b6c65cee..00000000 --- a/src/algokit_utils/_simulate_315_compat.py +++ /dev/null @@ -1,73 +0,0 @@ -import base64 -from typing import Any - -from algosdk import encoding -from algosdk.atomic_transaction_composer import ( - AtomicTransactionComposer, - AtomicTransactionComposerStatus, - SimulateABIResult, - SimulateAtomicTransactionResponse, -) -from algosdk.error import AtomicTransactionComposerError -from algosdk.v2client.algod import AlgodClient - - -def simulate_atc_315(atc: AtomicTransactionComposer, client: AlgodClient) -> SimulateAtomicTransactionResponse: - """ - Ported from algosdk 2.1.2 - - Send the transaction group to the `simulate` endpoint and wait for results. - An error will be thrown if submission or execution fails. - The composer's status must be SUBMITTED or lower before calling this method, - since execution is only allowed once. - - Returns: - SimulateAtomicTransactionResponse: Object with simulation results for this - transaction group, a list of txIDs of the simulated transactions, - an array of results for each method call transaction in this group. - If a method has no return value (void), then the method results array - will contain None for that method's return value. - """ - - if atc.status > AtomicTransactionComposerStatus.SUBMITTED: - raise AtomicTransactionComposerError( # type: ignore[no-untyped-call] - "AtomicTransactionComposerStatus must be submitted or lower to simulate a group" - ) - - signed_txns = atc.gather_signatures() - txn = b"".join( - base64.b64decode(encoding.msgpack_encode(txn)) for txn in signed_txns # type: ignore[no-untyped-call] - ) - simulation_result = client.algod_request( - "POST", "/transactions/simulate", data=txn, headers={"Content-Type": "application/x-binary"} - ) - assert isinstance(simulation_result, dict) - - # Only take the first group in the simulate response - txn_group: dict[str, Any] = simulation_result["txn-groups"][0] - txn_results = txn_group["txn-results"] - - # Parse out abi results - results = [] - for method_index, method in atc.method_dict.items(): - tx_info = txn_results[method_index]["txn-result"] - - result = atc.parse_result(method, atc.tx_ids[method_index], tx_info) - sim_result = SimulateABIResult( - tx_id=result.tx_id, - raw_value=result.raw_value, - return_value=result.return_value, - decode_error=result.decode_error, - tx_info=result.tx_info, - method=result.method, - ) - results.append(sim_result) - - return SimulateAtomicTransactionResponse( - version=simulation_result.get("version", 0), - failure_message=txn_group.get("failure-message", ""), - failed_at=txn_group.get("failed-at"), - simulate_response=simulation_result, - tx_ids=atc.tx_ids, - results=results, - ) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index bd7bdab5..ac11bb74 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -4,7 +4,6 @@ import logging import re import typing -from http import HTTPStatus from math import ceil from pathlib import Path from typing import Any, Literal, cast, overload @@ -19,6 +18,7 @@ AccountTransactionSigner, AtomicTransactionComposer, AtomicTransactionResponse, + EmptySigner, LogicSigTransactionSigner, MultisigTransactionSigner, SimulateAtomicTransactionResponse, @@ -26,13 +26,13 @@ TransactionWithSigner, ) from algosdk.constants import APP_PAGE_MAX_SIZE -from algosdk.error import AlgodHTTPError from algosdk.logic import get_application_address from algosdk.source_map import SourceMap +from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig import algokit_utils.application_specification as au_spec import algokit_utils.deploy as au_deploy -from algokit_utils._simulate_315_compat import simulate_atc_315 +from algokit_utils.config import config from algokit_utils.logic_error import LogicError, parse_logic_error from algokit_utils.models import ( ABIArgsDict, @@ -56,6 +56,7 @@ logger = logging.getLogger(__name__) + """A dictionary `dict[str, Any]` representing ABI argument names and values""" __all__ = [ @@ -169,7 +170,6 @@ def __init__( self._approval_program: Program | None = None self._approval_source_map: SourceMap | None = None self._clear_program: Program | None = None - self._use_simulate_315 = False # flag to determine if old simulate 3.15 encoding should be used self.template_values: au_deploy.TemplateValueMapping = template_values or {} self.existing_deployments = existing_deployments @@ -870,36 +870,22 @@ def _check_is_compiled(self) -> tuple[Program, Program]: def _simulate_readonly_call( self, method: Method, atc: AtomicTransactionComposer ) -> ABITransactionResponse | TransactionResponse: - simulate_response = self._simulate_atc(atc) + simulate_response = _simulate_response(atc, self.algod_client) + traces = None + if config.debug: + traces = _create_simulate_traces(simulate_response) if simulate_response.failure_message: raise _try_convert_to_logic_error( simulate_response.failure_message, self.app_spec.approval_program, self._get_approval_source_map, + traces, ) or Exception( f"Simulate failed for readonly method {method.get_signature()}: {simulate_response.failure_message}" ) return TransactionResponse.from_atr(simulate_response) - def _simulate_atc(self, atc: AtomicTransactionComposer) -> SimulateAtomicTransactionResponse: - # TODO: remove this once 3.16 is in mainnet - # there was a breaking change in algod 3.16 to the simulate endpoint - # attempt to transparently handle this by calling the endpoint with the old behaviour if - # 3.15 is detected - if self._use_simulate_315: - return simulate_atc_315(atc, self.algod_client) - try: - return atc.simulate(self.algod_client) - except AlgodHTTPError as ex: - if ex.code == HTTPStatus.BAD_REQUEST.value and ( - "msgpack decode error [pos 12]: no matching struct field found when decoding stream map with key " - "txn-groups" in ex.args - ): - self._use_simulate_315 = True - return simulate_atc_315(atc, self.algod_client) - raise ex - def _load_reference_and_check_app_id(self) -> None: self._load_app_reference() self._check_app_id() @@ -1244,6 +1230,7 @@ def _try_convert_to_logic_error( source_ex: Exception | str, approval_program: str, approval_source_map: SourceMap | typing.Callable[[], SourceMap | None] | None = None, + simulate_traces: list | None = None, ) -> Exception | None: source_ex_str = str(source_ex) logic_error_data = parse_logic_error(source_ex_str) @@ -1254,6 +1241,7 @@ def _try_convert_to_logic_error( program=approval_program, source_map=approval_source_map() if callable(approval_source_map) else approval_source_map, **logic_error_data, + traces=simulate_traces, ) return None @@ -1277,12 +1265,56 @@ def execute_atc_with_logic_error( try: return atc.execute(algod_client, wait_rounds=wait_rounds) except Exception as ex: - logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map) + if config.debug: + simulate = _simulate_response(atc, algod_client) + traces = _create_simulate_traces(simulate) + else: + traces = None + logger.info("An error occurred while executing the transaction.") + logger.info("To see more details, enable debug mode by setting config.debug = True ") + + logic_error = _try_convert_to_logic_error(ex, approval_program, approval_source_map, traces) if logic_error: raise logic_error from ex raise ex +def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list[dict[str, Any]]: + traces = [] + if hasattr(simulate, "simulate_response") and hasattr(simulate, "failed_at") and simulate.failed_at: + for txn_group in simulate.simulate_response["txn-groups"]: + app_budget_added = txn_group.get("app-budget-added", None) + app_budget_consumed = txn_group.get("app-budget-consumed", None) + failure_message = txn_group.get("failure-message", None) + txn_result = txn_group.get("txn-results", [{}])[0] + exec_trace = txn_result.get("exec-trace", {}) + traces.append( + { + "app-budget-added": app_budget_added, + "app-budget-consumed": app_budget_consumed, + "failure-message": failure_message, + "exec-trace": exec_trace, + } + ) + return traces + + +def _simulate_response( + atc: AtomicTransactionComposer, algod_client: "AlgodClient" +) -> SimulateAtomicTransactionResponse: + unsigned_txn_groups = atc.build_group() + empty_signer = EmptySigner() + txn_list = [txn_group.txn for txn_group in unsigned_txn_groups] + fake_signed_transactions = empty_signer.sign_transactions(txn_list, []) + txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)] + trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True) + + simulate_request = SimulateRequest( + txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config + ) + return atc.simulate(algod_client, simulate_request) + + def _convert_transaction_parameters( args: TransactionParameters | TransactionParametersDict | None, ) -> CreateCallParameters: diff --git a/src/algokit_utils/config.py b/src/algokit_utils/config.py new file mode 100644 index 00000000..acd2635f --- /dev/null +++ b/src/algokit_utils/config.py @@ -0,0 +1,25 @@ +from collections.abc import Callable + + +class UpdatableConfig: + def __init__(self) -> None: + self._debug: bool = False + + @property + def debug(self) -> bool: + return self._debug + + def with_debug(self, lambda_func: Callable[[], None | str]) -> None: + original = self._debug + try: + self._debug = True + lambda_func() + finally: + self._debug = original + + def configure(self, *, debug: bool) -> None: + if debug is not None: + self._debug = debug + + +config = UpdatableConfig() diff --git a/src/algokit_utils/logic_error.py b/src/algokit_utils/logic_error.py index 3e495862..e9be4bce 100644 --- a/src/algokit_utils/logic_error.py +++ b/src/algokit_utils/logic_error.py @@ -47,6 +47,7 @@ def __init__( message: str, pc: int, logic_error: Exception | None = None, + traces: list | None = None, ): self.logic_error = logic_error self.logic_error_str = logic_error_str @@ -56,6 +57,7 @@ def __init__( self.transaction_id = transaction_id self.message = message self.pc = pc + self.traces = traces self.line_no = self.source_map.get_line_for_pc(self.pc) if self.source_map else None diff --git a/tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt b/tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt new file mode 100644 index 00000000..4af18322 --- /dev/null +++ b/tests/test_app_client_call.approvals/test_readonly_call_with_error_debug_mode_disabled.approved.txt @@ -0,0 +1 @@ +None \ No newline at end of file diff --git a/tests/test_app_client_call.py b/tests/test_app_client_call.py index 11ff5fbe..940a8560 100644 --- a/tests/test_app_client_call.py +++ b/tests/test_app_client_call.py @@ -1,5 +1,7 @@ +from collections.abc import Generator from pathlib import Path from typing import TYPE_CHECKING +from unittest.mock import Mock, patch import algokit_utils import pytest @@ -34,6 +36,15 @@ def client_fixture(algod_client: "AlgodClient", app_spec: ApplicationSpecificati return client +# This fixture is automatically applied to all application call tests. +# If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. +@pytest.fixture(autouse=True) +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + mock_config.debug = True + yield mock_config + + def test_app_client_from_app_spec_path(algod_client: "AlgodClient") -> None: client = ApplicationClient(algod_client, Path(__file__).parent / "app_client_test.json") @@ -276,3 +287,66 @@ def test_readonly_call_with_error_with_new_client_missing_source_map( ) check_output_stability(str(ex.value).replace(ex.value.transaction_id, "{txn}")) + + +def test_readonly_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixture: ApplicationClient) -> None: + mock_config.debug = False + with pytest.raises(algokit_utils.LogicError) as ex: + client_fixture.call( + "readonly", + error=1, + ) + assert ex.value.traces is None + mock_config.debug = True + + +def test_readonly_call_with_error_debug_mode_enabled(client_fixture: ApplicationClient) -> None: + with pytest.raises(algokit_utils.LogicError) as ex: + client_fixture.call( + "readonly", + error=1, + ) + + assert ex.value.traces is not None + assert ex.value.traces[0]["exec-trace"]["approval-program-trace"] is not None + + +def test_app_call_with_error_debug_mode_disabled(mock_config: Mock, client_fixture: ApplicationClient) -> None: + mock_config.debug = False + algokit_utils.ensure_funded( + client_fixture.algod_client, + algokit_utils.EnsureBalanceParameters( + account_to_fund=client_fixture.app_address, + min_spending_balance_micro_algos=200_000, + min_funding_increment_micro_algos=200_000, + ), + ) + with pytest.raises(algokit_utils.LogicError) as ex: + client_fixture.call( + "set_box", + name=b"ssss", + value="test", + ) + + assert ex.value.traces is None + mock_config.debug = True + + +def test_app_call_with_error_debug_mode_enabled(client_fixture: ApplicationClient) -> None: + algokit_utils.ensure_funded( + client_fixture.algod_client, + algokit_utils.EnsureBalanceParameters( + account_to_fund=client_fixture.app_address, + min_spending_balance_micro_algos=200_000, + min_funding_increment_micro_algos=200_000, + ), + ) + with pytest.raises(algokit_utils.LogicError) as ex: + client_fixture.call( + "set_box", + name=b"ssss", + value="test", + ) + + assert ex.value.traces is not None + assert ex.value.traces[0]["exec-trace"]["approval-program-trace"] is not None diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index 7f502633..d5a3ba0b 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -1,6 +1,8 @@ import logging import re +from collections.abc import Generator from enum import Enum +from unittest.mock import Mock, patch import pytest from algokit_utils import ( @@ -22,6 +24,15 @@ logger = logging.getLogger(__name__) +# This fixture is automatically applied to all application deployment tests. +# If you need to run a test without debug mode, you can reference this mock within the test and disable it explicitly. +@pytest.fixture(autouse=True) +def mock_config() -> Generator[Mock, None, None]: + with patch("algokit_utils.application_client.config", new_callable=Mock) as mock_config: + mock_config.debug = True + yield mock_config + + class DeployFixture: def __init__( self,