algokit-utils 4.1.0b8__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 +22 -0
- algokit_utils/applications/app_client.py +76 -0
- algokit_utils/transactions/transaction_composer.py +64 -7
- {algokit_utils-4.1.0b8.dist-info → algokit_utils-4.2.0.dist-info}/METADATA +1 -1
- {algokit_utils-4.1.0b8.dist-info → algokit_utils-4.2.0.dist-info}/RECORD +7 -7
- {algokit_utils-4.1.0b8.dist-info → algokit_utils-4.2.0.dist-info}/LICENSE +0 -0
- {algokit_utils-4.1.0b8.dist-info → algokit_utils-4.2.0.dist-info}/WHEEL +0 -0
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
|
|
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
|
|
1863
|
-
raise
|
|
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
|
-
|
|
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,
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
68
|
-
algokit_utils-4.
|
|
69
|
-
algokit_utils-4.
|
|
70
|
-
algokit_utils-4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|