algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of algokit-utils might be problematic. Click here for more details.

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