algokit-utils 2.4.0__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.0.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.0.dist-info → algokit_utils-3.0.0.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0.dist-info → algokit_utils-3.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,725 @@
1
+ import base64
2
+ import dataclasses
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+ from typing import Literal
6
+
7
+ from algosdk.logic import get_application_address
8
+ from algosdk.v2client.indexer import IndexerClient
9
+
10
+ from algokit_utils.applications.abi import ABIReturn
11
+ from algokit_utils.applications.app_manager import AppManager
12
+ from algokit_utils.applications.enums import OnSchemaBreak, OnUpdate, OperationPerformed
13
+ from algokit_utils.config import config
14
+ from algokit_utils.models.state import TealTemplateParams
15
+ from algokit_utils.models.transaction import SendParams
16
+ from algokit_utils.transactions.transaction_composer import (
17
+ AppCreateMethodCallParams,
18
+ AppCreateParams,
19
+ AppDeleteMethodCallParams,
20
+ AppDeleteParams,
21
+ AppUpdateMethodCallParams,
22
+ AppUpdateParams,
23
+ TransactionComposer,
24
+ calculate_extra_program_pages,
25
+ )
26
+ from algokit_utils.transactions.transaction_sender import (
27
+ AlgorandClientTransactionSender,
28
+ SendAppCreateTransactionResult,
29
+ SendAppTransactionResult,
30
+ SendAppUpdateTransactionResult,
31
+ )
32
+
33
+ __all__ = [
34
+ "APP_DEPLOY_NOTE_DAPP",
35
+ "AppDeployParams",
36
+ "AppDeployResult",
37
+ "AppDeployer",
38
+ "AppDeploymentMetaData",
39
+ "ApplicationLookup",
40
+ "ApplicationMetaData",
41
+ "ApplicationReference",
42
+ "OnSchemaBreak",
43
+ "OnUpdate",
44
+ "OperationPerformed",
45
+ ]
46
+
47
+
48
+ APP_DEPLOY_NOTE_DAPP: str = "ALGOKIT_DEPLOYER"
49
+
50
+ logger = config.logger
51
+
52
+
53
+ @dataclasses.dataclass
54
+ class AppDeploymentMetaData:
55
+ """Metadata about an application stored in a transaction note during creation."""
56
+
57
+ name: str
58
+ version: str
59
+ deletable: bool | None
60
+ updatable: bool | None
61
+
62
+ def dictify(self) -> dict[str, str | bool]:
63
+ return {k: v for k, v in asdict(self).items() if v is not None}
64
+
65
+
66
+ @dataclasses.dataclass(frozen=True)
67
+ class ApplicationReference:
68
+ """Information about an Algorand app"""
69
+
70
+ app_id: int
71
+ app_address: str
72
+
73
+
74
+ @dataclasses.dataclass(frozen=True)
75
+ class ApplicationMetaData:
76
+ """Complete metadata about a deployed app"""
77
+
78
+ reference: ApplicationReference
79
+ deploy_metadata: AppDeploymentMetaData
80
+ created_round: int
81
+ updated_round: int
82
+ deleted: bool = False
83
+
84
+ @property
85
+ def app_id(self) -> int:
86
+ return self.reference.app_id
87
+
88
+ @property
89
+ def app_address(self) -> str:
90
+ return self.reference.app_address
91
+
92
+ @property
93
+ def name(self) -> str:
94
+ return self.deploy_metadata.name
95
+
96
+ @property
97
+ def version(self) -> str:
98
+ return self.deploy_metadata.version
99
+
100
+ @property
101
+ def deletable(self) -> bool | None:
102
+ return self.deploy_metadata.deletable
103
+
104
+ @property
105
+ def updatable(self) -> bool | None:
106
+ return self.deploy_metadata.updatable
107
+
108
+
109
+ @dataclasses.dataclass
110
+ class ApplicationLookup:
111
+ """Cache of {py:class}`ApplicationMetaData` for a specific `creator`
112
+
113
+ Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple
114
+ apps or discovering multiple app_ids
115
+ """
116
+
117
+ creator: str
118
+ apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict)
119
+
120
+
121
+ @dataclass(kw_only=True)
122
+ class AppDeployParams:
123
+ """Parameters for deploying an app"""
124
+
125
+ metadata: AppDeploymentMetaData
126
+ """The deployment metadata"""
127
+ deploy_time_params: TealTemplateParams | None = None
128
+ """Optional template parameters to use during compilation"""
129
+ on_schema_break: (Literal["replace", "fail", "append"] | OnSchemaBreak) | None = None
130
+ """Optional on schema break action"""
131
+ on_update: (Literal["update", "replace", "fail", "append"] | OnUpdate) | None = None
132
+ """Optional on update action"""
133
+ create_params: AppCreateParams | AppCreateMethodCallParams
134
+ """The creation parameters"""
135
+ update_params: AppUpdateParams | AppUpdateMethodCallParams
136
+ """The update parameters"""
137
+ delete_params: AppDeleteParams | AppDeleteMethodCallParams
138
+ """The deletion parameters"""
139
+ existing_deployments: ApplicationLookup | None = None
140
+ """Optional existing deployments"""
141
+ ignore_cache: bool = False
142
+ """Whether to ignore the cache"""
143
+ max_fee: int | None = None
144
+ """Optional maximum fee"""
145
+ send_params: SendParams | None = None
146
+ """Optional send parameters"""
147
+
148
+
149
+ # Union type for all possible deploy results
150
+ @dataclass(frozen=True)
151
+ class AppDeployResult:
152
+ """The result of a deployment"""
153
+
154
+ app: ApplicationMetaData
155
+ """The application metadata"""
156
+ operation_performed: OperationPerformed
157
+ """The operation performed"""
158
+ create_result: SendAppCreateTransactionResult[ABIReturn] | None = None
159
+ """The create result"""
160
+ update_result: SendAppUpdateTransactionResult[ABIReturn] | None = None
161
+ """The update result"""
162
+ delete_result: SendAppTransactionResult[ABIReturn] | None = None
163
+ """The delete result"""
164
+
165
+
166
+ class AppDeployer:
167
+ """Manages deployment and deployment metadata of applications
168
+
169
+ :param app_manager: The app manager to use
170
+ :param transaction_sender: The transaction sender to use
171
+ :param indexer: The indexer to use
172
+
173
+ :example:
174
+ >>> deployer = AppDeployer(app_manager, transaction_sender, indexer)
175
+ """
176
+
177
+ def __init__(
178
+ self,
179
+ app_manager: AppManager,
180
+ transaction_sender: AlgorandClientTransactionSender,
181
+ indexer: IndexerClient | None = None,
182
+ ):
183
+ self._app_manager = app_manager
184
+ self._transaction_sender = transaction_sender
185
+ self._indexer = indexer
186
+ self._app_lookups: dict[str, ApplicationLookup] = {}
187
+
188
+ def deploy(self, deployment: AppDeployParams) -> AppDeployResult:
189
+ """Idempotently deploy (create if not exists, update if changed) an app against the given name for the given
190
+ creator account, including deploy-time TEAL template placeholder substitutions (if specified).
191
+
192
+ To understand the architecture decisions behind this functionality please see
193
+ https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md
194
+
195
+ **Note:** When using the return from this function be sure to check `operation_performed` to get access to
196
+ return properties like `transaction`, `confirmation` and `delete_result`.
197
+
198
+ **Note:** if there is a breaking state schema change to an existing app (and `on_schema_break` is set to
199
+ `'replace'`) the existing app will be deleted and re-created.
200
+
201
+ **Note:** if there is an update (different TEAL code) to an existing app (and `on_update` is set to `'replace'`)
202
+ the existing app will be deleted and re-created.
203
+
204
+ :param deployment: The arguments to control the app deployment
205
+ :returns: The result of the deployment
206
+ :raises ValueError: If the app spec format is invalid
207
+
208
+ :example:
209
+ >>> deployer.deploy(AppDeployParams(
210
+ ... create_params=AppCreateParams(
211
+ ... sender='SENDER_ADDRESS',
212
+ ... approval_program='APPROVAL PROGRAM',
213
+ ... clear_state_program='CLEAR PROGRAM',
214
+ ... schema={
215
+ ... 'global_byte_slices': 0,
216
+ ... 'global_ints': 0,
217
+ ... 'local_byte_slices': 0,
218
+ ... 'local_ints': 0
219
+ ... }
220
+ ... ),
221
+ ... update_params=AppUpdateParams(
222
+ ... sender='SENDER_ADDRESS'
223
+ ... ),
224
+ ... delete_params=AppDeleteParams(
225
+ ... sender='SENDER_ADDRESS'
226
+ ... ),
227
+ ... metadata=AppDeploymentMetaData(
228
+ ... name='my_app',
229
+ ... version='2.0',
230
+ ... updatable=False,
231
+ ... deletable=False
232
+ ... ),
233
+ ... on_schema_break=OnSchemaBreak.AppendApp,
234
+ ... on_update=OnUpdate.AppendApp
235
+ ... )
236
+ ... )
237
+ """
238
+
239
+ # Create new instances with updated notes
240
+ send_params = deployment.send_params or SendParams()
241
+ suppress_log = send_params.get("suppress_log") or False
242
+
243
+ logger.info(
244
+ f"Idempotently deploying app \"{deployment.metadata.name}\" from creator "
245
+ f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of "
246
+ f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and "
247
+ f"{len(deployment.create_params.clear_state_program)} bytes of "
248
+ f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}",
249
+ extra={"suppress_log": suppress_log},
250
+ )
251
+ note = TransactionComposer.arc2_note(
252
+ {
253
+ "dapp_name": APP_DEPLOY_NOTE_DAPP,
254
+ "format": "j",
255
+ "data": deployment.metadata.dictify(),
256
+ }
257
+ )
258
+ create_params = dataclasses.replace(deployment.create_params, note=note)
259
+ update_params = dataclasses.replace(deployment.update_params, note=note)
260
+
261
+ deployment = dataclasses.replace(
262
+ deployment,
263
+ create_params=create_params,
264
+ update_params=update_params,
265
+ )
266
+
267
+ # Validate inputs
268
+ if (
269
+ deployment.existing_deployments
270
+ and deployment.existing_deployments.creator != deployment.create_params.sender
271
+ ):
272
+ raise ValueError(
273
+ f"Received invalid existingDeployments value for creator "
274
+ f"{deployment.existing_deployments.creator} when attempting to deploy "
275
+ f"for creator {deployment.create_params.sender}"
276
+ )
277
+
278
+ if not deployment.existing_deployments and not self._indexer:
279
+ raise ValueError(
280
+ "Didn't receive an indexer client when this AppManager was created, "
281
+ "but also didn't receive an existingDeployments cache - one of them must be provided"
282
+ )
283
+
284
+ # Compile code if needed
285
+ approval_program = deployment.create_params.approval_program
286
+ clear_program = deployment.create_params.clear_state_program
287
+
288
+ if isinstance(approval_program, str):
289
+ compiled_approval = self._app_manager.compile_teal_template(
290
+ approval_program,
291
+ deployment.deploy_time_params,
292
+ deployment.metadata.__dict__,
293
+ )
294
+ approval_program = compiled_approval.compiled_base64_to_bytes
295
+
296
+ if isinstance(clear_program, str):
297
+ compiled_clear = self._app_manager.compile_teal_template(
298
+ clear_program,
299
+ deployment.deploy_time_params,
300
+ )
301
+ clear_program = compiled_clear.compiled_base64_to_bytes
302
+
303
+ # Get existing app metadata
304
+ apps = deployment.existing_deployments or self.get_creator_apps_by_name(
305
+ creator_address=deployment.create_params.sender,
306
+ ignore_cache=deployment.ignore_cache,
307
+ )
308
+
309
+ existing_app = apps.apps.get(deployment.metadata.name)
310
+ if not existing_app or existing_app.deleted:
311
+ return self._create_app(
312
+ deployment=deployment,
313
+ approval_program=approval_program,
314
+ clear_program=clear_program,
315
+ )
316
+
317
+ # Check for changes
318
+ existing_app_record = self._app_manager.get_by_id(existing_app.app_id)
319
+
320
+ existing_approval = base64.b64encode(existing_app_record.approval_program).decode()
321
+ existing_clear = base64.b64encode(existing_app_record.clear_state_program).decode()
322
+ existing_extra_pages = calculate_extra_program_pages(
323
+ existing_app_record.approval_program, existing_app_record.clear_state_program
324
+ )
325
+
326
+ new_approval = base64.b64encode(approval_program).decode()
327
+ new_clear = base64.b64encode(clear_program).decode()
328
+ new_extra_pages = calculate_extra_program_pages(approval_program, clear_program)
329
+
330
+ is_update = new_approval != existing_approval or new_clear != existing_clear
331
+ is_schema_break = (
332
+ existing_app_record.local_ints
333
+ < (deployment.create_params.schema.get("local_ints", 0) if deployment.create_params.schema else 0)
334
+ or existing_app_record.global_ints
335
+ < (deployment.create_params.schema.get("global_ints", 0) if deployment.create_params.schema else 0)
336
+ or existing_app_record.local_byte_slices
337
+ < (deployment.create_params.schema.get("local_byte_slices", 0) if deployment.create_params.schema else 0)
338
+ or existing_app_record.global_byte_slices
339
+ < (deployment.create_params.schema.get("global_byte_slices", 0) if deployment.create_params.schema else 0)
340
+ or existing_extra_pages < new_extra_pages
341
+ )
342
+
343
+ if is_schema_break:
344
+ logger.warning(
345
+ f"Detected a breaking app schema change in app {existing_app.app_id}:",
346
+ extra={
347
+ "from": {
348
+ "global_ints": existing_app_record.global_ints,
349
+ "global_byte_slices": existing_app_record.global_byte_slices,
350
+ "local_ints": existing_app_record.local_ints,
351
+ "local_byte_slices": existing_app_record.local_byte_slices,
352
+ },
353
+ "to": deployment.create_params.schema,
354
+ "suppress_log": suppress_log,
355
+ },
356
+ )
357
+
358
+ return self._handle_schema_break(
359
+ deployment=deployment,
360
+ existing_app=existing_app,
361
+ approval_program=approval_program,
362
+ clear_program=clear_program,
363
+ )
364
+
365
+ if is_update:
366
+ return self._handle_update(
367
+ deployment=deployment,
368
+ existing_app=existing_app,
369
+ approval_program=approval_program,
370
+ clear_program=clear_program,
371
+ )
372
+
373
+ logger.debug("No detected changes in app, nothing to do.", extra={"suppress_log": suppress_log})
374
+ return AppDeployResult(
375
+ app=existing_app,
376
+ operation_performed=OperationPerformed.Nothing,
377
+ )
378
+
379
+ def _create_app(
380
+ self,
381
+ deployment: AppDeployParams,
382
+ approval_program: bytes,
383
+ clear_program: bytes,
384
+ ) -> AppDeployResult:
385
+ """Create a new application"""
386
+
387
+ if isinstance(deployment.create_params, AppCreateMethodCallParams):
388
+ create_result = self._transaction_sender.app_create_method_call(
389
+ AppCreateMethodCallParams(
390
+ **{
391
+ **asdict(deployment.create_params),
392
+ "approval_program": approval_program,
393
+ "clear_state_program": clear_program,
394
+ }
395
+ ),
396
+ send_params=deployment.send_params,
397
+ )
398
+ else:
399
+ create_result = self._transaction_sender.app_create(
400
+ AppCreateParams(
401
+ **{
402
+ **asdict(deployment.create_params),
403
+ "approval_program": approval_program,
404
+ "clear_state_program": clear_program,
405
+ }
406
+ ),
407
+ send_params=deployment.send_params,
408
+ )
409
+
410
+ app_metadata = ApplicationMetaData(
411
+ reference=ApplicationReference(
412
+ app_id=create_result.app_id, app_address=get_application_address(create_result.app_id)
413
+ ),
414
+ deploy_metadata=deployment.metadata,
415
+ created_round=create_result.confirmation.get("confirmed-round", 0)
416
+ if isinstance(create_result.confirmation, dict)
417
+ else 0,
418
+ updated_round=create_result.confirmation.get("confirmed-round", 0)
419
+ if isinstance(create_result.confirmation, dict)
420
+ else 0,
421
+ deleted=False,
422
+ )
423
+
424
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
425
+ logger.debug(
426
+ f"Sent transaction ID {create_result.app_id} (AppCreate) from {deployment.create_params.sender}",
427
+ extra={
428
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
429
+ },
430
+ )
431
+
432
+ return AppDeployResult(
433
+ app=app_metadata,
434
+ operation_performed=OperationPerformed.Create,
435
+ create_result=create_result,
436
+ )
437
+
438
+ def _replace_app(
439
+ self,
440
+ deployment: AppDeployParams,
441
+ existing_app: ApplicationMetaData,
442
+ approval_program: bytes,
443
+ clear_program: bytes,
444
+ ) -> AppDeployResult:
445
+ composer = self._transaction_sender.new_group()
446
+
447
+ # Add create transaction
448
+ if isinstance(deployment.create_params, AppCreateMethodCallParams):
449
+ composer.add_app_create_method_call(
450
+ AppCreateMethodCallParams(
451
+ **{
452
+ **deployment.create_params.__dict__,
453
+ "approval_program": approval_program,
454
+ "clear_state_program": clear_program,
455
+ }
456
+ )
457
+ )
458
+ else:
459
+ composer.add_app_create(
460
+ AppCreateParams(
461
+ **{
462
+ **deployment.create_params.__dict__,
463
+ "approval_program": approval_program,
464
+ "clear_state_program": clear_program,
465
+ }
466
+ )
467
+ )
468
+ create_txn_index = composer.count() - 1
469
+
470
+ # Add delete transaction
471
+ if isinstance(deployment.delete_params, AppDeleteMethodCallParams):
472
+ delete_call_params = AppDeleteMethodCallParams(
473
+ **{
474
+ **deployment.delete_params.__dict__,
475
+ "app_id": existing_app.app_id,
476
+ }
477
+ )
478
+ composer.add_app_delete_method_call(delete_call_params)
479
+ else:
480
+ delete_params = AppDeleteParams(
481
+ **{
482
+ **deployment.delete_params.__dict__,
483
+ "app_id": existing_app.app_id,
484
+ }
485
+ )
486
+ composer.add_app_delete(delete_params)
487
+ delete_txn_index = composer.count() - 1
488
+
489
+ result = composer.send()
490
+
491
+ create_result = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index)
492
+ delete_result = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index)
493
+
494
+ app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload]
495
+ app_metadata = ApplicationMetaData(
496
+ reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)),
497
+ deploy_metadata=deployment.metadata,
498
+ created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload]
499
+ updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload]
500
+ deleted=False,
501
+ )
502
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
503
+ logger.debug(
504
+ f"Group transaction sent: Replaced app {existing_app.app_id} with new app {app_id} from "
505
+ f"{deployment.create_params.sender} (Composer group count: {composer.count()})",
506
+ extra={
507
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
508
+ },
509
+ )
510
+
511
+ return AppDeployResult(
512
+ app=app_metadata,
513
+ operation_performed=OperationPerformed.Replace,
514
+ create_result=create_result,
515
+ update_result=None,
516
+ delete_result=delete_result,
517
+ )
518
+
519
+ def _update_app(
520
+ self,
521
+ deployment: AppDeployParams,
522
+ existing_app: ApplicationMetaData,
523
+ approval_program: bytes,
524
+ clear_program: bytes,
525
+ ) -> AppDeployResult:
526
+ """Update an existing application"""
527
+
528
+ if isinstance(deployment.update_params, AppUpdateMethodCallParams):
529
+ result = self._transaction_sender.app_update_method_call(
530
+ AppUpdateMethodCallParams(
531
+ **{
532
+ **deployment.update_params.__dict__,
533
+ "app_id": existing_app.app_id,
534
+ "approval_program": approval_program,
535
+ "clear_state_program": clear_program,
536
+ }
537
+ ),
538
+ send_params=deployment.send_params,
539
+ )
540
+ else:
541
+ result = self._transaction_sender.app_update(
542
+ AppUpdateParams(
543
+ **{
544
+ **deployment.update_params.__dict__,
545
+ "app_id": existing_app.app_id,
546
+ "approval_program": approval_program,
547
+ "clear_state_program": clear_program,
548
+ }
549
+ ),
550
+ send_params=deployment.send_params,
551
+ )
552
+
553
+ app_metadata = ApplicationMetaData(
554
+ reference=ApplicationReference(app_id=existing_app.app_id, app_address=existing_app.app_address),
555
+ deploy_metadata=deployment.metadata,
556
+ created_round=existing_app.created_round,
557
+ updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0,
558
+ deleted=False,
559
+ )
560
+
561
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
562
+ logger.debug(
563
+ f"Sent transaction ID {existing_app.app_id} (AppUpdate) from {deployment.create_params.sender}",
564
+ extra={
565
+ "suppress_log": deployment.send_params.get("suppress_log") or False if deployment.send_params else False
566
+ },
567
+ )
568
+
569
+ return AppDeployResult(
570
+ app=app_metadata,
571
+ operation_performed=OperationPerformed.Update,
572
+ update_result=result,
573
+ )
574
+
575
+ def _handle_schema_break(
576
+ self,
577
+ deployment: AppDeployParams,
578
+ existing_app: ApplicationMetaData,
579
+ approval_program: bytes,
580
+ clear_program: bytes,
581
+ ) -> AppDeployResult:
582
+ if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail") or deployment.on_schema_break is None:
583
+ raise ValueError(
584
+ "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. "
585
+ "If you want to try deleting and recreating the app then "
586
+ "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp"
587
+ )
588
+
589
+ if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"):
590
+ return self._create_app(deployment, approval_program, clear_program)
591
+
592
+ if existing_app.deletable:
593
+ return self._replace_app(deployment, existing_app, approval_program, clear_program)
594
+ else:
595
+ raise ValueError(
596
+ f"App is {'not' if not existing_app.deletable else ''} deletable and onSchemaBreak=ReplaceApp, "
597
+ "cannot delete and recreate app"
598
+ )
599
+
600
+ def _handle_update(
601
+ self,
602
+ deployment: AppDeployParams,
603
+ existing_app: ApplicationMetaData,
604
+ approval_program: bytes,
605
+ clear_program: bytes,
606
+ ) -> AppDeployResult:
607
+ if deployment.on_update in (OnUpdate.Fail, "fail") or deployment.on_update is None:
608
+ raise ValueError(
609
+ "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail."
610
+ )
611
+
612
+ if deployment.on_update in (OnUpdate.AppendApp, "append"):
613
+ return self._create_app(deployment, approval_program, clear_program)
614
+
615
+ if deployment.on_update in (OnUpdate.UpdateApp, "update"):
616
+ if existing_app.updatable:
617
+ return self._update_app(deployment, existing_app, approval_program, clear_program)
618
+ else:
619
+ raise ValueError(
620
+ f"App is {'not' if not existing_app.updatable else ''} updatable and onUpdate=UpdateApp, "
621
+ "cannot update app"
622
+ )
623
+
624
+ if deployment.on_update in (OnUpdate.ReplaceApp, "replace"):
625
+ if existing_app.deletable:
626
+ return self._replace_app(deployment, existing_app, approval_program, clear_program)
627
+ else:
628
+ raise ValueError(
629
+ f"App is {'not' if not existing_app.deletable else ''} deletable and onUpdate=ReplaceApp, "
630
+ "cannot delete and recreate app"
631
+ )
632
+
633
+ raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}")
634
+
635
+ def _update_app_lookup(self, sender: str, app_metadata: ApplicationMetaData) -> None:
636
+ """Update the app lookup cache"""
637
+
638
+ lookup = self._app_lookups.get(sender)
639
+ if not lookup:
640
+ self._app_lookups[sender] = ApplicationLookup(
641
+ creator=sender,
642
+ apps={app_metadata.name: app_metadata},
643
+ )
644
+ else:
645
+ lookup.apps[app_metadata.name] = app_metadata
646
+
647
+ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> ApplicationLookup:
648
+ """Returns a lookup of name => app metadata (id, address, ...metadata) for all apps created by the given account
649
+ that have an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) `AppDeployNote` as
650
+ the transaction note of the app creation transaction.
651
+
652
+ This function caches the result for the given creator account so that subsequent calls won't require an indexer
653
+ lookup.
654
+
655
+ If the `AppManager` instance wasn't created with an indexer client, this function will throw an error.
656
+
657
+ :param creator_address: The address of the account that is the creator of the apps you want to search for
658
+ :param ignore_cache: Whether or not to ignore the cache and force a lookup, default: use the cache
659
+ :returns: A name-based lookup of the app metadata
660
+ :raises ValueError: If the app spec format is invalid
661
+ :example:
662
+ >>> result = await deployer.get_creator_apps_by_name(creator)
663
+ """
664
+
665
+ if not ignore_cache and creator_address in self._app_lookups:
666
+ return self._app_lookups[creator_address]
667
+
668
+ if not self._indexer:
669
+ raise ValueError(
670
+ "Didn't receive an indexer client when this AppManager was created, "
671
+ "but received a call to get_creator_apps"
672
+ )
673
+
674
+ app_lookup: dict[str, ApplicationMetaData] = {}
675
+
676
+ # Get all apps created by account
677
+ created_apps = self._indexer.search_applications(creator=creator_address)
678
+
679
+ for app in created_apps["applications"]:
680
+ app_id = app["id"]
681
+
682
+ # Get creation transaction
683
+ creation_txns = self._indexer.search_transactions(
684
+ application_id=app_id,
685
+ min_round=app["created-at-round"],
686
+ address=creator_address,
687
+ address_role="sender",
688
+ note_prefix=APP_DEPLOY_NOTE_DAPP.encode(),
689
+ limit=1,
690
+ )
691
+
692
+ if not creation_txns["transactions"]:
693
+ continue
694
+
695
+ creation_txn = creation_txns["transactions"][0]
696
+
697
+ try:
698
+ note = base64.b64decode(creation_txn["note"]).decode()
699
+ if not note.startswith(f"{APP_DEPLOY_NOTE_DAPP}:j"):
700
+ continue
701
+
702
+ metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :])
703
+
704
+ if metadata.get("name"):
705
+ app_lookup[metadata["name"]] = ApplicationMetaData(
706
+ reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)),
707
+ deploy_metadata=AppDeploymentMetaData(
708
+ name=metadata["name"],
709
+ version=metadata.get("version", "1.0"),
710
+ deletable=metadata.get("deletable"),
711
+ updatable=metadata.get("updatable"),
712
+ ),
713
+ created_round=creation_txn["confirmed-round"],
714
+ updated_round=creation_txn["confirmed-round"],
715
+ deleted=app.get("deleted", False),
716
+ )
717
+ except Exception as e:
718
+ logger.warning(
719
+ f"Error processing app {app_id} for creator {creator_address}: {e}",
720
+ )
721
+ continue
722
+
723
+ lookup = ApplicationLookup(creator=creator_address, apps=app_lookup)
724
+ self._app_lookups[creator_address] = lookup
725
+ return lookup