algokit-utils 3.0.0b1__py3-none-any.whl → 3.0.0b3__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 -183
  2. algokit_utils/_debugging.py +123 -97
  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} +19 -142
  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 -1453
  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 +2054 -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 -892
  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 -80
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +197 -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 -152
  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 +2287 -0
  64. algokit_utils/transactions/transaction_creator.py +156 -0
  65. algokit_utils/transactions/transaction_sender.py +574 -0
  66. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/METADATA +13 -8
  67. algokit_utils-3.0.0b3.dist-info/RECORD +70 -0
  68. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/WHEEL +1 -1
  69. algokit_utils-3.0.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b3.dist-info}/LICENSE +0 -0
@@ -0,0 +1,2054 @@
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 asdict, 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 = asdict(
803
+ self._client.compile(
804
+ app_spec=self._client.app_spec,
805
+ app_manager=self._algorand.app,
806
+ compilation_params=compilation_params,
807
+ )
808
+ )
809
+
810
+ input_params = {
811
+ **self._get_abi_params(
812
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.UpdateApplicationOC
813
+ ),
814
+ **compile_params,
815
+ }
816
+ # Filter input_params to include only fields valid for AppUpdateMethodCallParams
817
+ app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCallParams)}
818
+ filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields}
819
+ return AppUpdateMethodCallParams(**filtered_input_params)
820
+
821
+ def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
822
+ """Create parameters for closing out of an application.
823
+
824
+ :param params: Parameters for the close-out call
825
+ :return: Parameters for closing out of the application
826
+ """
827
+ input_params = self._get_abi_params(
828
+ params.__dict__, on_complete=params.on_complete or algosdk.transaction.OnComplete.CloseOutOC
829
+ )
830
+ return AppCallMethodCallParams(**input_params)
831
+
832
+ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
833
+ input_params = copy.deepcopy(params)
834
+
835
+ input_params["app_id"] = self._app_id
836
+ input_params["on_complete"] = on_complete
837
+ input_params["sender"] = self._client._get_sender(params["sender"])
838
+ input_params["signer"] = self._client._get_signer(params["sender"], params["signer"])
839
+
840
+ if params.get("method"):
841
+ input_params["method"] = self._app_spec.get_arc56_method(params["method"]).to_abi_method()
842
+ input_params["args"] = self._client._get_abi_args_with_default_values(
843
+ method_name_or_signature=params["method"],
844
+ args=params.get("args"),
845
+ sender=self._client._get_sender(input_params["sender"]),
846
+ )
847
+
848
+ return input_params
849
+
850
+
851
+ class _AppClientBareCallCreateTransactionMethods:
852
+ def __init__(self, client: AppClient) -> None:
853
+ self._client = client
854
+ self._algorand = client._algorand
855
+
856
+ def update(self, params: AppClientBareCallParams | None = None) -> Transaction:
857
+ """Create a transaction to update an application.
858
+
859
+ Creates a transaction that will update an existing application with new approval and clear state programs.
860
+
861
+ :param params: Parameters for the update call including compilation and transaction options, defaults to None
862
+ :return: The constructed application update transaction
863
+ """
864
+ return self._algorand.create_transaction.app_update(
865
+ self._client.params.bare.update(params or AppClientBareCallParams())
866
+ )
867
+
868
+ def opt_in(self, params: AppClientBareCallParams | None = None) -> Transaction:
869
+ """Create a transaction to opt into an application.
870
+
871
+ Creates a transaction that will opt the sender account into using this application.
872
+
873
+ :param params: Parameters for the opt-in call including transaction options, defaults to None
874
+ :return: The constructed opt-in transaction
875
+ """
876
+ return self._algorand.create_transaction.app_call(
877
+ self._client.params.bare.opt_in(params or AppClientBareCallParams())
878
+ )
879
+
880
+ def delete(self, params: AppClientBareCallParams | None = None) -> Transaction:
881
+ """Create a transaction to delete an application.
882
+
883
+ Creates a transaction that will delete this application from the blockchain.
884
+
885
+ :param params: Parameters for the delete call including transaction options, defaults to None
886
+ :return: The constructed delete transaction
887
+ """
888
+ return self._algorand.create_transaction.app_call(
889
+ self._client.params.bare.delete(params or AppClientBareCallParams())
890
+ )
891
+
892
+ def clear_state(self, params: AppClientBareCallParams | None = None) -> Transaction:
893
+ """Create a transaction to clear application state.
894
+
895
+ Creates a transaction that will clear the sender's local state for this application.
896
+
897
+ :param params: Parameters for the clear state call including transaction options, defaults to None
898
+ :return: The constructed clear state transaction
899
+ """
900
+ return self._algorand.create_transaction.app_call(
901
+ self._client.params.bare.clear_state(params or AppClientBareCallParams())
902
+ )
903
+
904
+ def close_out(self, params: AppClientBareCallParams | None = None) -> Transaction:
905
+ """Create a transaction to close out of an application.
906
+
907
+ Creates a transaction that will close out the sender's participation in this application.
908
+
909
+ :param params: Parameters for the close out call including transaction options, defaults to None
910
+ :return: The constructed close out transaction
911
+ """
912
+ return self._algorand.create_transaction.app_call(
913
+ self._client.params.bare.close_out(params or AppClientBareCallParams())
914
+ )
915
+
916
+ def call(
917
+ self, params: AppClientBareCallParams | None = None, on_complete: OnComplete | None = OnComplete.NoOpOC
918
+ ) -> Transaction:
919
+ """Create a transaction to call an application.
920
+
921
+ Creates a transaction that will call this application with the specified parameters.
922
+
923
+ :param params: Parameters for the application call including on complete action, defaults to None
924
+ :param on_complete: The OnComplete action, defaults to OnComplete.NoOpOC
925
+ :return: The constructed application call transaction
926
+ """
927
+ return self._algorand.create_transaction.app_call(
928
+ self._client.params.bare.call(params or AppClientBareCallParams(), on_complete or OnComplete.NoOpOC)
929
+ )
930
+
931
+
932
+ class _TransactionCreator:
933
+ def __init__(self, client: AppClient) -> None:
934
+ self._client = client
935
+ self._algorand = client._algorand
936
+ self._app_id = client._app_id
937
+ self._app_spec = client._app_spec
938
+ self._bare_create_transaction_methods = _AppClientBareCallCreateTransactionMethods(client)
939
+
940
+ @property
941
+ def bare(self) -> _AppClientBareCallCreateTransactionMethods:
942
+ return self._bare_create_transaction_methods
943
+
944
+ def fund_app_account(self, params: FundAppAccountParams) -> Transaction:
945
+ """Create a transaction to fund an application account.
946
+
947
+ Creates a payment transaction to fund the application account with the specified parameters.
948
+
949
+ :param params: Parameters for funding the application account including amount and transaction options
950
+ :return: The constructed payment transaction
951
+ """
952
+ return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params))
953
+
954
+ def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions:
955
+ """Create a transaction to opt into an application.
956
+
957
+ Creates a transaction that will opt the sender into this application with the specified parameters.
958
+
959
+ :param params: Parameters for the opt-in call including method arguments and transaction options
960
+ :return: The constructed opt-in transaction(s)
961
+ """
962
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params))
963
+
964
+ def update(self, params: AppClientMethodCallParams) -> BuiltTransactions:
965
+ """Create a transaction to update an application.
966
+
967
+ Creates a transaction that will update this application with new approval and clear state programs.
968
+
969
+ :param params: Parameters for the update call including method arguments and transaction options
970
+ :return: The constructed update transaction(s)
971
+ """
972
+ return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params))
973
+
974
+ def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions:
975
+ """Create a transaction to delete an application.
976
+
977
+ Creates a transaction that will delete this application.
978
+
979
+ :param params: Parameters for the delete call including method arguments and transaction options
980
+ :return: The constructed delete transaction(s)
981
+ """
982
+ return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params))
983
+
984
+ def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions:
985
+ """Create a transaction to close out of an application.
986
+
987
+ Creates a transaction that will close out the sender's participation in this application.
988
+
989
+ :param params: Parameters for the close out call including method arguments and transaction options
990
+ :return: The constructed close out transaction(s)
991
+ """
992
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params))
993
+
994
+ def call(self, params: AppClientMethodCallParams) -> BuiltTransactions:
995
+ """Create a transaction to call an application.
996
+
997
+ Creates a transaction that will call this application with the specified parameters.
998
+
999
+ :param params: Parameters for the application call including method arguments and transaction options
1000
+ :return: The constructed application call transaction(s)
1001
+ """
1002
+ return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params))
1003
+
1004
+
1005
+ class _AppClientBareSendAccessor:
1006
+ def __init__(self, client: AppClient) -> None:
1007
+ self._client = client
1008
+ self._algorand = client._algorand
1009
+ self._app_id = client._app_id
1010
+ self._app_spec = client._app_spec
1011
+
1012
+ def update(
1013
+ self,
1014
+ params: AppClientBareCallParams | None = None,
1015
+ send_params: SendParams | None = None,
1016
+ compilation_params: AppClientCompilationParams | None = None,
1017
+ ) -> SendAppTransactionResult[ABIReturn]:
1018
+ """Send an application update transaction.
1019
+
1020
+ Sends a transaction to update an existing application with new approval and clear state programs.
1021
+
1022
+ :param params: The parameters for the update call, including optional compilation parameters,
1023
+ deploy time parameters, and transaction configuration
1024
+ :param send_params: Send parameters, defaults to None
1025
+ :param compilation_params: Parameters for the compilation, defaults to None
1026
+ :return: The result of sending the transaction, including compilation artifacts and ABI return
1027
+ value if applicable
1028
+ """
1029
+ params = params or AppClientBareCallParams()
1030
+ compilation = compilation_params or AppClientCompilationParams()
1031
+ compiled = self._client.compile_app(
1032
+ {
1033
+ "deploy_time_params": compilation.get("deploy_time_params"),
1034
+ "updatable": compilation.get("updatable"),
1035
+ "deletable": compilation.get("deletable"),
1036
+ }
1037
+ )
1038
+ bare_params = self._client.params.bare.update(params)
1039
+ bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval)
1040
+ bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear)
1041
+ call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params, send_params))
1042
+ return SendAppTransactionResult[ABIReturn](
1043
+ **{**call_result.__dict__, **(compiled.__dict__ if compiled else {})},
1044
+ abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)),
1045
+ )
1046
+
1047
+ def opt_in(
1048
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1049
+ ) -> SendAppTransactionResult[ABIReturn]:
1050
+ """Send an application opt-in transaction.
1051
+
1052
+ Creates and sends a transaction that will opt the sender's account into this application.
1053
+
1054
+ :param params: Parameters for the opt-in call including transaction options, defaults to None
1055
+ :param send_params: Send parameters, defaults to None
1056
+ :return: The result of sending the transaction, including ABI return value if applicable
1057
+ """
1058
+ return self._client._handle_call_errors(
1059
+ lambda: self._algorand.send.app_call(
1060
+ self._client.params.bare.opt_in(params or AppClientBareCallParams()), send_params
1061
+ )
1062
+ )
1063
+
1064
+ def delete(
1065
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1066
+ ) -> SendAppTransactionResult[ABIReturn]:
1067
+ """Send an application delete transaction.
1068
+
1069
+ Creates and sends a transaction that will delete this application.
1070
+
1071
+ :param params: Parameters for the delete call including transaction options, defaults to None
1072
+ :param send_params: Send parameters, defaults to None
1073
+ :return: The result of sending the transaction, including ABI return value if applicable
1074
+ """
1075
+ return self._client._handle_call_errors(
1076
+ lambda: self._algorand.send.app_call(
1077
+ self._client.params.bare.delete(params or AppClientBareCallParams()), send_params
1078
+ )
1079
+ )
1080
+
1081
+ def clear_state(
1082
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1083
+ ) -> SendAppTransactionResult[ABIReturn]:
1084
+ """Send an application clear state transaction.
1085
+
1086
+ Creates and sends a transaction that will clear the sender's local state for this application.
1087
+
1088
+ :param params: Parameters for the clear state call including transaction options, defaults to None
1089
+ :param send_params: Send parameters, defaults to None
1090
+ :return: The result of sending the transaction, including ABI return value if applicable
1091
+ """
1092
+ return self._client._handle_call_errors(
1093
+ lambda: self._algorand.send.app_call(
1094
+ self._client.params.bare.clear_state(params or AppClientBareCallParams()), send_params
1095
+ )
1096
+ )
1097
+
1098
+ def close_out(
1099
+ self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
1100
+ ) -> SendAppTransactionResult[ABIReturn]:
1101
+ """Send an application close out transaction.
1102
+
1103
+ Creates and sends a transaction that will close out the sender's participation in this application.
1104
+
1105
+ :param params: Parameters for the close out call including transaction options, defaults to None
1106
+ :param send_params: Send parameters, defaults to None
1107
+ :return: The result of sending the transaction, including ABI return value if applicable
1108
+ """
1109
+ return self._client._handle_call_errors(
1110
+ lambda: self._algorand.send.app_call(
1111
+ self._client.params.bare.close_out(params or AppClientBareCallParams()), send_params
1112
+ )
1113
+ )
1114
+
1115
+ def call(
1116
+ self,
1117
+ params: AppClientBareCallParams | None = None,
1118
+ on_complete: OnComplete | None = None,
1119
+ send_params: SendParams | None = None,
1120
+ ) -> SendAppTransactionResult[ABIReturn]:
1121
+ """Send an application call transaction.
1122
+
1123
+ Creates and sends a transaction that will call this application with the specified parameters.
1124
+
1125
+ :param params: Parameters for the application call including transaction options, defaults to None
1126
+ :param on_complete: The OnComplete action, defaults to None
1127
+ :param send_params: Send parameters, defaults to None
1128
+ :return: The result of sending the transaction, including ABI return value if applicable
1129
+ """
1130
+ return self._client._handle_call_errors(
1131
+ lambda: self._algorand.send.app_call(
1132
+ self._client.params.bare.call(params or AppClientBareCallParams(), on_complete), send_params
1133
+ )
1134
+ )
1135
+
1136
+
1137
+ class _TransactionSender:
1138
+ def __init__(self, client: AppClient) -> None:
1139
+ self._client = client
1140
+ self._algorand = client._algorand
1141
+ self._app_id = client._app_id
1142
+ self._app_spec = client._app_spec
1143
+ self._bare_send_accessor = _AppClientBareSendAccessor(client)
1144
+
1145
+ @property
1146
+ def bare(self) -> _AppClientBareSendAccessor:
1147
+ """Get accessor for bare application calls.
1148
+
1149
+ :return: Accessor for making bare application calls without ABI encoding
1150
+ """
1151
+ return self._bare_send_accessor
1152
+
1153
+ def fund_app_account(
1154
+ self, params: FundAppAccountParams, send_params: SendParams | None = None
1155
+ ) -> SendSingleTransactionResult:
1156
+ """Send funds to the application account.
1157
+
1158
+ Creates and sends a payment transaction to fund the application account.
1159
+
1160
+ :param params: Parameters for funding the app account including amount and transaction options
1161
+ :param send_params: Send parameters, defaults to None
1162
+ :return: The result of sending the payment transaction
1163
+ """
1164
+ return self._client._handle_call_errors( # type: ignore[no-any-return]
1165
+ lambda: self._algorand.send.payment(self._client.params.fund_app_account(params), send_params)
1166
+ )
1167
+
1168
+ def opt_in(
1169
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1170
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1171
+ """Send an application opt-in transaction.
1172
+
1173
+ Creates and sends a transaction that will opt the sender into this application.
1174
+
1175
+ :param params: Parameters for the opt-in call including method and transaction options
1176
+ :param send_params: Send parameters, defaults to None
1177
+ :return: The result of sending the transaction, including ABI return value if applicable
1178
+ """
1179
+ return self._client._handle_call_errors(
1180
+ lambda: self._client._process_method_call_return(
1181
+ lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params), send_params),
1182
+ self._app_spec.get_arc56_method(params.method),
1183
+ )
1184
+ )
1185
+
1186
+ def delete(
1187
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1188
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1189
+ """Send an application delete transaction.
1190
+
1191
+ Creates and sends a transaction that will delete this application.
1192
+
1193
+ :param params: Parameters for the delete call including method and transaction options
1194
+ :param send_params: Send parameters, defaults to None
1195
+ :return: The result of sending the transaction, including ABI return value if applicable
1196
+ """
1197
+ return self._client._handle_call_errors(
1198
+ lambda: self._client._process_method_call_return(
1199
+ lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params), send_params),
1200
+ self._app_spec.get_arc56_method(params.method),
1201
+ )
1202
+ )
1203
+
1204
+ def update(
1205
+ self,
1206
+ params: AppClientMethodCallParams,
1207
+ compilation_params: AppClientCompilationParams | None = None,
1208
+ send_params: SendParams | None = None,
1209
+ ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]:
1210
+ """Send an application update transaction.
1211
+
1212
+ Creates and sends a transaction that will update this application's program.
1213
+
1214
+ :param params: Parameters for the update call including method, compilation and transaction options
1215
+ :param compilation_params: Parameters for the compilation, defaults to None
1216
+ :param send_params: Send parameters, defaults to None
1217
+ :return: The result of sending the transaction, including ABI return value if applicable
1218
+ """
1219
+ result = self._client._handle_call_errors(
1220
+ lambda: self._client._process_method_call_return(
1221
+ lambda: self._algorand.send.app_update_method_call(
1222
+ self._client.params.update(params, compilation_params), send_params
1223
+ ),
1224
+ self._app_spec.get_arc56_method(params.method),
1225
+ )
1226
+ )
1227
+ assert isinstance(result, SendAppUpdateTransactionResult)
1228
+ return result
1229
+
1230
+ def close_out(
1231
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1232
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1233
+ """Send an application close out transaction.
1234
+
1235
+ Creates and sends a transaction that will close out the sender's participation in this application.
1236
+
1237
+ :param params: Parameters for the close out call including method and transaction options
1238
+ :param send_params: Send parameters, defaults to None
1239
+ :return: The result of sending the transaction, including ABI return value if applicable
1240
+ """
1241
+ return self._client._handle_call_errors(
1242
+ lambda: self._client._process_method_call_return(
1243
+ lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params), send_params),
1244
+ self._app_spec.get_arc56_method(params.method),
1245
+ )
1246
+ )
1247
+
1248
+ def call(
1249
+ self, params: AppClientMethodCallParams, send_params: SendParams | None = None
1250
+ ) -> SendAppTransactionResult[Arc56ReturnValueType]:
1251
+ """Send an application call transaction.
1252
+
1253
+ Creates and sends a transaction that will call this application with the specified parameters.
1254
+ For read-only calls, simulates the transaction instead of sending it.
1255
+
1256
+ :param params: Parameters for the application call including method and transaction options
1257
+ :param send_params: Send parameters
1258
+ :return: The result of sending or simulating the transaction, including ABI return value if applicable
1259
+ """
1260
+ is_read_only_call = (
1261
+ params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None
1262
+ ) and self._app_spec.get_arc56_method(params.method).readonly
1263
+
1264
+ if is_read_only_call:
1265
+ method_call_to_simulate = self._algorand.new_group().add_app_call_method_call(
1266
+ self._client.params.call(params)
1267
+ )
1268
+ send_params = send_params or SendParams()
1269
+ simulate_response = self._client._handle_call_errors(
1270
+ lambda: method_call_to_simulate.simulate(
1271
+ allow_unnamed_resources=send_params.get("populate_app_call_resources") or True,
1272
+ skip_signatures=True,
1273
+ allow_more_logs=True,
1274
+ allow_empty_signatures=True,
1275
+ extra_opcode_budget=None,
1276
+ exec_trace_config=None,
1277
+ simulation_round=None,
1278
+ )
1279
+ )
1280
+
1281
+ return SendAppTransactionResult[Arc56ReturnValueType](
1282
+ tx_ids=simulate_response.tx_ids,
1283
+ transactions=simulate_response.transactions,
1284
+ transaction=simulate_response.transactions[-1],
1285
+ confirmation=simulate_response.confirmations[-1] if simulate_response.confirmations else b"",
1286
+ confirmations=simulate_response.confirmations,
1287
+ group_id=simulate_response.group_id or "",
1288
+ returns=simulate_response.returns,
1289
+ abi_return=simulate_response.returns[-1].get_arc56_value(
1290
+ self._app_spec.get_arc56_method(params.method), self._app_spec.structs
1291
+ ),
1292
+ )
1293
+
1294
+ return self._client._handle_call_errors(
1295
+ lambda: self._client._process_method_call_return(
1296
+ lambda: self._algorand.send.app_call_method_call(self._client.params.call(params), send_params),
1297
+ self._app_spec.get_arc56_method(params.method),
1298
+ )
1299
+ )
1300
+
1301
+
1302
+ @dataclass(kw_only=True, frozen=True)
1303
+ class AppClientParams:
1304
+ """Full parameters for creating an app client"""
1305
+
1306
+ app_spec: Arc56Contract | Arc32Contract | str
1307
+ algorand: AlgorandClient
1308
+ app_id: int
1309
+ app_name: str | None = None
1310
+ default_sender: str | None = None
1311
+ default_signer: TransactionSigner | None = None
1312
+ approval_source_map: SourceMap | None = None
1313
+ clear_source_map: SourceMap | None = None
1314
+
1315
+
1316
+ class AppClient:
1317
+ """A client for interacting with an Algorand smart contract application.
1318
+
1319
+ Provides a high-level interface for interacting with Algorand smart contracts, including
1320
+ methods for calling application methods, managing state, and handling transactions.
1321
+
1322
+ :param params: Parameters for creating the app client
1323
+ """
1324
+
1325
+ def __init__(self, params: AppClientParams) -> None:
1326
+ self._app_id = params.app_id
1327
+ self._app_spec = self.normalise_app_spec(params.app_spec)
1328
+ self._algorand = params.algorand
1329
+ self._app_address = algosdk.logic.get_application_address(self._app_id)
1330
+ self._app_name = params.app_name or self._app_spec.name
1331
+ self._default_sender = params.default_sender
1332
+ self._default_signer = params.default_signer
1333
+ self._approval_source_map = params.approval_source_map
1334
+ self._clear_source_map = params.clear_source_map
1335
+ self._state_accessor = _StateAccessor(self)
1336
+ self._params_accessor = _MethodParamsBuilder(self)
1337
+ self._send_accessor = _TransactionSender(self)
1338
+ self._create_transaction_accessor = _TransactionCreator(self)
1339
+
1340
+ @property
1341
+ def algorand(self) -> AlgorandClient:
1342
+ """Get the Algorand client instance.
1343
+
1344
+ :return: The Algorand client used by this app client
1345
+ """
1346
+ return self._algorand
1347
+
1348
+ @property
1349
+ def app_id(self) -> int:
1350
+ """Get the application ID.
1351
+
1352
+ :return: The ID of the Algorand application
1353
+ """
1354
+ return self._app_id
1355
+
1356
+ @property
1357
+ def app_address(self) -> str:
1358
+ """Get the application's Algorand address.
1359
+
1360
+ :return: The Algorand address associated with this application
1361
+ """
1362
+ return self._app_address
1363
+
1364
+ @property
1365
+ def app_name(self) -> str:
1366
+ """Get the application name.
1367
+
1368
+ :return: The name of the application
1369
+ """
1370
+ return self._app_name
1371
+
1372
+ @property
1373
+ def app_spec(self) -> Arc56Contract:
1374
+ """Get the application specification.
1375
+
1376
+ :return: The ARC-56 contract specification for this application
1377
+ """
1378
+ return self._app_spec
1379
+
1380
+ @property
1381
+ def state(self) -> _StateAccessor:
1382
+ """Get the state accessor.
1383
+
1384
+ :return: The state accessor for this application
1385
+ """
1386
+ return self._state_accessor
1387
+
1388
+ @property
1389
+ def params(self) -> _MethodParamsBuilder:
1390
+ """Get the method parameters builder.
1391
+
1392
+ :return: The method parameters builder for this application
1393
+ """
1394
+ return self._params_accessor
1395
+
1396
+ @property
1397
+ def send(self) -> _TransactionSender:
1398
+ """Get the transaction sender.
1399
+
1400
+ :return: The transaction sender for this application
1401
+ """
1402
+ return self._send_accessor
1403
+
1404
+ @property
1405
+ def create_transaction(self) -> _TransactionCreator:
1406
+ """Get the transaction creator.
1407
+
1408
+ :return: The transaction creator for this application
1409
+ """
1410
+ return self._create_transaction_accessor
1411
+
1412
+ @staticmethod
1413
+ def normalise_app_spec(app_spec: Arc56Contract | Arc32Contract | str) -> Arc56Contract:
1414
+ """Normalize an application specification to ARC-56 format.
1415
+
1416
+ :param app_spec: The application specification to normalize
1417
+ :return: The normalized ARC-56 contract specification
1418
+ :raises ValueError: If the app spec format is invalid
1419
+ """
1420
+ if isinstance(app_spec, str):
1421
+ spec_dict = json.loads(app_spec)
1422
+ spec = Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict
1423
+ else:
1424
+ spec = app_spec
1425
+
1426
+ match spec:
1427
+ case Arc56Contract():
1428
+ return spec
1429
+ case Arc32Contract():
1430
+ return Arc56Contract.from_arc32(spec.to_json())
1431
+ case dict():
1432
+ return Arc56Contract.from_dict(spec)
1433
+ case _:
1434
+ raise ValueError("Invalid app spec format")
1435
+
1436
+ @staticmethod
1437
+ def from_network(
1438
+ app_spec: Arc56Contract | Arc32Contract | str,
1439
+ algorand: AlgorandClient,
1440
+ app_name: str | None = None,
1441
+ default_sender: str | None = None,
1442
+ default_signer: TransactionSigner | None = None,
1443
+ approval_source_map: SourceMap | None = None,
1444
+ clear_source_map: SourceMap | None = None,
1445
+ ) -> AppClient:
1446
+ """Create an AppClient instance from network information.
1447
+
1448
+ :param app_spec: The application specification
1449
+ :param algorand: The Algorand client instance
1450
+ :param app_name: Optional application name
1451
+ :param default_sender: Optional default sender address
1452
+ :param default_signer: Optional default transaction signer
1453
+ :param approval_source_map: Optional approval program source map
1454
+ :param clear_source_map: Optional clear program source map
1455
+ :return: A new AppClient instance
1456
+ :raises Exception: If no app ID is found for the network
1457
+ """
1458
+ network = algorand.client.network()
1459
+ app_spec = AppClient.normalise_app_spec(app_spec)
1460
+ network_names = [network.genesis_hash]
1461
+
1462
+ if network.is_localnet:
1463
+ network_names.append("localnet")
1464
+ if network.is_mainnet:
1465
+ network_names.append("mainnet")
1466
+ if network.is_testnet:
1467
+ network_names.append("testnet")
1468
+
1469
+ available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else []
1470
+ network_index = next((i for i, n in enumerate(available_app_spec_networks) if n in network_names), None)
1471
+
1472
+ if network_index is None:
1473
+ raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec")
1474
+
1475
+ app_id = app_spec.networks[available_app_spec_networks[network_index]].app_id # type: ignore[index]
1476
+
1477
+ return AppClient(
1478
+ AppClientParams(
1479
+ app_id=app_id,
1480
+ app_spec=app_spec,
1481
+ algorand=algorand,
1482
+ app_name=app_name,
1483
+ default_sender=default_sender,
1484
+ default_signer=default_signer,
1485
+ approval_source_map=approval_source_map,
1486
+ clear_source_map=clear_source_map,
1487
+ )
1488
+ )
1489
+
1490
+ @staticmethod
1491
+ def from_creator_and_name(
1492
+ creator_address: str,
1493
+ app_name: str,
1494
+ app_spec: Arc56Contract | Arc32Contract | str,
1495
+ algorand: AlgorandClient,
1496
+ default_sender: str | None = None,
1497
+ default_signer: TransactionSigner | None = None,
1498
+ approval_source_map: SourceMap | None = None,
1499
+ clear_source_map: SourceMap | None = None,
1500
+ ignore_cache: bool | None = None,
1501
+ app_lookup_cache: ApplicationLookup | None = None,
1502
+ ) -> AppClient:
1503
+ """Create an AppClient instance from creator address and application name.
1504
+
1505
+ :param creator_address: The address of the application creator
1506
+ :param app_name: The name of the application
1507
+ :param app_spec: The application specification
1508
+ :param algorand: The Algorand client instance
1509
+ :param default_sender: Optional default sender address
1510
+ :param default_signer: Optional default transaction signer
1511
+ :param approval_source_map: Optional approval program source map
1512
+ :param clear_source_map: Optional clear program source map
1513
+ :param ignore_cache: Optional flag to ignore cache
1514
+ :param app_lookup_cache: Optional app lookup cache
1515
+ :return: A new AppClient instance
1516
+ :raises ValueError: If the app is not found for the creator and name
1517
+ """
1518
+ app_spec_ = AppClient.normalise_app_spec(app_spec)
1519
+ app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name(
1520
+ creator_address=creator_address, ignore_cache=ignore_cache or False
1521
+ )
1522
+ app_metadata = app_lookup.apps.get(app_name or app_spec_.name)
1523
+ if not app_metadata:
1524
+ raise ValueError(f"App not found for creator {creator_address} and name {app_name or app_spec_.name}")
1525
+
1526
+ return AppClient(
1527
+ AppClientParams(
1528
+ app_id=app_metadata.app_id,
1529
+ app_spec=app_spec_,
1530
+ algorand=algorand,
1531
+ app_name=app_name,
1532
+ default_sender=default_sender,
1533
+ default_signer=default_signer,
1534
+ approval_source_map=approval_source_map,
1535
+ clear_source_map=clear_source_map,
1536
+ )
1537
+ )
1538
+
1539
+ @staticmethod
1540
+ def compile(
1541
+ app_spec: Arc56Contract,
1542
+ app_manager: AppManager,
1543
+ compilation_params: AppClientCompilationParams | None = None,
1544
+ ) -> AppClientCompilationResult:
1545
+ """Compile the application's TEAL code.
1546
+
1547
+ :param app_spec: The application specification
1548
+ :param app_manager: The application manager instance
1549
+ :param compilation_params: Optional compilation parameters
1550
+ :return: The compilation result
1551
+ :raises ValueError: If attempting to compile without source or byte code
1552
+ """
1553
+ compilation_params = compilation_params or AppClientCompilationParams()
1554
+ deploy_time_params = compilation_params.get("deploy_time_params")
1555
+ updatable = compilation_params.get("updatable")
1556
+ deletable = compilation_params.get("deletable")
1557
+
1558
+ def is_base64(s: str) -> bool:
1559
+ try:
1560
+ return base64.b64encode(base64.b64decode(s)).decode() == s
1561
+ except Exception:
1562
+ return False
1563
+
1564
+ if not app_spec.source:
1565
+ if not app_spec.byte_code or not app_spec.byte_code.approval or not app_spec.byte_code.clear:
1566
+ raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code")
1567
+
1568
+ return AppClientCompilationResult(
1569
+ approval_program=base64.b64decode(app_spec.byte_code.approval),
1570
+ clear_state_program=base64.b64decode(app_spec.byte_code.clear),
1571
+ )
1572
+
1573
+ compiled_approval = app_manager.compile_teal_template(
1574
+ app_spec.source.get_decoded_approval(),
1575
+ template_params=deploy_time_params,
1576
+ deployment_metadata=(
1577
+ {"updatable": updatable, "deletable": deletable}
1578
+ if updatable is not None or deletable is not None
1579
+ else None
1580
+ ),
1581
+ )
1582
+
1583
+ compiled_clear = app_manager.compile_teal_template(
1584
+ app_spec.source.get_decoded_clear(),
1585
+ template_params=deploy_time_params,
1586
+ )
1587
+
1588
+ if config.debug and config.project_root:
1589
+ persist_sourcemaps(
1590
+ sources=[
1591
+ PersistSourceMapInput(
1592
+ compiled_teal=compiled_approval, app_name=app_spec.name, file_name="approval.teal"
1593
+ ),
1594
+ PersistSourceMapInput(compiled_teal=compiled_clear, app_name=app_spec.name, file_name="clear.teal"),
1595
+ ],
1596
+ project_root=config.project_root,
1597
+ client=app_manager._algod,
1598
+ with_sources=True,
1599
+ )
1600
+
1601
+ return AppClientCompilationResult(
1602
+ approval_program=compiled_approval.compiled_base64_to_bytes,
1603
+ compiled_approval=compiled_approval,
1604
+ clear_state_program=compiled_clear.compiled_base64_to_bytes,
1605
+ compiled_clear=compiled_clear,
1606
+ )
1607
+
1608
+ @staticmethod
1609
+ def _expose_logic_error_static( # noqa: C901
1610
+ *,
1611
+ e: Exception,
1612
+ app_spec: Arc56Contract,
1613
+ is_clear_state_program: bool = False,
1614
+ approval_source_map: SourceMap | None = None,
1615
+ clear_source_map: SourceMap | None = None,
1616
+ program: bytes | None = None,
1617
+ approval_source_info: ProgramSourceInfo | None = None,
1618
+ clear_source_info: ProgramSourceInfo | None = None,
1619
+ ) -> Exception:
1620
+ source_map = clear_source_map if is_clear_state_program else approval_source_map
1621
+
1622
+ error_details = parse_logic_error(str(e))
1623
+ if not error_details:
1624
+ return e
1625
+
1626
+ # The PC value to find in the ARC56 SourceInfo
1627
+ arc56_pc = error_details["pc"]
1628
+
1629
+ program_source_info = clear_source_info if is_clear_state_program else approval_source_info
1630
+
1631
+ # The offset to apply to the PC if using the cblocks pc offset method
1632
+ cblocks_offset = 0
1633
+
1634
+ # If the program uses cblocks offset, then we need to adjust the PC accordingly
1635
+ if program_source_info and program_source_info.pc_offset_method == PcOffsetMethod.CBLOCKS:
1636
+ if not program:
1637
+ raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset")
1638
+
1639
+ cblocks_offset = get_constant_block_offset(program)
1640
+ arc56_pc = error_details["pc"] - cblocks_offset
1641
+
1642
+ # Find the source info for this PC and get the error message
1643
+ source_info = None
1644
+ if program_source_info and program_source_info.source_info:
1645
+ source_info = next(
1646
+ (s for s in program_source_info.source_info if isinstance(s, SourceInfo) and arc56_pc in s.pc),
1647
+ None,
1648
+ )
1649
+ error_message = source_info.error_message if source_info else None
1650
+
1651
+ # If we have the source we can display the TEAL in the error message
1652
+ if hasattr(app_spec, "source"):
1653
+ program_source = (
1654
+ (
1655
+ app_spec.source.get_decoded_clear()
1656
+ if is_clear_state_program
1657
+ else app_spec.source.get_decoded_approval()
1658
+ )
1659
+ if app_spec.source
1660
+ else None
1661
+ )
1662
+ custom_get_line_for_pc = None
1663
+
1664
+ def get_line_for_pc(input_pc: int) -> int | None:
1665
+ if not program_source_info:
1666
+ return None
1667
+ teal = [line.teal for line in program_source_info.source_info if input_pc - cblocks_offset in line.pc]
1668
+ return teal[0] if teal else None
1669
+
1670
+ if not source_map:
1671
+ custom_get_line_for_pc = get_line_for_pc
1672
+
1673
+ if program_source:
1674
+ e = LogicError(
1675
+ logic_error_str=str(e),
1676
+ program=program_source,
1677
+ source_map=source_map,
1678
+ transaction_id=error_details["transaction_id"],
1679
+ message=error_details["message"],
1680
+ pc=error_details["pc"],
1681
+ logic_error=e,
1682
+ get_line_for_pc=custom_get_line_for_pc,
1683
+ traces=None,
1684
+ )
1685
+
1686
+ if error_message:
1687
+ import re
1688
+
1689
+ message = e.logic_error_str if isinstance(e, LogicError) else str(e)
1690
+ app_id = re.search(r"(?<=app=)\d+", message)
1691
+ tx_id = re.search(r"(?<=transaction )\S+(?=:)", message)
1692
+ error = Exception(
1693
+ f"Runtime error when executing {app_spec.name} "
1694
+ f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction "
1695
+ f"{tx_id.group() if tx_id else 'N/A'}: {error_message}"
1696
+ )
1697
+ error.__cause__ = e
1698
+ return error
1699
+
1700
+ return e
1701
+
1702
+ def compile_app(
1703
+ self,
1704
+ compilation_params: AppClientCompilationParams | None = None,
1705
+ ) -> AppClientCompilationResult:
1706
+ """Compile the application's TEAL code.
1707
+
1708
+ :param compilation_params: Optional compilation parameters
1709
+ :return: The compilation result
1710
+ """
1711
+ result = AppClient.compile(self._app_spec, self._algorand.app, compilation_params)
1712
+
1713
+ if result.compiled_approval:
1714
+ self._approval_source_map = result.compiled_approval.source_map
1715
+ if result.compiled_clear:
1716
+ self._clear_source_map = result.compiled_clear.source_map
1717
+
1718
+ return result
1719
+
1720
+ def clone(
1721
+ self,
1722
+ app_name: str | None = _MISSING, # type: ignore[assignment]
1723
+ default_sender: str | None = _MISSING, # type: ignore[assignment]
1724
+ default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment]
1725
+ approval_source_map: SourceMap | None = _MISSING, # type: ignore[assignment]
1726
+ clear_source_map: SourceMap | None = _MISSING, # type: ignore[assignment]
1727
+ ) -> AppClient:
1728
+ """Create a cloned AppClient instance with optionally overridden parameters.
1729
+
1730
+ :param app_name: Optional new application name
1731
+ :param default_sender: Optional new default sender
1732
+ :param default_signer: Optional new default signer
1733
+ :param approval_source_map: Optional new approval source map
1734
+ :param clear_source_map: Optional new clear source map
1735
+ :return: A new AppClient instance with the specified parameters
1736
+ """
1737
+ return AppClient(
1738
+ AppClientParams(
1739
+ app_id=self._app_id,
1740
+ algorand=self._algorand,
1741
+ app_spec=self._app_spec,
1742
+ app_name=self._app_name if app_name is _MISSING else app_name,
1743
+ default_sender=self._default_sender if default_sender is _MISSING else default_sender,
1744
+ default_signer=self._default_signer if default_signer is _MISSING else default_signer,
1745
+ approval_source_map=(
1746
+ self._approval_source_map if approval_source_map is _MISSING else approval_source_map
1747
+ ),
1748
+ clear_source_map=(self._clear_source_map if clear_source_map is _MISSING else clear_source_map),
1749
+ )
1750
+ )
1751
+
1752
+ def export_source_maps(self) -> AppSourceMaps:
1753
+ """Export the application's source maps.
1754
+
1755
+ :return: The application's source maps
1756
+ :raises ValueError: If source maps haven't been loaded
1757
+ """
1758
+ if not self._approval_source_map or not self._clear_source_map:
1759
+ raise ValueError(
1760
+ "Unable to export source maps; they haven't been loaded into this client - "
1761
+ "you need to call create, update, or deploy first"
1762
+ )
1763
+
1764
+ return AppSourceMaps(
1765
+ approval_source_map=self._approval_source_map,
1766
+ clear_source_map=self._clear_source_map,
1767
+ )
1768
+
1769
+ def import_source_maps(self, source_maps: AppSourceMaps) -> None:
1770
+ """Import source maps for the application.
1771
+
1772
+ :param source_maps: The source maps to import
1773
+ :raises ValueError: If source maps are invalid or missing
1774
+ """
1775
+ if not source_maps.approval_source_map:
1776
+ raise ValueError("Approval source map is required")
1777
+ if not source_maps.clear_source_map:
1778
+ raise ValueError("Clear source map is required")
1779
+
1780
+ if not isinstance(source_maps.approval_source_map, dict | SourceMap):
1781
+ raise ValueError(
1782
+ "Approval source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`"
1783
+ )
1784
+ if not isinstance(source_maps.clear_source_map, dict | SourceMap):
1785
+ raise ValueError(
1786
+ "Clear source map supplied is of invalid type. Must be a raw dict or `algosdk.source_map.SourceMap`"
1787
+ )
1788
+
1789
+ self._approval_source_map = (
1790
+ SourceMap(source_map=source_maps.approval_source_map)
1791
+ if isinstance(source_maps.approval_source_map, dict)
1792
+ else source_maps.approval_source_map
1793
+ )
1794
+ self._clear_source_map = (
1795
+ SourceMap(source_map=source_maps.clear_source_map)
1796
+ if isinstance(source_maps.clear_source_map, dict)
1797
+ else source_maps.clear_source_map
1798
+ )
1799
+
1800
+ def get_local_state(self, address: str) -> dict[str, AppState]:
1801
+ """Get local state for an account.
1802
+
1803
+ :param address: The account address
1804
+ :return: The account's local state for this application
1805
+ """
1806
+ return self._state_accessor.get_local_state(address)
1807
+
1808
+ def get_global_state(self) -> dict[str, AppState]:
1809
+ """Get the application's global state.
1810
+
1811
+ :return: The application's global state
1812
+ """
1813
+ return self._state_accessor.get_global_state()
1814
+
1815
+ def get_box_names(self) -> list[BoxName]:
1816
+ """Get all box names for the application.
1817
+
1818
+ :return: List of box names
1819
+ """
1820
+ return self._algorand.app.get_box_names(self._app_id)
1821
+
1822
+ def get_box_value(self, name: BoxIdentifier) -> bytes:
1823
+ """Get the value of a box.
1824
+
1825
+ :param name: The box identifier
1826
+ :return: The box value as bytes
1827
+ """
1828
+ return self._algorand.app.get_box_value(self._app_id, name)
1829
+
1830
+ def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue:
1831
+ """Get a box value decoded according to an ABI type.
1832
+
1833
+ :param name: The box identifier
1834
+ :param abi_type: The ABI type to decode as
1835
+ :return: The decoded box value
1836
+ """
1837
+ return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type)
1838
+
1839
+ def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]:
1840
+ """Get values for multiple boxes.
1841
+
1842
+ :param filter_func: Optional function to filter box names
1843
+ :return: List of box values
1844
+ """
1845
+ names = [n for n in self.get_box_names() if not filter_func or filter_func(n)]
1846
+ values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names])
1847
+ return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)]
1848
+
1849
+ def get_box_values_from_abi_type(
1850
+ self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None
1851
+ ) -> list[BoxABIValue]:
1852
+ """Get multiple box values decoded according to an ABI type.
1853
+
1854
+ :param abi_type: The ABI type to decode as
1855
+ :param filter_func: Optional function to filter box names
1856
+ :return: List of decoded box values
1857
+ """
1858
+ names = self.get_box_names()
1859
+ if filter_func:
1860
+ names = [name for name in names if filter_func(name)]
1861
+
1862
+ values = self._algorand.app.get_box_values_from_abi_type(
1863
+ self.app_id, [name.name_raw for name in names], abi_type
1864
+ )
1865
+
1866
+ return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)]
1867
+
1868
+ def fund_app_account(
1869
+ self, params: FundAppAccountParams, send_params: SendParams | None = None
1870
+ ) -> SendSingleTransactionResult:
1871
+ """Fund the application's account.
1872
+
1873
+ :param params: The funding parameters
1874
+ :param send_params: Send parameters, defaults to None
1875
+ :return: The transaction result
1876
+ """
1877
+ return self.send.fund_app_account(params, send_params)
1878
+
1879
+ def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception:
1880
+ source_info = None
1881
+ if hasattr(self._app_spec, "source_info") and self._app_spec.source_info:
1882
+ source_info = (
1883
+ self._app_spec.source_info.clear if is_clear_state_program else self._app_spec.source_info.approval
1884
+ )
1885
+
1886
+ pc_offset_method = source_info.pc_offset_method if source_info else None
1887
+
1888
+ program: bytes | None = None
1889
+ if pc_offset_method == "cblocks":
1890
+ app_info = self._algorand.app.get_by_id(self.app_id)
1891
+ program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program
1892
+
1893
+ return AppClient._expose_logic_error_static(
1894
+ e=e,
1895
+ app_spec=self._app_spec,
1896
+ is_clear_state_program=is_clear_state_program,
1897
+ approval_source_map=self._approval_source_map,
1898
+ clear_source_map=self._clear_source_map,
1899
+ program=program,
1900
+ approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None),
1901
+ clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None),
1902
+ )
1903
+
1904
+ def _handle_call_errors(self, call: Callable[[], T]) -> T:
1905
+ try:
1906
+ return call()
1907
+ except Exception as e:
1908
+ raise self._expose_logic_error(e=e) from None
1909
+
1910
+ def _get_sender(self, sender: str | None) -> str:
1911
+ if not sender and not self._default_sender:
1912
+ raise Exception(
1913
+ f"No sender provided and no default sender present in app client for call to app {self.app_name}"
1914
+ )
1915
+ return sender or self._default_sender # type: ignore[return-value]
1916
+
1917
+ def _get_signer(
1918
+ self, sender: str | None, signer: TransactionSigner | TransactionSignerAccountProtocol | None
1919
+ ) -> TransactionSigner | TransactionSignerAccountProtocol | None:
1920
+ return signer or (self._default_signer if not sender or sender == self._default_sender else None)
1921
+
1922
+ def _get_bare_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
1923
+ sender = self._get_sender(params.get("sender"))
1924
+ return {
1925
+ **params,
1926
+ "app_id": self._app_id,
1927
+ "sender": sender,
1928
+ "signer": self._get_signer(params.get("sender"), params.get("signer")),
1929
+ "on_complete": on_complete,
1930
+ }
1931
+
1932
+ def _get_abi_args_with_default_values( # noqa: C901, PLR0912
1933
+ self,
1934
+ *,
1935
+ method_name_or_signature: str,
1936
+ args: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None,
1937
+ sender: str,
1938
+ ) -> list[Any]:
1939
+ method = self._app_spec.get_arc56_method(method_name_or_signature)
1940
+ result: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] = []
1941
+
1942
+ for i, method_arg in enumerate(method.args):
1943
+ arg_value = args[i] if args and i < len(args) else None
1944
+
1945
+ if arg_value is not None:
1946
+ if method_arg.struct and isinstance(arg_value, dict):
1947
+ arg_value = get_abi_tuple_from_abi_struct(
1948
+ arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs
1949
+ )
1950
+ result.append(arg_value)
1951
+ continue
1952
+
1953
+ default_value = method_arg.default_value
1954
+ if default_value:
1955
+ match default_value.source:
1956
+ case "literal":
1957
+ value_raw = base64.b64decode(default_value.data)
1958
+ value_type = default_value.type or method_arg.type
1959
+ result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs))
1960
+
1961
+ case "method":
1962
+ default_method = self._app_spec.get_arc56_method(default_value.data)
1963
+ empty_args = [None] * len(default_method.args)
1964
+ call_result = self.send.call(
1965
+ AppClientMethodCallParams(
1966
+ method=default_value.data,
1967
+ args=empty_args,
1968
+ sender=sender,
1969
+ )
1970
+ )
1971
+
1972
+ if not call_result.abi_return:
1973
+ raise ValueError("Default value method call did not return a value")
1974
+
1975
+ if isinstance(call_result.abi_return, dict):
1976
+ result.append(
1977
+ get_abi_tuple_from_abi_struct(
1978
+ call_result.abi_return,
1979
+ self._app_spec.structs[str(default_method.returns.struct)],
1980
+ self._app_spec.structs,
1981
+ )
1982
+ )
1983
+ elif call_result.abi_return:
1984
+ result.append(call_result.abi_return)
1985
+
1986
+ case "local" | "global":
1987
+ state = (
1988
+ self.get_global_state()
1989
+ if default_value.source == "global"
1990
+ else self.get_local_state(sender)
1991
+ )
1992
+ value = next((s for s in state.values() if s.key_base64 == default_value.data), None)
1993
+ if not value:
1994
+ raise ValueError(
1995
+ f"Key '{default_value.data}' not found in {default_value.source} "
1996
+ f"storage for argument {method_arg.name or f'arg{i+1}'}"
1997
+ )
1998
+
1999
+ if value.value_raw:
2000
+ value_type = default_value.type or method_arg.type
2001
+ result.append(get_abi_decoded_value(value.value_raw, value_type, self._app_spec.structs))
2002
+ else:
2003
+ result.append(value.value)
2004
+
2005
+ case "box":
2006
+ box_name = base64.b64decode(default_value.data)
2007
+ box_value = self._algorand.app.get_box_value(self._app_id, box_name)
2008
+ value_type = default_value.type or method_arg.type
2009
+ result.append(get_abi_decoded_value(box_value, value_type, self._app_spec.structs))
2010
+
2011
+ elif not algosdk.abi.is_abi_transaction_type(method_arg.type):
2012
+ raise ValueError(
2013
+ f"No value provided for required argument "
2014
+ f"{method_arg.name or f'arg{i+1}'} in call to method {method.name}"
2015
+ )
2016
+ elif arg_value is None and default_value is None:
2017
+ # At this point only allow explicit None values if no default value was identified
2018
+ result.append(None)
2019
+
2020
+ return result
2021
+
2022
+ def _get_abi_params(self, params: dict[str, Any], on_complete: algosdk.transaction.OnComplete) -> dict[str, Any]:
2023
+ sender = self._get_sender(params.get("sender"))
2024
+ method = self._app_spec.get_arc56_method(params["method"])
2025
+ args = self._get_abi_args_with_default_values(
2026
+ method_name_or_signature=params["method"], args=params.get("args"), sender=sender
2027
+ )
2028
+ return {
2029
+ **params,
2030
+ "appId": self._app_id,
2031
+ "sender": sender,
2032
+ "signer": self._get_signer(params.get("sender"), params.get("signer")),
2033
+ "method": method,
2034
+ "onComplete": on_complete,
2035
+ "args": args,
2036
+ }
2037
+
2038
+ def _process_method_call_return(
2039
+ self,
2040
+ result: Callable[[], SendAppUpdateTransactionResult[ABIReturn] | SendAppTransactionResult[ABIReturn]],
2041
+ method: Method,
2042
+ ) -> SendAppUpdateTransactionResult[Arc56ReturnValueType] | SendAppTransactionResult[Arc56ReturnValueType]:
2043
+ result_value = result()
2044
+ abi_return = (
2045
+ result_value.abi_return.get_arc56_value(method, self._app_spec.structs)
2046
+ if isinstance(result_value.abi_return, ABIReturn)
2047
+ else None
2048
+ )
2049
+
2050
+ if isinstance(result_value, SendAppUpdateTransactionResult):
2051
+ return SendAppUpdateTransactionResult[Arc56ReturnValueType](
2052
+ **{**result_value.__dict__, "abi_return": abi_return}
2053
+ )
2054
+ return SendAppTransactionResult[Arc56ReturnValueType](**{**result_value.__dict__, "abi_return": abi_return})