algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.0b2__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.

Files changed (70) hide show
  1. algokit_utils/__init__.py +23 -181
  2. algokit_utils/_debugging.py +89 -45
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +19 -18
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +24 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1471 -0
  8. algokit_utils/_legacy_v2/application_specification.py +21 -0
  9. algokit_utils/_legacy_v2/asset.py +168 -0
  10. algokit_utils/_legacy_v2/common.py +28 -0
  11. algokit_utils/_legacy_v2/deploy.py +822 -0
  12. algokit_utils/_legacy_v2/logic_error.py +14 -0
  13. algokit_utils/{models.py → _legacy_v2/models.py} +16 -45
  14. algokit_utils/_legacy_v2/network_clients.py +140 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +909 -0
  18. algokit_utils/accounts/kmd_account_manager.py +159 -0
  19. algokit_utils/algorand.py +265 -0
  20. algokit_utils/application_client.py +9 -1447
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +276 -0
  24. algokit_utils/applications/app_client.py +2056 -0
  25. algokit_utils/applications/app_deployer.py +600 -0
  26. algokit_utils/applications/app_factory.py +826 -0
  27. algokit_utils/applications/app_manager.py +470 -0
  28. algokit_utils/applications/app_spec/__init__.py +2 -0
  29. algokit_utils/applications/app_spec/arc32.py +207 -0
  30. algokit_utils/applications/app_spec/arc56.py +1023 -0
  31. algokit_utils/applications/enums.py +40 -0
  32. algokit_utils/asset.py +32 -168
  33. algokit_utils/assets/__init__.py +1 -0
  34. algokit_utils/assets/asset_manager.py +320 -0
  35. algokit_utils/beta/_utils.py +36 -0
  36. algokit_utils/beta/account_manager.py +4 -195
  37. algokit_utils/beta/algorand_client.py +4 -314
  38. algokit_utils/beta/client_manager.py +5 -74
  39. algokit_utils/beta/composer.py +5 -712
  40. algokit_utils/clients/__init__.py +2 -0
  41. algokit_utils/clients/client_manager.py +656 -0
  42. algokit_utils/clients/dispenser_api_client.py +192 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +71 -18
  45. algokit_utils/deploy.py +7 -894
  46. algokit_utils/dispenser_api.py +8 -176
  47. algokit_utils/errors/__init__.py +1 -0
  48. algokit_utils/errors/logic_error.py +121 -0
  49. algokit_utils/logic_error.py +7 -82
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +193 -0
  52. algokit_utils/models/amount.py +198 -0
  53. algokit_utils/models/application.py +61 -0
  54. algokit_utils/models/network.py +25 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +59 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -128
  59. algokit_utils/protocols/__init__.py +2 -0
  60. algokit_utils/protocols/account.py +22 -0
  61. algokit_utils/protocols/typed_clients.py +108 -0
  62. algokit_utils/transactions/__init__.py +3 -0
  63. algokit_utils/transactions/transaction_composer.py +2293 -0
  64. algokit_utils/transactions/transaction_creator.py +156 -0
  65. algokit_utils/transactions/transaction_sender.py +574 -0
  66. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/METADATA +11 -7
  67. algokit_utils-3.0.0b2.dist-info/RECORD +70 -0
  68. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,2056 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import copy
5
+ import json
6
+ import os
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass, fields
9
+ from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar
10
+
11
+ import algosdk
12
+ from algosdk.source_map import SourceMap
13
+ from algosdk.transaction import OnComplete, Transaction
14
+
15
+ from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps
16
+ from algokit_utils.applications.abi import (
17
+ ABIReturn,
18
+ ABIStruct,
19
+ ABIType,
20
+ ABIValue,
21
+ Arc56ReturnValueType,
22
+ BoxABIValue,
23
+ get_abi_decoded_value,
24
+ get_abi_encoded_value,
25
+ get_abi_tuple_from_abi_struct,
26
+ )
27
+ from algokit_utils.applications.app_spec.arc32 import Arc32Contract
28
+ from algokit_utils.applications.app_spec.arc56 import (
29
+ Arc56Contract,
30
+ Method,
31
+ PcOffsetMethod,
32
+ ProgramSourceInfo,
33
+ SourceInfo,
34
+ StorageKey,
35
+ StorageMap,
36
+ )
37
+ from algokit_utils.config import config
38
+ from algokit_utils.errors.logic_error import LogicError, parse_logic_error
39
+ from algokit_utils.models.application import (
40
+ AppSourceMaps,
41
+ AppState,
42
+ CompiledTeal,
43
+ )
44
+ from algokit_utils.models.state import BoxName, BoxValue
45
+ from algokit_utils.models.transaction import SendParams
46
+ from algokit_utils.protocols.account import TransactionSignerAccountProtocol
47
+ from algokit_utils.transactions.transaction_composer import (
48
+ AppCallMethodCallParams,
49
+ AppCallParams,
50
+ AppCreateSchema,
51
+ AppDeleteMethodCallParams,
52
+ AppMethodCallTransactionArgument,
53
+ AppUpdateMethodCallParams,
54
+ AppUpdateParams,
55
+ BuiltTransactions,
56
+ PaymentParams,
57
+ )
58
+ from algokit_utils.transactions.transaction_sender import (
59
+ SendAppTransactionResult,
60
+ SendAppUpdateTransactionResult,
61
+ SendSingleTransactionResult,
62
+ )
63
+
64
+ if TYPE_CHECKING:
65
+ from collections.abc import Callable
66
+
67
+ from algosdk.atomic_transaction_composer import TransactionSigner
68
+
69
+ from algokit_utils.algorand import AlgorandClient
70
+ from algokit_utils.applications.app_deployer import ApplicationLookup
71
+ from algokit_utils.applications.app_manager import AppManager
72
+ from algokit_utils.models.amount import AlgoAmount
73
+ from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams
74
+
75
+ __all__ = [
76
+ "AppClient",
77
+ "AppClientBareCallCreateParams",
78
+ "AppClientBareCallParams",
79
+ "AppClientCallParams",
80
+ "AppClientCompilationParams",
81
+ "AppClientCompilationResult",
82
+ "AppClientCreateSchema",
83
+ "AppClientMethodCallCreateParams",
84
+ "AppClientMethodCallParams",
85
+ "AppClientParams",
86
+ "AppSourceMaps",
87
+ "BaseAppClientMethodCallParams",
88
+ "CreateOnComplete",
89
+ "FundAppAccountParams",
90
+ "get_constant_block_offset",
91
+ ]
92
+
93
+ # TEAL opcodes for constant blocks
94
+ BYTE_CBLOCK = 38 # bytecblock opcode
95
+ INT_CBLOCK = 32 # intcblock opcode
96
+
97
+ T = TypeVar("T") # For generic return type in _handle_call_errors
98
+
99
+ # Sentinel to detect missing arguments in clone() method of AppClient
100
+ _MISSING = object()
101
+
102
+
103
+ def get_constant_block_offset(program: bytes) -> int: # noqa: C901
104
+ """Calculate the offset after constant blocks in TEAL program.
105
+
106
+ Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations.
107
+
108
+ :param program: The compiled TEAL program as bytes
109
+ :return: The maximum offset position after any constant block operations
110
+ """
111
+ bytes_list = list(program)
112
+ program_size = len(bytes_list)
113
+
114
+ # Remove version byte
115
+ bytes_list.pop(0)
116
+
117
+ # Track offsets
118
+ bytecblock_offset: int | None = None
119
+ intcblock_offset: int | None = None
120
+
121
+ while bytes_list:
122
+ # Get current byte
123
+ byte = bytes_list.pop(0)
124
+
125
+ # Check if byte is a constant block opcode
126
+ if byte in (BYTE_CBLOCK, INT_CBLOCK):
127
+ is_bytecblock = byte == BYTE_CBLOCK
128
+
129
+ # Get number of values in constant block
130
+ if not bytes_list:
131
+ break
132
+ values_remaining = bytes_list.pop(0)
133
+
134
+ # Process each value in the block
135
+ for _ in range(values_remaining):
136
+ if is_bytecblock:
137
+ # For bytecblock, next byte is length of element
138
+ if not bytes_list:
139
+ break
140
+ length = bytes_list.pop(0)
141
+ # Remove the bytes for this element
142
+ bytes_list = bytes_list[length:]
143
+ else:
144
+ # For intcblock, read until we find end of uvarint (MSB not set)
145
+ while bytes_list:
146
+ byte = bytes_list.pop(0)
147
+ if not (byte & 0x80): # Check if MSB is not set
148
+ break
149
+
150
+ # Update appropriate offset
151
+ if is_bytecblock:
152
+ bytecblock_offset = program_size - len(bytes_list) - 1
153
+ else:
154
+ intcblock_offset = program_size - len(bytes_list) - 1
155
+
156
+ # If next byte isn't a constant block opcode, we're done
157
+ if not bytes_list or bytes_list[0] not in (BYTE_CBLOCK, INT_CBLOCK):
158
+ break
159
+
160
+ # Return maximum offset
161
+ return max(bytecblock_offset or 0, intcblock_offset or 0)
162
+
163
+
164
+ CreateOnComplete = Literal[
165
+ OnComplete.NoOpOC,
166
+ OnComplete.UpdateApplicationOC,
167
+ OnComplete.DeleteApplicationOC,
168
+ OnComplete.OptInOC,
169
+ OnComplete.CloseOutOC,
170
+ ]
171
+
172
+
173
+ @dataclass(kw_only=True, frozen=True)
174
+ class AppClientCompilationResult:
175
+ """Result of compiling an application's TEAL code.
176
+
177
+ Contains the compiled approval and clear state programs along with optional compilation artifacts.
178
+
179
+ :ivar approval_program: The compiled approval program bytes
180
+ :ivar clear_state_program: The compiled clear state program bytes
181
+ :ivar compiled_approval: Optional compilation artifacts for approval program
182
+ :ivar compiled_clear: Optional compilation artifacts for clear state program
183
+ """
184
+
185
+ approval_program: bytes
186
+ clear_state_program: bytes
187
+ compiled_approval: CompiledTeal | None = None
188
+ compiled_clear: CompiledTeal | None = None
189
+
190
+
191
+ class AppClientCompilationParams(TypedDict, total=False):
192
+ """Parameters for compiling an application's TEAL code.
193
+
194
+ :ivar deploy_time_params: Optional template parameters to use during compilation
195
+ :ivar updatable: Optional flag indicating if app should be updatable
196
+ :ivar deletable: Optional flag indicating if app should be deletable
197
+ """
198
+
199
+ deploy_time_params: TealTemplateParams | None
200
+ updatable: bool | None
201
+ deletable: bool | None
202
+
203
+
204
+ @dataclass(kw_only=True)
205
+ class FundAppAccountParams:
206
+ """Parameters for funding an application's account.
207
+
208
+ :ivar sender: Optional sender address
209
+ :ivar signer: Optional transaction signer
210
+ :ivar rekey_to: Optional address to rekey to
211
+ :ivar note: Optional transaction note
212
+ :ivar lease: Optional lease
213
+ :ivar static_fee: Optional static fee
214
+ :ivar extra_fee: Optional extra fee
215
+ :ivar max_fee: Optional maximum fee
216
+ :ivar validity_window: Optional validity window in rounds
217
+ :ivar first_valid_round: Optional first valid round
218
+ :ivar last_valid_round: Optional last valid round
219
+ :ivar amount: Amount to fund
220
+ :ivar close_remainder_to: Optional address to close remainder to
221
+ :ivar on_complete: Optional on complete action
222
+ """
223
+
224
+ sender: str | None = None
225
+ signer: TransactionSigner | None = None
226
+ rekey_to: str | None = None
227
+ note: bytes | None = None
228
+ lease: bytes | None = None
229
+ static_fee: AlgoAmount | None = None
230
+ extra_fee: AlgoAmount | None = None
231
+ max_fee: AlgoAmount | None = None
232
+ validity_window: int | None = None
233
+ first_valid_round: int | None = None
234
+ last_valid_round: int | None = None
235
+ amount: AlgoAmount
236
+ close_remainder_to: str | None = None
237
+ on_complete: algosdk.transaction.OnComplete | None = None
238
+
239
+
240
+ @dataclass(kw_only=True)
241
+ class AppClientCallParams:
242
+ """Parameters for calling an application.
243
+
244
+ :ivar method: Optional ABI method name or signature
245
+ :ivar args: Optional arguments to pass to method
246
+ :ivar boxes: Optional box references to load
247
+ :ivar accounts: Optional account addresses to load
248
+ :ivar apps: Optional app IDs to load
249
+ :ivar assets: Optional asset IDs to load
250
+ :ivar lease: Optional lease
251
+ :ivar sender: Optional sender address
252
+ :ivar note: Optional transaction note
253
+ :ivar send_params: Optional parameters to control transaction sending
254
+ """
255
+
256
+ method: str | None = None
257
+ args: list | None = None
258
+ boxes: list | None = None
259
+ accounts: list[str] | None = None
260
+ apps: list[int] | None = None
261
+ assets: list[int] | None = None
262
+ lease: (str | bytes) | None = None
263
+ sender: str | None = None
264
+ note: (bytes | dict | str) | None = None
265
+ send_params: dict | None = None
266
+
267
+
268
+ ArgsT = TypeVar("ArgsT")
269
+ MethodT = TypeVar("MethodT")
270
+
271
+
272
+ @dataclass(kw_only=True, frozen=True)
273
+ class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT]):
274
+ """Base parameters for application method calls.
275
+
276
+ :ivar method: Method to call
277
+ :ivar args: Optional arguments to pass to method
278
+ :ivar account_references: Optional account references
279
+ :ivar app_references: Optional application references
280
+ :ivar asset_references: Optional asset references
281
+ :ivar box_references: Optional box references
282
+ :ivar extra_fee: Optional extra fee
283
+ :ivar first_valid_round: Optional first valid round
284
+ :ivar lease: Optional lease
285
+ :ivar max_fee: Optional maximum fee
286
+ :ivar note: Optional note
287
+ :ivar rekey_to: Optional rekey to address
288
+ :ivar sender: Optional sender address
289
+ :ivar signer: Optional transaction signer
290
+ :ivar static_fee: Optional static fee
291
+ :ivar validity_window: Optional validity window
292
+ :ivar last_valid_round: Optional last valid round
293
+ :ivar on_complete: Optional on complete action
294
+ """
295
+
296
+ method: MethodT
297
+ args: ArgsT | None = None
298
+ account_references: list[str] | None = None
299
+ app_references: list[int] | None = None
300
+ asset_references: list[int] | None = None
301
+ box_references: Sequence[BoxReference | BoxIdentifier] | None = None
302
+ extra_fee: AlgoAmount | None = None
303
+ first_valid_round: int | None = None
304
+ lease: bytes | None = None
305
+ max_fee: AlgoAmount | None = None
306
+ note: bytes | None = None
307
+ rekey_to: str | None = None
308
+ sender: str | None = None
309
+ signer: TransactionSigner | None = None
310
+ static_fee: AlgoAmount | None = None
311
+ validity_window: int | None = None
312
+ last_valid_round: int | None = None
313
+ on_complete: algosdk.transaction.OnComplete | None = None
314
+
315
+
316
+ @dataclass(kw_only=True, frozen=True)
317
+ class AppClientMethodCallParams(
318
+ BaseAppClientMethodCallParams[
319
+ Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None],
320
+ str,
321
+ ]
322
+ ):
323
+ """Parameters for application method calls."""
324
+
325
+
326
+ @dataclass(kw_only=True, frozen=True)
327
+ class AppClientBareCallParams:
328
+ """Parameters for bare application calls.
329
+
330
+ :ivar signer: Optional transaction signer
331
+ :ivar rekey_to: Optional rekey to address
332
+ :ivar lease: Optional lease
333
+ :ivar static_fee: Optional static fee
334
+ :ivar extra_fee: Optional extra fee
335
+ :ivar max_fee: Optional maximum fee
336
+ :ivar validity_window: Optional validity window
337
+ :ivar first_valid_round: Optional first valid round
338
+ :ivar last_valid_round: Optional last valid round
339
+ :ivar sender: Optional sender address
340
+ :ivar note: Optional note
341
+ :ivar args: Optional arguments
342
+ :ivar account_references: Optional account references
343
+ :ivar app_references: Optional application references
344
+ :ivar asset_references: Optional asset references
345
+ :ivar box_references: Optional box references
346
+ """
347
+
348
+ signer: TransactionSigner | None = None
349
+ rekey_to: str | None = None
350
+ lease: bytes | None = None
351
+ static_fee: AlgoAmount | None = None
352
+ extra_fee: AlgoAmount | None = None
353
+ max_fee: AlgoAmount | None = None
354
+ validity_window: int | None = None
355
+ first_valid_round: int | None = None
356
+ last_valid_round: int | None = None
357
+ sender: str | None = None
358
+ note: bytes | None = None
359
+ args: list[bytes] | None = None
360
+ account_references: list[str] | None = None
361
+ app_references: list[int] | None = None
362
+ asset_references: list[int] | None = None
363
+ box_references: list[BoxReference | BoxIdentifier] | None = None
364
+
365
+
366
+ @dataclass(frozen=True)
367
+ class AppClientCreateSchema:
368
+ """Schema for application creation.
369
+
370
+ :ivar extra_program_pages: Optional number of extra program pages
371
+ :ivar schema: Optional application creation schema
372
+ """
373
+
374
+ extra_program_pages: int | None = None
375
+ schema: AppCreateSchema | None = None
376
+
377
+
378
+ @dataclass(frozen=True)
379
+ class AppClientBareCallCreateParams(AppClientCreateSchema, AppClientBareCallParams):
380
+ """Parameters for creating application with bare call."""
381
+
382
+ on_complete: OnComplete | None = None
383
+
384
+
385
+ @dataclass(frozen=True)
386
+ class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams):
387
+ """Parameters for creating application with method call."""
388
+
389
+ on_complete: CreateOnComplete | None = None
390
+
391
+
392
+ class _AppClientStateMethods:
393
+ def __init__(
394
+ self,
395
+ *,
396
+ get_all: Callable[[], dict[str, Any]],
397
+ get_value: Callable[[str, dict[str, AppState] | None], ABIValue | None],
398
+ get_map_value: Callable[[str, bytes | Any, dict[str, AppState] | None], Any],
399
+ get_map: Callable[[str], dict[str, ABIValue]],
400
+ ) -> None:
401
+ self._get_all = get_all
402
+ self._get_value = get_value
403
+ self._get_map_value = get_map_value
404
+ self._get_map = get_map
405
+
406
+ def get_all(self) -> dict[str, Any]:
407
+ return self._get_all()
408
+
409
+ def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None:
410
+ return self._get_value(name, app_state)
411
+
412
+ def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401
413
+ return self._get_map_value(map_name, key, app_state)
414
+
415
+ def get_map(self, map_name: str) -> dict[str, ABIValue]:
416
+ return self._get_map(map_name)
417
+
418
+
419
+ class _AppClientBoxMethods:
420
+ def __init__(
421
+ self,
422
+ *,
423
+ get_all: Callable[[], dict[str, Any]],
424
+ get_value: Callable[[str], ABIValue | None],
425
+ get_map_value: Callable[[str, bytes | Any], Any],
426
+ get_map: Callable[[str], dict[str, ABIValue]],
427
+ ) -> None:
428
+ self._get_all = get_all
429
+ self._get_value = get_value
430
+ self._get_map_value = get_map_value
431
+ self._get_map = get_map
432
+
433
+ def get_all(self) -> dict[str, Any]:
434
+ return self._get_all()
435
+
436
+ def get_value(self, name: str) -> ABIValue | None:
437
+ return self._get_value(name)
438
+
439
+ def get_map_value(self, map_name: str, key: bytes | Any) -> Any: # noqa: ANN401
440
+ return self._get_map_value(map_name, key)
441
+
442
+ def get_map(self, map_name: str) -> dict[str, ABIValue]:
443
+ return self._get_map(map_name)
444
+
445
+
446
+ class _StateAccessor:
447
+ def __init__(self, client: AppClient) -> None:
448
+ self._client = client
449
+ self._algorand = client._algorand
450
+ self._app_id = client._app_id
451
+ self._app_spec = client._app_spec
452
+
453
+ def local_state(self, address: str) -> _AppClientStateMethods:
454
+ """Methods to access local state for the current app for a given address"""
455
+ return self._get_state_methods(
456
+ state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address),
457
+ key_getter=lambda: self._app_spec.state.keys.local_state,
458
+ map_getter=lambda: self._app_spec.state.maps.local_state,
459
+ )
460
+
461
+ @property
462
+ def global_state(self) -> _AppClientStateMethods:
463
+ """Methods to access global state for the current app"""
464
+ return self._get_state_methods(
465
+ state_getter=lambda: self._algorand.app.get_global_state(self._app_id),
466
+ key_getter=lambda: self._app_spec.state.keys.global_state,
467
+ map_getter=lambda: self._app_spec.state.maps.global_state,
468
+ )
469
+
470
+ @property
471
+ def box(self) -> _AppClientBoxMethods:
472
+ """Methods to access box storage for the current app"""
473
+ return self._get_box_methods()
474
+
475
+ def _get_box_methods(self) -> _AppClientBoxMethods:
476
+ def get_all() -> dict[str, Any]:
477
+ """Returns all single-key box values in a dict keyed by the key name."""
478
+ return {key: get_value(key) for key in self._app_spec.state.keys.box}
479
+
480
+ def get_value(name: str) -> ABIValue | None:
481
+ """Returns a single box value for the current app with the value a decoded ABI value.
482
+
483
+ :param name: The name of the box value to retrieve
484
+ :return: The decoded ABI value from the box storage, or None if not found
485
+ """
486
+ metadata = self._app_spec.state.keys.box[name]
487
+ value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(metadata.key))
488
+ return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs)
489
+
490
+ def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401
491
+ """Get a value from a box map.
492
+
493
+ Retrieves a value from a box map storage using the provided map name and key.
494
+
495
+ :param map_name: The name of the map to read from
496
+ :param key: The key within the map (without any map prefix) as either bytes or a value
497
+ that will be converted to bytes by encoding it using the specified ABI key type
498
+ :return: The decoded value from the box map storage
499
+ """
500
+ metadata = self._app_spec.state.maps.box[map_name]
501
+ prefix = base64.b64decode(metadata.prefix or "")
502
+ encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs)
503
+ full_key = base64.b64encode(prefix + encoded_key).decode("utf-8")
504
+ value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(full_key))
505
+ return get_abi_decoded_value(value, metadata.value_type, self._app_spec.structs)
506
+
507
+ def get_map(map_name: str) -> dict[str, ABIValue]:
508
+ """Get all key-value pairs from a box map.
509
+
510
+ Retrieves all key-value pairs stored in a box map for the current app.
511
+
512
+ :param map_name: The name of the map to read from
513
+ :return: A dictionary mapping string keys to their corresponding ABI-decoded values
514
+ :raises ValueError: If there is an error decoding any key or value in the map
515
+ """
516
+ metadata = self._app_spec.state.maps.box[map_name]
517
+ prefix = base64.b64decode(metadata.prefix or "")
518
+ box_names = self._algorand.app.get_box_names(self._app_id)
519
+
520
+ result = {}
521
+ for box in box_names:
522
+ if not box.name_raw.startswith(prefix):
523
+ continue
524
+
525
+ encoded_key = prefix + box.name_raw
526
+ base64_key = base64.b64encode(encoded_key).decode("utf-8")
527
+
528
+ try:
529
+ key = get_abi_decoded_value(box.name_raw[len(prefix) :], metadata.key_type, self._app_spec.structs)
530
+ value = get_abi_decoded_value(
531
+ self._algorand.app.get_box_value(self._app_id, base64.b64decode(base64_key)),
532
+ metadata.value_type,
533
+ self._app_spec.structs,
534
+ )
535
+ result[str(key)] = value
536
+ except Exception as e:
537
+ if "Failed to decode key" in str(e):
538
+ raise ValueError(f"Failed to decode key {base64_key}") from e
539
+ raise ValueError(f"Failed to decode value for key {base64_key}") from e
540
+
541
+ return result
542
+
543
+ return _AppClientBoxMethods(
544
+ get_all=get_all,
545
+ get_value=get_value,
546
+ get_map_value=get_map_value,
547
+ get_map=get_map,
548
+ )
549
+
550
+ def _get_state_methods( # noqa: C901
551
+ self,
552
+ state_getter: Callable[[], dict[str, AppState]],
553
+ key_getter: Callable[[], dict[str, StorageKey]],
554
+ map_getter: Callable[[], dict[str, StorageMap]],
555
+ ) -> _AppClientStateMethods:
556
+ def get_all() -> dict[str, Any]:
557
+ state = state_getter()
558
+ keys = key_getter()
559
+ return {key: get_value(key, state) for key in keys}
560
+
561
+ def get_value(name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None:
562
+ state = app_state or state_getter()
563
+ key_info = key_getter()[name]
564
+ value = next((s for s in state.values() if s.key_base64 == key_info.key), None)
565
+
566
+ if value and value.value_raw:
567
+ return get_abi_decoded_value(value.value_raw, key_info.value_type, self._app_spec.structs)
568
+
569
+ return value.value if value else None
570
+
571
+ def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401
572
+ state = app_state or state_getter()
573
+ metadata = map_getter()[map_name]
574
+
575
+ prefix = base64.b64decode(metadata.prefix or "")
576
+ encoded_key = get_abi_encoded_value(key, metadata.key_type, self._app_spec.structs)
577
+ full_key = base64.b64encode(prefix + encoded_key).decode("utf-8")
578
+ value = next((s for s in state.values() if s.key_base64 == full_key), None)
579
+ if value and value.value_raw:
580
+ return get_abi_decoded_value(value.value_raw, metadata.value_type, self._app_spec.structs)
581
+ return value.value if value else None
582
+
583
+ def get_map(map_name: str) -> dict[str, ABIValue]:
584
+ state = state_getter()
585
+ metadata = map_getter()[map_name]
586
+
587
+ prefix = base64.b64decode(metadata.prefix or "").decode("utf-8")
588
+
589
+ prefixed_state = {k: v for k, v in state.items() if k.startswith(prefix)}
590
+
591
+ decoded_map = {}
592
+
593
+ for key_encoded, value in prefixed_state.items():
594
+ key_bytes = key_encoded[len(prefix) :]
595
+ try:
596
+ decoded_key = get_abi_decoded_value(key_bytes, metadata.key_type, self._app_spec.structs)
597
+ except Exception as e:
598
+ raise ValueError(f"Failed to decode key {key_encoded}") from e
599
+
600
+ try:
601
+ if value and value.value_raw:
602
+ decoded_value = get_abi_decoded_value(
603
+ value.value_raw, metadata.value_type, self._app_spec.structs
604
+ )
605
+ else:
606
+ decoded_value = get_abi_decoded_value(value.value, metadata.value_type, self._app_spec.structs)
607
+ except Exception as e:
608
+ raise ValueError(f"Failed to decode value {value}") from e
609
+
610
+ decoded_map[str(decoded_key)] = decoded_value
611
+
612
+ return decoded_map
613
+
614
+ return _AppClientStateMethods(
615
+ get_all=get_all,
616
+ get_value=get_value,
617
+ get_map_value=get_map_value,
618
+ get_map=get_map,
619
+ )
620
+
621
+ def get_local_state(self, address: str) -> dict[str, AppState]:
622
+ return self._algorand.app.get_local_state(self._app_id, address)
623
+
624
+ def get_global_state(self) -> dict[str, AppState]:
625
+ return self._algorand.app.get_global_state(self._app_id)
626
+
627
+
628
+ class _BareParamsBuilder:
629
+ def __init__(self, client: AppClient) -> None:
630
+ self._client = client
631
+ self._algorand = client._algorand
632
+ self._app_id = client._app_id
633
+ self._app_spec = client._app_spec
634
+
635
+ def _get_bare_params(
636
+ self, params: dict[str, Any] | None, on_complete: algosdk.transaction.OnComplete | None = None
637
+ ) -> dict[str, Any]:
638
+ params = params or {}
639
+ sender = self._client._get_sender(params.get("sender"))
640
+ return {
641
+ **params,
642
+ "app_id": self._app_id,
643
+ "sender": sender,
644
+ "signer": self._client._get_signer(params.get("sender"), params.get("signer")),
645
+ "on_complete": on_complete or OnComplete.NoOpOC,
646
+ }
647
+
648
+ def update(
649
+ self,
650
+ params: AppClientBareCallParams | None = None,
651
+ ) -> AppUpdateParams:
652
+ """Create parameters for updating an application.
653
+
654
+ :param params: Optional compilation and send parameters, defaults to None
655
+ :return: Parameters for updating the application
656
+ """
657
+ call_params: AppUpdateParams = AppUpdateParams(
658
+ **self._get_bare_params(params.__dict__ if params else {}, OnComplete.UpdateApplicationOC)
659
+ )
660
+ return call_params
661
+
662
+ def opt_in(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
663
+ """Create parameters for opting into an application.
664
+
665
+ :param params: Optional send parameters, defaults to None
666
+ :return: Parameters for opting into the application
667
+ """
668
+ call_params: AppCallParams = AppCallParams(
669
+ **self._get_bare_params(params.__dict__ if params else {}, OnComplete.OptInOC)
670
+ )
671
+ return call_params
672
+
673
+ def delete(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
674
+ """Create parameters for deleting an application.
675
+
676
+ :param params: Optional send parameters, defaults to None
677
+ :return: Parameters for deleting the application
678
+ """
679
+ call_params: AppCallParams = AppCallParams(
680
+ **self._get_bare_params(params.__dict__ if params else {}, OnComplete.DeleteApplicationOC)
681
+ )
682
+ return call_params
683
+
684
+ def clear_state(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
685
+ """Create parameters for clearing application state.
686
+
687
+ :param params: Optional send parameters, defaults to None
688
+ :return: Parameters for clearing application state
689
+ """
690
+ call_params: AppCallParams = AppCallParams(
691
+ **self._get_bare_params(params.__dict__ if params else {}, OnComplete.ClearStateOC)
692
+ )
693
+ return call_params
694
+
695
+ def close_out(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
696
+ """Create parameters for closing out of an application.
697
+
698
+ :param params: Optional send parameters, defaults to None
699
+ :return: Parameters for closing out of the application
700
+ """
701
+ call_params: AppCallParams = AppCallParams(
702
+ **self._get_bare_params(params.__dict__ if params else {}, OnComplete.CloseOutOC)
703
+ )
704
+ return call_params
705
+
706
+ def call(
707
+ self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC
708
+ ) -> AppCallParams:
709
+ """Create parameters for calling an application.
710
+
711
+ :param params: Optional call parameters with on complete action, defaults to None
712
+ :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC
713
+ :return: Parameters for calling the application
714
+ """
715
+ call_params: AppCallParams = AppCallParams(
716
+ **self._get_bare_params(params.__dict__ if params else {}, on_complete or OnComplete.NoOpOC)
717
+ )
718
+ return call_params
719
+
720
+
721
+ class _MethodParamsBuilder:
722
+ def __init__(self, client: AppClient) -> None:
723
+ self._client = client
724
+ self._algorand = client._algorand
725
+ self._app_id = client._app_id
726
+ self._app_spec = client._app_spec
727
+ self._bare_params_accessor = _BareParamsBuilder(client)
728
+
729
+ @property
730
+ def bare(self) -> _BareParamsBuilder:
731
+ return self._bare_params_accessor
732
+
733
+ def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams:
734
+ """Create parameters for funding an application account.
735
+
736
+ :param params: Parameters for funding the application account
737
+ :return: Parameters for sending a payment transaction to fund the application account
738
+ """
739
+
740
+ def random_note() -> bytes:
741
+ return base64.b64encode(os.urandom(16))
742
+
743
+ return PaymentParams(
744
+ sender=self._client._get_sender(params.sender),
745
+ signer=self._client._get_signer(params.sender, params.signer),
746
+ receiver=self._client.app_address,
747
+ amount=params.amount,
748
+ rekey_to=params.rekey_to,
749
+ note=params.note or random_note(),
750
+ lease=params.lease,
751
+ static_fee=params.static_fee,
752
+ extra_fee=params.extra_fee,
753
+ max_fee=params.max_fee,
754
+ validity_window=params.validity_window,
755
+ first_valid_round=params.first_valid_round,
756
+ last_valid_round=params.last_valid_round,
757
+ close_remainder_to=params.close_remainder_to,
758
+ )
759
+
760
+ def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
761
+ """Create parameters for opting into an application.
762
+
763
+ :param params: Parameters for the opt-in call
764
+ :return: Parameters for opting into the application
765
+ """
766
+ input_params = self._get_abi_params(
767
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.OptInOC
768
+ )
769
+ return AppCallMethodCallParams(**input_params)
770
+
771
+ def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
772
+ """Create parameters for calling an application method.
773
+
774
+ :param params: Parameters for the method call
775
+ :return: Parameters for calling the application method
776
+ """
777
+ input_params = self._get_abi_params(
778
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC
779
+ )
780
+ return AppCallMethodCallParams(**input_params)
781
+
782
+ def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams:
783
+ """Create parameters for deleting an application.
784
+
785
+ :param params: Parameters for the delete call
786
+ :return: Parameters for deleting the application
787
+ """
788
+ input_params = self._get_abi_params(
789
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.DeleteApplicationOC
790
+ )
791
+ return AppDeleteMethodCallParams(**input_params)
792
+
793
+ def update(
794
+ self, params: AppClientMethodCallParams, compilation_params: AppClientCompilationParams | None = None
795
+ ) -> AppUpdateMethodCallParams:
796
+ """Create parameters for updating an application.
797
+
798
+ :param params: Parameters for the update call, optionally including compilation parameters
799
+ :param compilation_params: Parameters for the compilation, defaults to None
800
+ :return: Parameters for updating the application
801
+ """
802
+ compile_params = (
803
+ self._client.compile(
804
+ app_spec=self._client.app_spec,
805
+ app_manager=self._algorand.app,
806
+ compilation_params=compilation_params,
807
+ ).__dict__
808
+ if compilation_params
809
+ else {}
810
+ )
811
+
812
+ input_params = {
813
+ **self._get_abi_params(
814
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.UpdateApplicationOC
815
+ ),
816
+ **compile_params,
817
+ }
818
+ # Filter input_params to include only fields valid for AppUpdateMethodCallParams
819
+ app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCallParams)}
820
+ filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields}
821
+ return AppUpdateMethodCallParams(**filtered_input_params)
822
+
823
+ def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
824
+ """Create parameters for closing out of an application.
825
+
826
+ :param params: Parameters for the close-out call
827
+ :return: Parameters for closing out of the application
828
+ """
829
+ input_params = self._get_abi_params(
830
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.CloseOutOC
831
+ )
832
+ return AppCallMethodCallParams(**input_params)
833
+
834
+ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
835
+ input_params = copy.deepcopy(params)
836
+
837
+ input_params["app_id"] = self._app_id
838
+ input_params["on_complete"] = on_complete
839
+ input_params["sender"] = self._client._get_sender(params["sender"])
840
+ input_params["signer"] = self._client._get_signer(params["sender"], params["signer"])
841
+
842
+ if params.get("method"):
843
+ input_params["method"] = self._app_spec.get_arc56_method(params["method"]).to_abi_method()
844
+ input_params["args"] = self._client._get_abi_args_with_default_values(
845
+ method_name_or_signature=params["method"],
846
+ args=params.get("args"),
847
+ sender=self._client._get_sender(input_params["sender"]),
848
+ )
849
+
850
+ return input_params
851
+
852
+
853
+ class _AppClientBareCallCreateTransactionMethods:
854
+ def __init__(self, client: AppClient) -> None:
855
+ self._client = client
856
+ self._algorand = client._algorand
857
+
858
+ def update(self, params: AppClientBareCallParams | None = None) -> Transaction:
859
+ """Create a transaction to update an application.
860
+
861
+ Creates a transaction that will update an existing application with new approval and clear state programs.
862
+
863
+ :param params: Parameters for the update call including compilation and transaction options, defaults to None
864
+ :return: The constructed application update transaction
865
+ """
866
+ return self._algorand.create_transaction.app_update(
867
+ self._client.params.bare.update(params or AppClientBareCallParams())
868
+ )
869
+
870
+ def opt_in(self, params: AppClientBareCallParams | None = None) -> Transaction:
871
+ """Create a transaction to opt into an application.
872
+
873
+ Creates a transaction that will opt the sender account into using this application.
874
+
875
+ :param params: Parameters for the opt-in call including transaction options, defaults to None
876
+ :return: The constructed opt-in transaction
877
+ """
878
+ return self._algorand.create_transaction.app_call(
879
+ self._client.params.bare.opt_in(params or AppClientBareCallParams())
880
+ )
881
+
882
+ def delete(self, params: AppClientBareCallParams | None = None) -> Transaction:
883
+ """Create a transaction to delete an application.
884
+
885
+ Creates a transaction that will delete this application from the blockchain.
886
+
887
+ :param params: Parameters for the delete call including transaction options, defaults to None
888
+ :return: The constructed delete transaction
889
+ """
890
+ return self._algorand.create_transaction.app_call(
891
+ self._client.params.bare.delete(params or AppClientBareCallParams())
892
+ )
893
+
894
+ def clear_state(self, params: AppClientBareCallParams | None = None) -> Transaction:
895
+ """Create a transaction to clear application state.
896
+
897
+ Creates a transaction that will clear the sender's local state for this application.
898
+
899
+ :param params: Parameters for the clear state call including transaction options, defaults to None
900
+ :return: The constructed clear state transaction
901
+ """
902
+ return self._algorand.create_transaction.app_call(
903
+ self._client.params.bare.clear_state(params or AppClientBareCallParams())
904
+ )
905
+
906
+ def close_out(self, params: AppClientBareCallParams | None = None) -> Transaction:
907
+ """Create a transaction to close out of an application.
908
+
909
+ Creates a transaction that will close out the sender's participation in this application.
910
+
911
+ :param params: Parameters for the close out call including transaction options, defaults to None
912
+ :return: The constructed close out transaction
913
+ """
914
+ return self._algorand.create_transaction.app_call(
915
+ self._client.params.bare.close_out(params or AppClientBareCallParams())
916
+ )
917
+
918
+ def call(
919
+ self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC
920
+ ) -> Transaction:
921
+ """Create a transaction to call an application.
922
+
923
+ Creates a transaction that will call this application with the specified parameters.
924
+
925
+ :param params: Parameters for the application call including on complete action, defaults to None
926
+ :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC
927
+ :return: The constructed application call transaction
928
+ """
929
+ return self._algorand.create_transaction.app_call(
930
+ self._client.params.bare.call(params or AppClientBareCallParams(), on_complete or OnComplete.NoOpOC)
931
+ )
932
+
933
+
934
+ class _TransactionCreator:
935
+ def __init__(self, client: AppClient) -> None:
936
+ self._client = client
937
+ self._algorand = client._algorand
938
+ self._app_id = client._app_id
939
+ self._app_spec = client._app_spec
940
+ self._bare_create_transaction_methods = _AppClientBareCallCreateTransactionMethods(client)
941
+
942
+ @property
943
+ def bare(self) -> _AppClientBareCallCreateTransactionMethods:
944
+ return self._bare_create_transaction_methods
945
+
946
+ def fund_app_account(self, params: FundAppAccountParams) -> Transaction:
947
+ """Create a transaction to fund an application account.
948
+
949
+ Creates a payment transaction to fund the application account with the specified parameters.
950
+
951
+ :param params: Parameters for funding the application account including amount and transaction options
952
+ :return: The constructed payment transaction
953
+ """
954
+ return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params))
955
+
956
+ def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions:
957
+ """Create a transaction to opt into an application.
958
+
959
+ Creates a transaction that will opt the sender into this application with the specified parameters.
960
+
961
+ :param params: Parameters for the opt-in call including method arguments and transaction options
962
+ :return: The constructed opt-in transaction(s)
963
+ """
964
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params))
965
+
966
+ def update(self, params: AppClientMethodCallParams) -> BuiltTransactions:
967
+ """Create a transaction to update an application.
968
+
969
+ Creates a transaction that will update this application with new approval and clear state programs.
970
+
971
+ :param params: Parameters for the update call including method arguments and transaction options
972
+ :return: The constructed update transaction(s)
973
+ """
974
+ return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params))
975
+
976
+ def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions:
977
+ """Create a transaction to delete an application.
978
+
979
+ Creates a transaction that will delete this application.
980
+
981
+ :param params: Parameters for the delete call including method arguments and transaction options
982
+ :return: The constructed delete transaction(s)
983
+ """
984
+ return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params))
985
+
986
+ def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions:
987
+ """Create a transaction to close out of an application.
988
+
989
+ Creates a transaction that will close out the sender's participation in this application.
990
+
991
+ :param params: Parameters for the close out call including method arguments and transaction options
992
+ :return: The constructed close out transaction(s)
993
+ """
994
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params))
995
+
996
+ def call(self, params: AppClientMethodCallParams) -> BuiltTransactions:
997
+ """Create a transaction to call an application.
998
+
999
+ Creates a transaction that will call this application with the specified parameters.
1000
+
1001
+ :param params: Parameters for the application call including method arguments and transaction options
1002
+ :return: The constructed application call transaction(s)
1003
+ """
1004
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params))
1005
+
1006
+
1007
+ class _AppClientBareSendAccessor:
1008
+ def __init__(self, client: AppClient) -> None:
1009
+ self._client = client
1010
+ self._algorand = client._algorand
1011
+ self._app_id = client._app_id
1012
+ self._app_spec = client._app_spec
1013
+
1014
+ def update(
1015
+ self,
1016
+ params: AppClientBareCallParams | None = None,
1017
+ send_params: SendParams | None = None,
1018
+ compilation_params: AppClientCompilationParams | None = None,
1019
+ ) -> SendAppTransactionResult[ABIReturn]:
1020
+ """Send an application update transaction.
1021
+
1022
+ Sends a transaction to update an existing application with new approval and clear state programs.
1023
+
1024
+ :param params: The parameters for the update call, including optional compilation parameters,
1025
+ deploy time parameters, and transaction configuration
1026
+ :param send_params: Send parameters, defaults to None
1027
+ :param compilation_params: Parameters for the compilation, defaults to None
1028
+ :return: The result of sending the transaction, including compilation artifacts and ABI return
1029
+ value if applicable
1030
+ """
1031
+ params = params or AppClientBareCallParams()
1032
+ compilation = compilation_params or AppClientCompilationParams()
1033
+ compiled = self._client.compile_app(
1034
+ {
1035
+ "deploy_time_params": compilation.get("deploy_time_params"),
1036
+ "updatable": compilation.get("updatable"),
1037
+ "deletable": compilation.get("deletable"),
1038
+ }
1039
+ )
1040
+ bare_params = self._client.params.bare.update(params)
1041
+ bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval)
1042
+ bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear)
1043
+ call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params, send_params))
1044
+ return SendAppTransactionResult[ABIReturn](
1045
+ **{**call_result.__dict__, **(compiled.__dict__ if compiled else {})},
1046
+ abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)),
1047
+ )
1048
+
1049
+ def opt_in(
1050
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1051
+ ) -> SendAppTransactionResult[ABIReturn]:
1052
+ """Send an application opt-in transaction.
1053
+
1054
+ Creates and sends a transaction that will opt the sender's account into this application.
1055
+
1056
+ :param params: Parameters for the opt-in call including transaction options, defaults to None
1057
+ :param send_params: Send parameters, defaults to None
1058
+ :return: The result of sending the transaction, including ABI return value if applicable
1059
+ """
1060
+ return self._client._handle_call_errors(
1061
+ lambda: self._algorand.send.app_call(
1062
+ self._client.params.bare.opt_in(params or AppClientBareCallParams()), send_params
1063
+ )
1064
+ )
1065
+
1066
+ def delete(
1067
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1068
+ ) -> SendAppTransactionResult[ABIReturn]:
1069
+ """Send an application delete transaction.
1070
+
1071
+ Creates and sends a transaction that will delete this application.
1072
+
1073
+ :param params: Parameters for the delete call including transaction options, defaults to None
1074
+ :param send_params: Send parameters, defaults to None
1075
+ :return: The result of sending the transaction, including ABI return value if applicable
1076
+ """
1077
+ return self._client._handle_call_errors(
1078
+ lambda: self._algorand.send.app_call(
1079
+ self._client.params.bare.delete(params or AppClientBareCallParams()), send_params
1080
+ )
1081
+ )
1082
+
1083
+ def clear_state(
1084
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1085
+ ) -> SendAppTransactionResult[ABIReturn]:
1086
+ """Send an application clear state transaction.
1087
+
1088
+ Creates and sends a transaction that will clear the sender's local state for this application.
1089
+
1090
+ :param params: Parameters for the clear state call including transaction options, defaults to None
1091
+ :param send_params: Send parameters, defaults to None
1092
+ :return: The result of sending the transaction, including ABI return value if applicable
1093
+ """
1094
+ return self._client._handle_call_errors(
1095
+ lambda: self._algorand.send.app_call(
1096
+ self._client.params.bare.clear_state(params or AppClientBareCallParams()), send_params
1097
+ )
1098
+ )
1099
+
1100
+ def close_out(
1101
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1102
+ ) -> SendAppTransactionResult[ABIReturn]:
1103
+ """Send an application close out transaction.
1104
+
1105
+ Creates and sends a transaction that will close out the sender's participation in this application.
1106
+
1107
+ :param params: Parameters for the close out call including transaction options, defaults to None
1108
+ :param send_params: Send parameters, defaults to None
1109
+ :return: The result of sending the transaction, including ABI return value if applicable
1110
+ """
1111
+ return self._client._handle_call_errors(
1112
+ lambda: self._algorand.send.app_call(
1113
+ self._client.params.bare.close_out(params or AppClientBareCallParams()), send_params
1114
+ )
1115
+ )
1116
+
1117
+ def call(
1118
+ self,
1119
+ params: AppClientBareCallParams | None = None,
1120
+ on_complete: OnComplete | None = None,
1121
+ send_params: SendParams | None = None,
1122
+ ) -> SendAppTransactionResult[ABIReturn]:
1123
+ """Send an application call transaction.
1124
+
1125
+ Creates and sends a transaction that will call this application with the specified parameters.
1126
+
1127
+ :param params: Parameters for the application call including transaction options, defaults to None
1128
+ :param on_complete: The OnComplete action, defaults to None
1129
+ :param send_params: Send parameters, defaults to None
1130
+ :return: The result of sending the transaction, including ABI return value if applicable
1131
+ """
1132
+ return self._client._handle_call_errors(
1133
+ lambda: self._algorand.send.app_call(
1134
+ self._client.params.bare.call(params or AppClientBareCallParams(), on_complete), send_params
1135
+ )
1136
+ )
1137
+
1138
+
1139
+ class _TransactionSender:
1140
+ def __init__(self, client: AppClient) -> None:
1141
+ self._client = client
1142
+ self._algorand = client._algorand
1143
+ self._app_id = client._app_id
1144
+ self._app_spec = client._app_spec
1145
+ self._bare_send_accessor = _AppClientBareSendAccessor(client)
1146
+
1147
+ @property
1148
+ def bare(self) -> _AppClientBareSendAccessor:
1149
+ """Get accessor for bare application calls.
1150
+
1151
+ :return: Accessor for making bare application calls without ABI encoding
1152
+ """
1153
+ return self._bare_send_accessor
1154
+
1155
+ def fund_app_account(
1156
+ self, params: FundAppAccountParams, send_params: SendParams | None = None
1157
+ ) -> SendSingleTransactionResult:
1158
+ """Send funds to the application account.
1159
+
1160
+ Creates and sends a payment transaction to fund the application account.
1161
+
1162
+ :param params: Parameters for funding the app account including amount and transaction options
1163
+ :param send_params: Send parameters, defaults to None
1164
+ :return: The result of sending the payment transaction
1165
+ """
1166
+ return self._client._handle_call_errors( # type: ignore[no-any-return]
1167
+ lambda: self._algorand.send.payment(self._client.params.fund_app_account(params), send_params)
1168
+ )
1169
+
1170
+ def opt_in(
1171
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1172
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1173
+ """Send an application opt-in transaction.
1174
+
1175
+ Creates and sends a transaction that will opt the sender into this application.
1176
+
1177
+ :param params: Parameters for the opt-in call including method and transaction options
1178
+ :param send_params: Send parameters, defaults to None
1179
+ :return: The result of sending the transaction, including ABI return value if applicable
1180
+ """
1181
+ return self._client._handle_call_errors(
1182
+ lambda: self._client._process_method_call_return(
1183
+ lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params), send_params),
1184
+ self._app_spec.get_arc56_method(params.method),
1185
+ )
1186
+ )
1187
+
1188
+ def delete(
1189
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1190
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1191
+ """Send an application delete transaction.
1192
+
1193
+ Creates and sends a transaction that will delete this application.
1194
+
1195
+ :param params: Parameters for the delete call including method and transaction options
1196
+ :param send_params: Send parameters, defaults to None
1197
+ :return: The result of sending the transaction, including ABI return value if applicable
1198
+ """
1199
+ return self._client._handle_call_errors(
1200
+ lambda: self._client._process_method_call_return(
1201
+ lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params), send_params),
1202
+ self._app_spec.get_arc56_method(params.method),
1203
+ )
1204
+ )
1205
+
1206
+ def update(
1207
+ self,
1208
+ params: AppClientMethodCallParams,
1209
+ compilation_params: AppClientCompilationParams | None = None,
1210
+ send_params: SendParams | None = None,
1211
+ ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]:
1212
+ """Send an application update transaction.
1213
+
1214
+ Creates and sends a transaction that will update this application's program.
1215
+
1216
+ :param params: Parameters for the update call including method, compilation and transaction options
1217
+ :param compilation_params: Parameters for the compilation, defaults to None
1218
+ :param send_params: Send parameters, defaults to None
1219
+ :return: The result of sending the transaction, including ABI return value if applicable
1220
+ """
1221
+ result = self._client._handle_call_errors(
1222
+ lambda: self._client._process_method_call_return(
1223
+ lambda: self._algorand.send.app_update_method_call(
1224
+ self._client.params.update(params, compilation_params), send_params
1225
+ ),
1226
+ self._app_spec.get_arc56_method(params.method),
1227
+ )
1228
+ )
1229
+ assert isinstance(result, SendAppUpdateTransactionResult)
1230
+ return result
1231
+
1232
+ def close_out(
1233
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1234
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1235
+ """Send an application close out transaction.
1236
+
1237
+ Creates and sends a transaction that will close out the sender's participation in this application.
1238
+
1239
+ :param params: Parameters for the close out call including method and transaction options
1240
+ :param send_params: Send parameters, defaults to None
1241
+ :return: The result of sending the transaction, including ABI return value if applicable
1242
+ """
1243
+ return self._client._handle_call_errors(
1244
+ lambda: self._client._process_method_call_return(
1245
+ lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params), send_params),
1246
+ self._app_spec.get_arc56_method(params.method),
1247
+ )
1248
+ )
1249
+
1250
+ def call(
1251
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1252
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1253
+ """Send an application call transaction.
1254
+
1255
+ Creates and sends a transaction that will call this application with the specified parameters.
1256
+ For read-only calls, simulates the transaction instead of sending it.
1257
+
1258
+ :param params: Parameters for the application call including method and transaction options
1259
+ :param send_params: Send parameters
1260
+ :return: The result of sending or simulating the transaction, including ABI return value if applicable
1261
+ """
1262
+ is_read_only_call = (
1263
+ params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None
1264
+ ) and self._app_spec.get_arc56_method(params.method).readonly
1265
+
1266
+ if is_read_only_call:
1267
+ method_call_to_simulate = self._algorand.new_group().add_app_call_method_call(
1268
+ self._client.params.call(params)
1269
+ )
1270
+ send_params = send_params or SendParams()
1271
+ simulate_response = self._client._handle_call_errors(
1272
+ lambda: method_call_to_simulate.simulate(
1273
+ allow_unnamed_resources=send_params.get("populate_app_call_resources") or True,
1274
+ skip_signatures=True,
1275
+ allow_more_logs=True,
1276
+ allow_empty_signatures=True,
1277
+ extra_opcode_budget=None,
1278
+ exec_trace_config=None,
1279
+ simulation_round=None,
1280
+ )
1281
+ )
1282
+
1283
+ return SendAppTransactionResult[Arc56ReturnValueType](
1284
+ tx_ids=simulate_response.tx_ids,
1285
+ transactions=simulate_response.transactions,
1286
+ transaction=simulate_response.transactions[-1],
1287
+ confirmation=simulate_response.confirmations[-1] if simulate_response.confirmations else b"",
1288
+ confirmations=simulate_response.confirmations,
1289
+ group_id=simulate_response.group_id or "",
1290
+ returns=simulate_response.returns,
1291
+ abi_return=simulate_response.returns[-1].get_arc56_value(
1292
+ self._app_spec.get_arc56_method(params.method), self._app_spec.structs
1293
+ ),
1294
+ )
1295
+
1296
+ return self._client._handle_call_errors(
1297
+ lambda: self._client._process_method_call_return(
1298
+ lambda: self._algorand.send.app_call_method_call(self._client.params.call(params), send_params),
1299
+ self._app_spec.get_arc56_method(params.method),
1300
+ )
1301
+ )
1302
+
1303
+
1304
+ @dataclass(kw_only=True, frozen=True)
1305
+ class AppClientParams:
1306
+ """Full parameters for creating an app client"""
1307
+
1308
+ app_spec: Arc56Contract | Arc32Contract | str
1309
+ algorand: AlgorandClient
1310
+ app_id: int
1311
+ app_name: str | None = None
1312
+ default_sender: str | None = None
1313
+ default_signer: TransactionSigner | None = None
1314
+ approval_source_map: SourceMap | None = None
1315
+ clear_source_map: SourceMap | None = None
1316
+
1317
+
1318
+ class AppClient:
1319
+ """A client for interacting with an Algorand smart contract application.
1320
+
1321
+ Provides a high-level interface for interacting with Algorand smart contracts, including
1322
+ methods for calling application methods, managing state, and handling transactions.
1323
+
1324
+ :param params: Parameters for creating the app client
1325
+ """
1326
+
1327
+ def __init__(self, params: AppClientParams) -> None:
1328
+ self._app_id = params.app_id
1329
+ self._app_spec = self.normalise_app_spec(params.app_spec)
1330
+ self._algorand = params.algorand
1331
+ self._app_address = algosdk.logic.get_application_address(self._app_id)
1332
+ self._app_name = params.app_name or self._app_spec.name
1333
+ self._default_sender = params.default_sender
1334
+ self._default_signer = params.default_signer
1335
+ self._approval_source_map = params.approval_source_map
1336
+ self._clear_source_map = params.clear_source_map
1337
+ self._state_accessor = _StateAccessor(self)
1338
+ self._params_accessor = _MethodParamsBuilder(self)
1339
+ self._send_accessor = _TransactionSender(self)
1340
+ self._create_transaction_accessor = _TransactionCreator(self)
1341
+
1342
+ @property
1343
+ def algorand(self) -> AlgorandClient:
1344
+ """Get the Algorand client instance.
1345
+
1346
+ :return: The Algorand client used by this app client
1347
+ """
1348
+ return self._algorand
1349
+
1350
+ @property
1351
+ def app_id(self) -> int:
1352
+ """Get the application ID.
1353
+
1354
+ :return: The ID of the Algorand application
1355
+ """
1356
+ return self._app_id
1357
+
1358
+ @property
1359
+ def app_address(self) -> str:
1360
+ """Get the application's Algorand address.
1361
+
1362
+ :return: The Algorand address associated with this application
1363
+ """
1364
+ return self._app_address
1365
+
1366
+ @property
1367
+ def app_name(self) -> str:
1368
+ """Get the application name.
1369
+
1370
+ :return: The name of the application
1371
+ """
1372
+ return self._app_name
1373
+
1374
+ @property
1375
+ def app_spec(self) -> Arc56Contract:
1376
+ """Get the application specification.
1377
+
1378
+ :return: The ARC-56 contract specification for this application
1379
+ """
1380
+ return self._app_spec
1381
+
1382
+ @property
1383
+ def state(self) -> _StateAccessor:
1384
+ """Get the state accessor.
1385
+
1386
+ :return: The state accessor for this application
1387
+ """
1388
+ return self._state_accessor
1389
+
1390
+ @property
1391
+ def params(self) -> _MethodParamsBuilder:
1392
+ """Get the method parameters builder.
1393
+
1394
+ :return: The method parameters builder for this application
1395
+ """
1396
+ return self._params_accessor
1397
+
1398
+ @property
1399
+ def send(self) -> _TransactionSender:
1400
+ """Get the transaction sender.
1401
+
1402
+ :return: The transaction sender for this application
1403
+ """
1404
+ return self._send_accessor
1405
+
1406
+ @property
1407
+ def create_transaction(self) -> _TransactionCreator:
1408
+ """Get the transaction creator.
1409
+
1410
+ :return: The transaction creator for this application
1411
+ """
1412
+ return self._create_transaction_accessor
1413
+
1414
+ @staticmethod
1415
+ def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Contract:
1416
+ """Normalize an application specification to ARC-56 format.
1417
+
1418
+ :param app_spec: The application specification to normalize
1419
+ :return: The normalized ARC-56 contract specification
1420
+ :raises ValueError: If the app spec format is invalid
1421
+ """
1422
+ if isinstance(app_spec, str):
1423
+ spec_dict = json.loads(app_spec)
1424
+ spec = Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict
1425
+ else:
1426
+ spec = app_spec
1427
+
1428
+ match spec:
1429
+ case Arc56Contract():
1430
+ return spec
1431
+ case Arc32Contract():
1432
+ return Arc56Contract.from_arc32(spec.to_json())
1433
+ case dict():
1434
+ return Arc56Contract.from_dict(spec)
1435
+ case _:
1436
+ raise ValueError("Invalid app spec format")
1437
+
1438
+ @staticmethod
1439
+ def from_network(
1440
+ app_spec: Arc56Contract | Arc32Contract | str,
1441
+ algorand: AlgorandClient,
1442
+ app_name: str | None = None,
1443
+ default_sender: str | None = None,
1444
+ default_signer: TransactionSigner | None = None,
1445
+ approval_source_map: SourceMap | None = None,
1446
+ clear_source_map: SourceMap | None = None,
1447
+ ) -> AppClient:
1448
+ """Create an AppClient instance from network information.
1449
+
1450
+ :param app_spec: The application specification
1451
+ :param algorand: The Algorand client instance
1452
+ :param app_name: Optional application name
1453
+ :param default_sender: Optional default sender address
1454
+ :param default_signer: Optional default transaction signer
1455
+ :param approval_source_map: Optional approval program source map
1456
+ :param clear_source_map: Optional clear program source map
1457
+ :return: A new AppClient instance
1458
+ :raises Exception: If no app ID is found for the network
1459
+ """
1460
+ network = algorand.client.network()
1461
+ app_spec = AppClient.normalise_app_spec(app_spec)
1462
+ network_names = [network.genesis_hash]
1463
+
1464
+ if network.is_localnet:
1465
+ network_names.append("localnet")
1466
+ if network.is_mainnet:
1467
+ network_names.append("mainnet")
1468
+ if network.is_testnet:
1469
+ network_names.append("testnet")
1470
+
1471
+ available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else []
1472
+ network_index = next((i for i, n in enumerate(available_app_spec_networks) if n in network_names), None)
1473
+
1474
+ if network_index is None:
1475
+ raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec")
1476
+
1477
+ app_id = app_spec.networks[available_app_spec_networks[network_index]].app_id # type: ignore[index]
1478
+
1479
+ return AppClient(
1480
+ AppClientParams(
1481
+ app_id=app_id,
1482
+ app_spec=app_spec,
1483
+ algorand=algorand,
1484
+ app_name=app_name,
1485
+ default_sender=default_sender,
1486
+ default_signer=default_signer,
1487
+ approval_source_map=approval_source_map,
1488
+ clear_source_map=clear_source_map,
1489
+ )
1490
+ )
1491
+
1492
+ @staticmethod
1493
+ def from_creator_and_name(
1494
+ creator_address: str,
1495
+ app_name: str,
1496
+ app_spec: Arc56Contract | Arc32Contract | str,
1497
+ algorand: AlgorandClient,
1498
+ default_sender: str | None = None,
1499
+ default_signer: TransactionSigner | None = None,
1500
+ approval_source_map: SourceMap | None = None,
1501
+ clear_source_map: SourceMap | None = None,
1502
+ ignore_cache: bool | None = None,
1503
+ app_lookup_cache: ApplicationLookup | None = None,
1504
+ ) -> AppClient:
1505
+ """Create an AppClient instance from creator address and application name.
1506
+
1507
+ :param creator_address: The address of the application creator
1508
+ :param app_name: The name of the application
1509
+ :param app_spec: The application specification
1510
+ :param algorand: The Algorand client instance
1511
+ :param default_sender: Optional default sender address
1512
+ :param default_signer: Optional default transaction signer
1513
+ :param approval_source_map: Optional approval program source map
1514
+ :param clear_source_map: Optional clear program source map
1515
+ :param ignore_cache: Optional flag to ignore cache
1516
+ :param app_lookup_cache: Optional app lookup cache
1517
+ :return: A new AppClient instance
1518
+ :raises ValueError: If the app is not found for the creator and name
1519
+ """
1520
+ app_spec_ = AppClient.normalise_app_spec(app_spec)
1521
+ app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name(
1522
+ creator_address=creator_address, ignore_cache=ignore_cache or False
1523
+ )
1524
+ app_metadata = app_lookup.apps.get(app_name or app_spec_.name)
1525
+ if not app_metadata:
1526
+ raise ValueError(f"App not found for creator {creator_address} and name {app_name or app_spec_.name}")
1527
+
1528
+ return AppClient(
1529
+ AppClientParams(
1530
+ app_id=app_metadata.app_id,
1531
+ app_spec=app_spec_,
1532
+ algorand=algorand,
1533
+ app_name=app_name,
1534
+ default_sender=default_sender,
1535
+ default_signer=default_signer,
1536
+ approval_source_map=approval_source_map,
1537
+ clear_source_map=clear_source_map,
1538
+ )
1539
+ )
1540
+
1541
+ @staticmethod
1542
+ def compile(
1543
+ app_spec: Arc56Contract,
1544
+ app_manager: AppManager,
1545
+ compilation_params: AppClientCompilationParams | None = None,
1546
+ ) -> AppClientCompilationResult:
1547
+ """Compile the application's TEAL code.
1548
+
1549
+ :param app_spec: The application specification
1550
+ :param app_manager: The application manager instance
1551
+ :param compilation_params: Optional compilation parameters
1552
+ :return: The compilation result
1553
+ :raises ValueError: If attempting to compile without source or byte code
1554
+ """
1555
+ compilation_params = compilation_params or AppClientCompilationParams()
1556
+ deploy_time_params = compilation_params.get("deploy_time_params")
1557
+ updatable = compilation_params.get("updatable")
1558
+ deletable = compilation_params.get("deletable")
1559
+
1560
+ def is_base64(s: str) -> bool:
1561
+ try:
1562
+ return base64.b64encode(base64.b64decode(s)).decode() == s
1563
+ except Exception:
1564
+ return False
1565
+
1566
+ if not app_spec.source:
1567
+ if not app_spec.byte_code or not app_spec.byte_code.approval or not app_spec.byte_code.clear:
1568
+ raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code")
1569
+
1570
+ return AppClientCompilationResult(
1571
+ approval_program=base64.b64decode(app_spec.byte_code.approval),
1572
+ clear_state_program=base64.b64decode(app_spec.byte_code.clear),
1573
+ )
1574
+
1575
+ compiled_approval = app_manager.compile_teal_template(
1576
+ app_spec.source.get_decoded_approval(),
1577
+ template_params=deploy_time_params,
1578
+ deployment_metadata=(
1579
+ {"updatable": updatable, "deletable": deletable}
1580
+ if updatable is not None or deletable is not None
1581
+ else None
1582
+ ),
1583
+ )
1584
+
1585
+ compiled_clear = app_manager.compile_teal_template(
1586
+ app_spec.source.get_decoded_clear(),
1587
+ template_params=deploy_time_params,
1588
+ )
1589
+
1590
+ if config.debug and config.project_root:
1591
+ persist_sourcemaps(
1592
+ sources=[
1593
+ PersistSourceMapInput(
1594
+ compiled_teal=compiled_approval, app_name=app_spec.name, file_name="approval.teal"
1595
+ ),
1596
+ PersistSourceMapInput(compiled_teal=compiled_clear, app_name=app_spec.name, file_name="clear.teal"),
1597
+ ],
1598
+ project_root=config.project_root,
1599
+ client=app_manager._algod,
1600
+ with_sources=True,
1601
+ )
1602
+
1603
+ return AppClientCompilationResult(
1604
+ approval_program=compiled_approval.compiled_base64_to_bytes,
1605
+ compiled_approval=compiled_approval,
1606
+ clear_state_program=compiled_clear.compiled_base64_to_bytes,
1607
+ compiled_clear=compiled_clear,
1608
+ )
1609
+
1610
+ @staticmethod
1611
+ def _expose_logic_error_static( # noqa: C901
1612
+ *,
1613
+ e: Exception,
1614
+ app_spec: Arc56Contract,
1615
+ is_clear_state_program: bool = False,
1616
+ approval_source_map: SourceMap | None = None,
1617
+ clear_source_map: SourceMap | None = None,
1618
+ program: bytes | None = None,
1619
+ approval_source_info: ProgramSourceInfo | None = None,
1620
+ clear_source_info: ProgramSourceInfo | None = None,
1621
+ ) -> Exception:
1622
+ source_map = clear_source_map if is_clear_state_program else approval_source_map
1623
+
1624
+ error_details = parse_logic_error(str(e))
1625
+ if not error_details:
1626
+ return e
1627
+
1628
+ # The PC value to find in the ARC56 SourceInfo
1629
+ arc56_pc = error_details["pc"]
1630
+
1631
+ program_source_info = clear_source_info if is_clear_state_program else approval_source_info
1632
+
1633
+ # The offset to apply to the PC if using the cblocks pc offset method
1634
+ cblocks_offset = 0
1635
+
1636
+ # If the program uses cblocks offset, then we need to adjust the PC accordingly
1637
+ if program_source_info and program_source_info.pc_offset_method == PcOffsetMethod.CBLOCKS:
1638
+ if not program:
1639
+ raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset")
1640
+
1641
+ cblocks_offset = get_constant_block_offset(program)
1642
+ arc56_pc = error_details["pc"] - cblocks_offset
1643
+
1644
+ # Find the source info for this PC and get the error message
1645
+ source_info = None
1646
+ if program_source_info and program_source_info.source_info:
1647
+ source_info = next(
1648
+ (s for s in program_source_info.source_info if isinstance(s, SourceInfo) and arc56_pc in s.pc),
1649
+ None,
1650
+ )
1651
+ error_message = source_info.error_message if source_info else None
1652
+
1653
+ # If we have the source we can display the TEAL in the error message
1654
+ if hasattr(app_spec, "source"):
1655
+ program_source = (
1656
+ (
1657
+ app_spec.source.get_decoded_clear()
1658
+ if is_clear_state_program
1659
+ else app_spec.source.get_decoded_approval()
1660
+ )
1661
+ if app_spec.source
1662
+ else None
1663
+ )
1664
+ custom_get_line_for_pc = None
1665
+
1666
+ def get_line_for_pc(input_pc: int) -> int | None:
1667
+ if not program_source_info:
1668
+ return None
1669
+ teal = [line.teal for line in program_source_info.source_info if input_pc - cblocks_offset in line.pc]
1670
+ return teal[0] if teal else None
1671
+
1672
+ if not source_map:
1673
+ custom_get_line_for_pc = get_line_for_pc
1674
+
1675
+ if program_source:
1676
+ e = LogicError(
1677
+ logic_error_str=str(e),
1678
+ program=program_source,
1679
+ source_map=source_map,
1680
+ transaction_id=error_details["transaction_id"],
1681
+ message=error_details["message"],
1682
+ pc=error_details["pc"],
1683
+ logic_error=e,
1684
+ get_line_for_pc=custom_get_line_for_pc,
1685
+ traces=None,
1686
+ )
1687
+
1688
+ if error_message:
1689
+ import re
1690
+
1691
+ message = e.logic_error_str if isinstance(e, LogicError) else str(e)
1692
+ app_id = re.search(r"(?<=app=)\d+", message)
1693
+ tx_id = re.search(r"(?<=transaction )\S+(?=:)", message)
1694
+ error = Exception(
1695
+ f"Runtime error when executing {app_spec.name} "
1696
+ f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction "
1697
+ f"{tx_id.group() if tx_id else 'N/A'}: {error_message}"
1698
+ )
1699
+ error.__cause__ = e
1700
+ return error
1701
+
1702
+ return e
1703
+
1704
+ def compile_app(
1705
+ self,
1706
+ compilation_params: AppClientCompilationParams | None = None,
1707
+ ) -> AppClientCompilationResult:
1708
+ """Compile the application's TEAL code.
1709
+
1710
+ :param compilation_params: Optional compilation parameters
1711
+ :return: The compilation result
1712
+ """
1713
+ result = AppClient.compile(self._app_spec, self._algorand.app, compilation_params)
1714
+
1715
+ if result.compiled_approval:
1716
+ self._approval_source_map = result.compiled_approval.source_map
1717
+ if result.compiled_clear:
1718
+ self._clear_source_map = result.compiled_clear.source_map
1719
+
1720
+ return result
1721
+
1722
+ def clone(
1723
+ self,
1724
+ app_name: str | None = _MISSING, # type: ignore[assignment]
1725
+ default_sender: str | None = _MISSING, # type: ignore[assignment]
1726
+ default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment]
1727
+ approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment]
1728
+ clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment]
1729
+ ) -> AppClient:
1730
+ """Create a cloned AppClient instance with optionally overridden parameters.
1731
+
1732
+ :param app_name: Optional new application name
1733
+ :param default_sender: Optional new default sender
1734
+ :param default_signer: Optional new default signer
1735
+ :param approval_source_map: Optional new approval source map
1736
+ :param clear_source_map: Optional new clear source map
1737
+ :return: A new AppClient instance with the specified parameters
1738
+ """
1739
+ return AppClient(
1740
+ AppClientParams(
1741
+ app_id=self._app_id,
1742
+ algorand=self._algorand,
1743
+ app_spec=self._app_spec,
1744
+ app_name=self._app_name if app_name is _MISSING else app_name,
1745
+ default_sender=self._default_sender if default_sender is _MISSING else default_sender,
1746
+ default_signer=self._default_signer if default_signer is _MISSING else default_signer,
1747
+ approval_source_map=(
1748
+ self._approval_source_map if approval_source_map is _MISSING else approval_source_map
1749
+ ),
1750
+ clear_source_map=(self._clear_source_map if clear_source_map is _MISSING else clear_source_map),
1751
+ )
1752
+ )
1753
+
1754
+ def export_source_maps(self) -> AppSourceMaps:
1755
+ """Export the application's source maps.
1756
+
1757
+ :return: The application's source maps
1758
+ :raises ValueError: If source maps haven't been loaded
1759
+ """
1760
+ if not self._approval_source_map or not self._clear_source_map:
1761
+ raise ValueError(
1762
+ "Unable to export source maps; they haven't been loaded into this client - "
1763
+ "you need to call create, update, or deploy first"
1764
+ )
1765
+
1766
+ return AppSourceMaps(
1767
+ approval_source_map=self._approval_source_map,
1768
+ clear_source_map=self._clear_source_map,
1769
+ )
1770
+
1771
+ def import_source_maps(self, source_maps: AppSourceMaps) -> None:
1772
+ """Import source maps for the application.
1773
+
1774
+ :param source_maps: The source maps to import
1775
+ :raises ValueError: If source maps are invalid or missing
1776
+ """
1777
+ if not source_maps.approval_source_map:
1778
+ raise ValueError("Approval source map is required")
1779
+ if not source_maps.clear_source_map:
1780
+ raise ValueError("Clear source map is required")
1781
+
1782
+ if not isinstance(source_maps.approval_source_map, dict | SourceMap):
1783
+ raise ValueError(
1784
+ "Approval source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`"
1785
+ )
1786
+ if not isinstance(source_maps.clear_source_map, dict | SourceMap):
1787
+ raise ValueError(
1788
+ "Clear source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`"
1789
+ )
1790
+
1791
+ self._approval_source_map = (
1792
+ SourceMap(source_map=source_maps.approval_source_map)
1793
+ if isinstance(source_maps.approval_source_map, dict)
1794
+ else source_maps.approval_source_map
1795
+ )
1796
+ self._clear_source_map = (
1797
+ SourceMap(source_map=source_maps.clear_source_map)
1798
+ if isinstance(source_maps.clear_source_map, dict)
1799
+ else source_maps.clear_source_map
1800
+ )
1801
+
1802
+ def get_local_state(self, address: str) -> dict[str, AppState]:
1803
+ """Get local state for an account.
1804
+
1805
+ :param address: The account address
1806
+ :return: The account's local state for this application
1807
+ """
1808
+ return self._state_accessor.get_local_state(address)
1809
+
1810
+ def get_global_state(self) -> dict[str, AppState]:
1811
+ """Get the application's global state.
1812
+
1813
+ :return: The application's global state
1814
+ """
1815
+ return self._state_accessor.get_global_state()
1816
+
1817
+ def get_box_names(self) -> list[BoxName]:
1818
+ """Get all box names for the application.
1819
+
1820
+ :return: List of box names
1821
+ """
1822
+ return self._algorand.app.get_box_names(self._app_id)
1823
+
1824
+ def get_box_value(self, name: BoxIdentifier) -> bytes:
1825
+ """Get the value of a box.
1826
+
1827
+ :param name: The box identifier
1828
+ :return: The box value as bytes
1829
+ """
1830
+ return self._algorand.app.get_box_value(self._app_id, name)
1831
+
1832
+ def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue:
1833
+ """Get a box value decoded according to an ABI type.
1834
+
1835
+ :param name: The box identifier
1836
+ :param abi_type: The ABI type to decode as
1837
+ :return: The decoded box value
1838
+ """
1839
+ return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type)
1840
+
1841
+ def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]:
1842
+ """Get values for multiple boxes.
1843
+
1844
+ :param filter_func: Optional function to filter box names
1845
+ :return: List of box values
1846
+ """
1847
+ names = [n for n in self.get_box_names() if not filter_func or filter_func(n)]
1848
+ values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names])
1849
+ return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)]
1850
+
1851
+ def get_box_values_from_abi_type(
1852
+ self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None
1853
+ ) -> list[BoxABIValue]:
1854
+ """Get multiple box values decoded according to an ABI type.
1855
+
1856
+ :param abi_type: The ABI type to decode as
1857
+ :param filter_func: Optional function to filter box names
1858
+ :return: List of decoded box values
1859
+ """
1860
+ names = self.get_box_names()
1861
+ if filter_func:
1862
+ names = [name for name in names if filter_func(name)]
1863
+
1864
+ values = self._algorand.app.get_box_values_from_abi_type(
1865
+ self.app_id, [name.name_raw for name in names], abi_type
1866
+ )
1867
+
1868
+ return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)]
1869
+
1870
+ def fund_app_account(
1871
+ self, params: FundAppAccountParams, send_params: SendParams | None = None
1872
+ ) -> SendSingleTransactionResult:
1873
+ """Fund the application's account.
1874
+
1875
+ :param params: The funding parameters
1876
+ :param send_params: Send parameters, defaults to None
1877
+ :return: The transaction result
1878
+ """
1879
+ return self.send.fund_app_account(params, send_params)
1880
+
1881
+ def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception:
1882
+ source_info = None
1883
+ if hasattr(self._app_spec, "source_info") and self._app_spec.source_info:
1884
+ source_info = (
1885
+ self._app_spec.source_info.clear if is_clear_state_program else self._app_spec.source_info.approval
1886
+ )
1887
+
1888
+ pc_offset_method = source_info.pc_offset_method if source_info else None
1889
+
1890
+ program: bytes | None = None
1891
+ if pc_offset_method == "cblocks":
1892
+ app_info = self._algorand.app.get_by_id(self.app_id)
1893
+ program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program
1894
+
1895
+ return AppClient._expose_logic_error_static(
1896
+ e=e,
1897
+ app_spec=self._app_spec,
1898
+ is_clear_state_program=is_clear_state_program,
1899
+ approval_source_map=self._approval_source_map,
1900
+ clear_source_map=self._clear_source_map,
1901
+ program=program,
1902
+ approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None),
1903
+ clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None),
1904
+ )
1905
+
1906
+ def _handle_call_errors(self, call: Callable[[], T]) -> T:
1907
+ try:
1908
+ return call()
1909
+ except Exception as e:
1910
+ raise self._expose_logic_error(e=e) from None
1911
+
1912
+ def _get_sender(self, sender: str | None) -> str:
1913
+ if not sender and not self._default_sender:
1914
+ raise Exception(
1915
+ f"No sender provided and no default sender present in app client for call to app {self.app_name}"
1916
+ )
1917
+ return sender or self._default_sender # type: ignore[return-value]
1918
+
1919
+ def _get_signer(
1920
+ self, sender: str | None, signer: TransactionSigner | TransactionSignerAccountProtocol | None
1921
+ ) -> TransactionSigner | TransactionSignerAccountProtocol | None:
1922
+ return signer or (self._default_signer if not sender or sender == self._default_sender else None)
1923
+
1924
+ def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
1925
+ sender = self._get_sender(params.get("sender"))
1926
+ return {
1927
+ **params,
1928
+ "app_id": self._app_id,
1929
+ "sender": sender,
1930
+ "signer": self._get_signer(params.get("sender"), params.get("signer")),
1931
+ "on_complete": on_complete,
1932
+ }
1933
+
1934
+ def _get_abi_args_with_default_values( # noqa: C901, PLR0912
1935
+ self,
1936
+ *,
1937
+ method_name_or_signature: str,
1938
+ args: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None,
1939
+ sender: str,
1940
+ ) -> list[Any]:
1941
+ method = self._app_spec.get_arc56_method(method_name_or_signature)
1942
+ result: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] = []
1943
+
1944
+ for i, method_arg in enumerate(method.args):
1945
+ arg_value = args[i] if args and i < len(args) else None
1946
+
1947
+ if arg_value is not None:
1948
+ if method_arg.struct and isinstance(arg_value, dict):
1949
+ arg_value = get_abi_tuple_from_abi_struct(
1950
+ arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs
1951
+ )
1952
+ result.append(arg_value)
1953
+ continue
1954
+
1955
+ default_value = method_arg.default_value
1956
+ if default_value:
1957
+ match default_value.source:
1958
+ case "literal":
1959
+ value_raw = base64.b64decode(default_value.data)
1960
+ value_type = default_value.type or method_arg.type
1961
+ result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs))
1962
+
1963
+ case "method":
1964
+ default_method = self._app_spec.get_arc56_method(default_value.data)
1965
+ empty_args = [None] * len(default_method.args)
1966
+ call_result = self.send.call(
1967
+ AppClientMethodCallParams(
1968
+ method=default_value.data,
1969
+ args=empty_args,
1970
+ sender=sender,
1971
+ )
1972
+ )
1973
+
1974
+ if not call_result.abi_return:
1975
+ raise ValueError("Default value method call did not return a value")
1976
+
1977
+ if isinstance(call_result.abi_return, dict):
1978
+ result.append(
1979
+ get_abi_tuple_from_abi_struct(
1980
+ call_result.abi_return,
1981
+ self._app_spec.structs[str(default_method.returns.struct)],
1982
+ self._app_spec.structs,
1983
+ )
1984
+ )
1985
+ elif call_result.abi_return:
1986
+ result.append(call_result.abi_return)
1987
+
1988
+ case "local" | "global":
1989
+ state = (
1990
+ self.get_global_state()
1991
+ if default_value.source == "global"
1992
+ else self.get_local_state(sender)
1993
+ )
1994
+ value = next((s for s in state.values() if s.key_base64 == default_value.data), None)
1995
+ if not value:
1996
+ raise ValueError(
1997
+ f"Key '{default_value.data}' not found in {default_value.source} "
1998
+ f"storage for argument {method_arg.name or f'arg{i+1}'}"
1999
+ )
2000
+
2001
+ if value.value_raw:
2002
+ value_type = default_value.type or method_arg.type
2003
+ result.append(get_abi_decoded_value(value.value_raw, value_type, self._app_spec.structs))
2004
+ else:
2005
+ result.append(value.value)
2006
+
2007
+ case "box":
2008
+ box_name = base64.b64decode(default_value.data)
2009
+ box_value = self._algorand.app.get_box_value(self._app_id, box_name)
2010
+ value_type = default_value.type or method_arg.type
2011
+ result.append(get_abi_decoded_value(box_value, value_type, self._app_spec.structs))
2012
+
2013
+ elif not algosdk.abi.is_abi_transaction_type(method_arg.type):
2014
+ raise ValueError(
2015
+ f"No value provided for required argument "
2016
+ f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}"
2017
+ )
2018
+ elif arg_value is None and default_value is None:
2019
+ # At this point only allow explicit None values if no default value was identified
2020
+ result.append(None)
2021
+
2022
+ return result
2023
+
2024
+ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
2025
+ sender = self._get_sender(params.get("sender"))
2026
+ method = self._app_spec.get_arc56_method(params["method"])
2027
+ args = self._get_abi_args_with_default_values(
2028
+ method_name_or_signature=params["method"], args=params.get("args"), sender=sender
2029
+ )
2030
+ return {
2031
+ **params,
2032
+ "appId": self._app_id,
2033
+ "sender": sender,
2034
+ "signer": self._get_signer(params.get("sender"), params.get("signer")),
2035
+ "method": method,
2036
+ "onComplete": on_complete,
2037
+ "args": args,
2038
+ }
2039
+
2040
+ def _process_method_call_return(
2041
+ self,
2042
+ result: Callable[[], SendAppUpdateTransactionResult[ABIReturn] | SendAppTransactionResult[ABIReturn]],
2043
+ method: Method,
2044
+ ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType] | SendAppTransactionResult[Arc56ReturnValueType]:
2045
+ result_value = result()
2046
+ abi_return = (
2047
+ result_value.abi_return.get_arc56_value(method, self._app_spec.structs)
2048
+ if isinstance(result_value.abi_return, ABIReturn)
2049
+ else None
2050
+ )
2051
+
2052
+ if isinstance(result_value, SendAppUpdateTransactionResult):
2053
+ return SendAppUpdateTransactionResult[Arc56ReturnValueType](
2054
+ **{**result_value.__dict__, "abi_return": abi_return}
2055
+ )
2056
+ return SendAppTransactionResult[Arc56ReturnValueType](**{**result_value.__dict__, "abi_return": abi_return})