algokit-utils 4.1.1b1__py3-none-any.whl → 4.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/algorand.py CHANGED
@@ -16,6 +16,7 @@ from algokit_utils.clients.client_manager import AlgoSdkClients, ClientManager
16
16
  from algokit_utils.models.network import AlgoClientConfigs, AlgoClientNetworkConfig
17
17
  from algokit_utils.protocols.account import TransactionSignerAccountProtocol
18
18
  from algokit_utils.transactions.transaction_composer import (
19
+ ErrorTransformer,
19
20
  TransactionComposer,
20
21
  )
21
22
  from algokit_utils.transactions.transaction_creator import AlgorandClientTransactionCreator
@@ -51,6 +52,7 @@ class AlgorandClient:
51
52
  self._cached_suggested_params_expiry: float | None = None
52
53
  self._cached_suggested_params_timeout: int = 3_000 # three seconds
53
54
  self._default_validity_window: int | None = None
55
+ self._error_transformers: set[ErrorTransformer] = set()
54
56
 
55
57
  def set_default_validity_window(self, validity_window: int) -> typing_extensions.Self:
56
58
  """
@@ -155,6 +157,25 @@ class AlgorandClient:
155
157
 
156
158
  return copy.deepcopy(self._cached_suggested_params)
157
159
 
160
+ def register_error_transformer(self, transformer: ErrorTransformer) -> typing_extensions.Self:
161
+ """Register a function that will be used to transform an error caught when simulating or executing
162
+ composed transaction groups made from `new_group`
163
+
164
+ :param transformer: The error transformer function
165
+ :return: The AlgorandClient so you can chain method calls
166
+ """
167
+ self._error_transformers.add(transformer)
168
+ return self
169
+
170
+ def unregister_error_transformer(self, transformer: ErrorTransformer) -> typing_extensions.Self:
171
+ """Unregister an error transformer function
172
+
173
+ :param transformer: The error transformer function to remove
174
+ :return: The AlgorandClient so you can chain method calls
175
+ """
176
+ self._error_transformers.discard(transformer)
177
+ return self
178
+
158
179
  def new_group(self) -> TransactionComposer:
159
180
  """
160
181
  Start a new `TransactionComposer` transaction group
@@ -169,6 +190,7 @@ class AlgorandClient:
169
190
  get_signer=lambda addr: self.account.get_signer(addr),
170
191
  get_suggested_params=self.get_suggested_params,
171
192
  default_validity_window=self._default_validity_window,
193
+ error_transformers=list(self._error_transformers),
172
194
  )
173
195
 
174
196
  @property
@@ -1305,11 +1305,15 @@ class AppClient:
1305
1305
  self._default_signer = params.default_signer
1306
1306
  self._approval_source_map = params.approval_source_map
1307
1307
  self._clear_source_map = params.clear_source_map
1308
+ self._last_compiled: dict[str, bytes] = {} # Track compiled programs for error filtering
1308
1309
  self._state_accessor = _StateAccessor(self)
1309
1310
  self._params_accessor = _MethodParamsBuilder(self)
1310
1311
  self._send_accessor = _TransactionSender(self)
1311
1312
  self._create_transaction_accessor = _TransactionCreator(self)
1312
1313
 
1314
+ # Register the error transformer to handle app-specific logic errors
1315
+ self._algorand.register_error_transformer(self._handle_call_errors_transform)
1316
+
1313
1317
  @property
1314
1318
  def algorand(self) -> AlgorandClient:
1315
1319
  """Get the Algorand client instance.
@@ -1735,6 +1739,10 @@ class AppClient:
1735
1739
  if result.compiled_clear:
1736
1740
  self._clear_source_map = result.compiled_clear.source_map
1737
1741
 
1742
+ # Store compiled programs for new app error filtering
1743
+ self._last_compiled["approval"] = result.approval_program
1744
+ self._last_compiled["clear"] = result.clear_state_program
1745
+
1738
1746
  return result
1739
1747
 
1740
1748
  def clone(
@@ -1951,6 +1959,74 @@ class AppClient:
1951
1959
  except Exception as e:
1952
1960
  raise self._expose_logic_error(e=e) from None
1953
1961
 
1962
+ def _is_new_app_error_for_this_app(self, error: Exception) -> bool:
1963
+ """Check if an error from a new app (app_id=0) is for this specific app by comparing program bytecode."""
1964
+ if not hasattr(error, "sent_transactions") or not error.sent_transactions:
1965
+ return False
1966
+
1967
+ # Find the transaction that caused the error
1968
+ txn = None
1969
+ for t in error.sent_transactions:
1970
+ if hasattr(t, "get_txid") and t.get_txid() in str(error):
1971
+ txn = t
1972
+ break
1973
+
1974
+ if not txn or not hasattr(txn, "application_call"):
1975
+ return False
1976
+
1977
+ def programs_defined_and_equal(a: bytes | None, b: bytes | None) -> bool:
1978
+ if a is None or b is None:
1979
+ return False
1980
+ return a == b
1981
+
1982
+ app_call = txn.application_call
1983
+ return programs_defined_and_equal(
1984
+ getattr(app_call, "clear_program", None), self._last_compiled.get("clear")
1985
+ ) and programs_defined_and_equal(
1986
+ getattr(app_call, "approval_program", None), self._last_compiled.get("approval")
1987
+ )
1988
+
1989
+ def _handle_call_errors_transform(self, error: Exception) -> Exception:
1990
+ """Error transformer function for app-specific logic errors.
1991
+
1992
+ This will be called by the transaction composer when errors occur during
1993
+ simulate or send operations to provide better error messages with source maps.
1994
+
1995
+ :param error: The error to potentially transform
1996
+ :return: The transformed error if it's an app logic error, otherwise the original error
1997
+ """
1998
+ try:
1999
+ # Check if this is a logic error that we can parse
2000
+ from algokit_utils.errors.logic_error import parse_logic_error
2001
+
2002
+ error_details = parse_logic_error(str(error))
2003
+ if not error_details:
2004
+ # Not a logic error, return unchanged
2005
+ return error
2006
+
2007
+ # Check if this error is for our app
2008
+ should_transform = False
2009
+
2010
+ if self._app_id == 0:
2011
+ # For new apps (app_id == 0), we can't use app ID filtering
2012
+ # Instead check the programs to identify if this is the correct app
2013
+ should_transform = self._is_new_app_error_for_this_app(error)
2014
+ else:
2015
+ # Only handle errors for this specific app
2016
+ app_id_string = f"app={self._app_id}"
2017
+ should_transform = app_id_string in str(error)
2018
+
2019
+ if not should_transform:
2020
+ # Error is not for this app, return unchanged
2021
+ return error
2022
+
2023
+ # This is a logic error for our app, transform it
2024
+ return self._expose_logic_error(e=error)
2025
+
2026
+ except Exception:
2027
+ # If transformation fails, return the original error
2028
+ return error
2029
+
1954
2030
  def _get_sender(self, sender: str | None) -> str:
1955
2031
  if not sender and not self._default_sender:
1956
2032
  raise Exception(
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import base64
4
4
  import json
5
5
  import re
6
+ from collections.abc import Callable
6
7
  from copy import deepcopy
7
8
  from dataclasses import dataclass
8
9
  from typing import TYPE_CHECKING, Any, TypedDict, Union, cast
@@ -31,15 +32,17 @@ from algokit_utils.models.transaction import SendParams, TransactionWrapper
31
32
  from algokit_utils.protocols.account import TransactionSignerAccountProtocol
32
33
 
33
34
  if TYPE_CHECKING:
34
- from collections.abc import Callable
35
-
36
35
  from algosdk.abi import Method
37
- from algosdk.v2client.algod import AlgodClient
38
36
  from algosdk.v2client.models import SimulateTraceConfig
39
37
 
40
38
  from algokit_utils.models.amount import AlgoAmount
41
39
  from algokit_utils.models.transaction import Arc2TransactionNote
42
40
 
41
+ # Type for error transformer function
42
+ # Note: The return type is Any rather than Exception to allow runtime validation
43
+ # that the transformer actually returns an Exception instance
44
+ ErrorTransformer = Callable[[Exception], Any]
45
+
43
46
 
44
47
  __all__ = [
45
48
  "AppCallMethodCallParams",
@@ -60,6 +63,7 @@ __all__ = [
60
63
  "AssetOptOutParams",
61
64
  "AssetTransferParams",
62
65
  "BuiltTransactions",
66
+ "ErrorTransformer",
63
67
  "MethodCallParams",
64
68
  "OfflineKeyRegistrationParams",
65
69
  "OnlineKeyRegistrationParams",
@@ -80,6 +84,27 @@ MAX_APP_CALL_FOREIGN_REFERENCES = 8
80
84
  MAX_APP_CALL_ACCOUNT_REFERENCES = 4
81
85
 
82
86
 
87
+ class InvalidErrorTransformerValueError(Exception):
88
+ """Raised when an error transformer returns a non-error value."""
89
+
90
+ def __init__(self, original_error: Exception, value: object) -> None:
91
+ super().__init__(
92
+ f"An error transformer returned a non-error value: {value}. "
93
+ f"The original error before any transformation: {original_error}"
94
+ )
95
+
96
+
97
+ class ErrorTransformerError(Exception):
98
+ """Raised when an error transformer throws an error."""
99
+
100
+ def __init__(self, original_error: Exception, cause: Exception) -> None:
101
+ super().__init__(
102
+ f"An error transformer threw an error: {cause}. "
103
+ f"The original error before any transformation: {original_error}"
104
+ )
105
+ self.__cause__ = cause
106
+
107
+
83
108
  @dataclass(kw_only=True, frozen=True)
84
109
  class _CommonTxnParams:
85
110
  sender: str
@@ -1333,6 +1358,7 @@ class TransactionComposer:
1333
1358
  defaults to using algod.suggested_params()
1334
1359
  :param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10
1335
1360
  :param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None
1361
+ :param error_transformers: Optional list of error transformers to use when an error is caught in simulate or send
1336
1362
  """
1337
1363
 
1338
1364
  def __init__(
@@ -1342,6 +1368,7 @@ class TransactionComposer:
1342
1368
  get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None,
1343
1369
  default_validity_window: int | None = None,
1344
1370
  app_manager: AppManager | None = None,
1371
+ error_transformers: list[ErrorTransformer] | None = None,
1345
1372
  ):
1346
1373
  # Map of transaction index in the atc to a max logical fee.
1347
1374
  # This is set using the value of either maxFee or staticFee.
@@ -1355,6 +1382,35 @@ class TransactionComposer:
1355
1382
  self._default_validity_window: int = default_validity_window or 10
1356
1383
  self._default_validity_window_is_explicit: bool = default_validity_window is not None
1357
1384
  self._app_manager = app_manager or AppManager(algod)
1385
+ self._error_transformers: list[ErrorTransformer] = error_transformers or []
1386
+
1387
+ def _transform_error(self, original_error: Exception) -> Exception:
1388
+ """Transform an error using registered error transformers.
1389
+
1390
+ :param original_error: The original error to transform
1391
+ :return: The transformed error or the original error if transformation fails
1392
+ """
1393
+ transformed_exception: Exception = original_error
1394
+
1395
+ for transformer in self._error_transformers:
1396
+ try:
1397
+ result = transformer(transformed_exception)
1398
+ if not isinstance(result, Exception):
1399
+ return InvalidErrorTransformerValueError(original_error, result)
1400
+ transformed_exception = result
1401
+ except Exception as error_from_transformer:
1402
+ return ErrorTransformerError(original_error, error_from_transformer)
1403
+
1404
+ return transformed_exception
1405
+
1406
+ def register_error_transformer(self, transformer: ErrorTransformer) -> TransactionComposer:
1407
+ """Register a function that will be used to transform an error caught when simulating or sending.
1408
+
1409
+ :param transformer: The error transformer function
1410
+ :return: The composer so you can chain method calls
1411
+ """
1412
+ self._error_transformers.append(transformer)
1413
+ return self
1358
1414
 
1359
1415
  def add_transaction(
1360
1416
  self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None
@@ -1828,7 +1884,7 @@ class TransactionComposer:
1828
1884
 
1829
1885
  :param params: Parameters for the send operation
1830
1886
  :return: The transaction send results
1831
- :raises Exception: If the transaction fails
1887
+ :raises self._transform_error: If the transaction fails (may be transformed by error transformers)
1832
1888
  """
1833
1889
  group = self.build().transactions
1834
1890
 
@@ -1859,8 +1915,8 @@ class TransactionComposer:
1859
1915
  max_fees=self._txn_max_fees,
1860
1916
  ),
1861
1917
  )
1862
- except algosdk.error.AlgodHTTPError as e:
1863
- raise Exception(f"Transaction failed: {e}") from e
1918
+ except Exception as original_error:
1919
+ raise self._transform_error(original_error) from original_error
1864
1920
 
1865
1921
  def _handle_simulate_error(self, simulate_response: SimulateAtomicTransactionResponse) -> None:
1866
1922
  # const failedGroup = simulateResponse?.txnGroups[0]
@@ -1872,7 +1928,8 @@ class TransactionComposer:
1872
1928
  f"Transaction failed at transaction(s) {', '.join(failed_at) if failed_at else 'N/A'} in the group. "
1873
1929
  f"{failure_message}"
1874
1930
  )
1875
- raise Exception(error_message)
1931
+ original_error = Exception(error_message)
1932
+ raise self._transform_error(original_error) from original_error
1876
1933
 
1877
1934
  def simulate(
1878
1935
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: algokit-utils
3
- Version: 4.1.1b1
3
+ Version: 4.2.0
4
4
  Summary: Utilities for Algorand development for use by AlgoKit
5
5
  License: MIT
6
6
  Author: Algorand Foundation
@@ -16,12 +16,12 @@ algokit_utils/account.py,sha256=gyGrBSoafUh8WV677IzYGkYoxtzzElsgxGMp4SgA4pk,410
16
16
  algokit_utils/accounts/__init__.py,sha256=_LyY0se6TaQOes7vAcmbpt6pmG4VKlzfTt37-IjwimA,138
17
17
  algokit_utils/accounts/account_manager.py,sha256=dIECz1QzkvV4bzsqoUJ4cRzJ6evHcRM2TpQpBf8l0ng,42242
18
18
  algokit_utils/accounts/kmd_account_manager.py,sha256=qPlklyoIk0B6B78GZX-VKwSgmfZBKgp5U2k51fg1YXg,6459
19
- algokit_utils/algorand.py,sha256=OvYMolOGK-tupKLDohtP_P59jlELIWW2hRqf1CYfrns,13732
19
+ algokit_utils/algorand.py,sha256=gRRWEwThmvAzS5ZKmlUHcqKxFz3XXMqairwi93nTatg,14730
20
20
  algokit_utils/application_client.py,sha256=5UIxXIBjukjRyjZPCeXmaNlAftbb3TziV7EfBolW79k,337
21
21
  algokit_utils/application_specification.py,sha256=wV0H088IudMqlxsW-gsZIfJyKA4e-zVwxJ-cR__ouBA,1379
22
22
  algokit_utils/applications/__init__.py,sha256=NGjhpBeExsQZOAYCT2QUFag1xuKoFiX-Ux5SR2GNzd8,452
23
23
  algokit_utils/applications/abi.py,sha256=OjTdn4szJPPeC8XmosdDYtkIIVgQSWAnqz2DHw5OH9g,10117
24
- algokit_utils/applications/app_client.py,sha256=891BMnUOIOenEWgeelSIMgi0Wj4kggluWiKAu6-GYrE,88208
24
+ algokit_utils/applications/app_client.py,sha256=6D6zZimJlRD6-EsNXfyC7uXPxYAKvC0JSrp-qcgcYPY,91494
25
25
  algokit_utils/applications/app_deployer.py,sha256=xJCu7SU66OTg5misSbSF0QI8abRB-DWAwAVKd1kNcPI,30685
26
26
  algokit_utils/applications/app_factory.py,sha256=jVAzoK1J9S-BTGHA5BLxT-cl0pWhPdf222W4fYpFihE,45352
27
27
  algokit_utils/applications/app_manager.py,sha256=8bboIswlwBQhPIqilSBMaxd83yHjIpkloezmtgcAdZY,22301
@@ -61,10 +61,10 @@ algokit_utils/protocols/account.py,sha256=CowaVY7ErBP84TWBHNvBjkZy18whPb8HIlMZtJ
61
61
  algokit_utils/protocols/typed_clients.py,sha256=UrQrHbN2SvS8pEFJ8JQodvouoWeBrQOQGZGyBQx1KLM,3322
62
62
  algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  algokit_utils/transactions/__init__.py,sha256=7fYF3m6DyOGzbV36MT5svo0wSkj9AIz496kWgIWSAlk,225
64
- algokit_utils/transactions/transaction_composer.py,sha256=eK-6l2W1CkbKID1ezi-zCs_TxA2i5ZTm3YTl8sT2Zfw,103995
64
+ algokit_utils/transactions/transaction_composer.py,sha256=iOIa_9hknbpCQc2hRyX-aBoHG-NGRxGnEAJstwGVKgw,106645
65
65
  algokit_utils/transactions/transaction_creator.py,sha256=cuP6Xm-fhGoCc2FNSbLiEg3iQRwW38rfdTzsqPyEcpM,29053
66
66
  algokit_utils/transactions/transaction_sender.py,sha256=Wi3ws9S-Df1JeTlaSTXmq-WS24Gsq7WGsKk1B0z23ao,50117
67
- algokit_utils-4.1.1b1.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
68
- algokit_utils-4.1.1b1.dist-info/METADATA,sha256=aq5pFQLu2tIVXOqbt9WKKfcfek9y4KDPx5DfFx79eL0,2421
69
- algokit_utils-4.1.1b1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
70
- algokit_utils-4.1.1b1.dist-info/RECORD,,
67
+ algokit_utils-4.2.0.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
68
+ algokit_utils-4.2.0.dist-info/METADATA,sha256=_65fNn_a68EmdV4pGJgXsA2EtFo-Qj1JrivWZ6nrGOI,2419
69
+ algokit_utils-4.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
70
+ algokit_utils-4.2.0.dist-info/RECORD,,