algokit-utils 3.0.0b1__py3-none-any.whl → 3.0.0b2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (70) hide show
  1. algokit_utils/__init__.py +23 -183
  2. algokit_utils/_debugging.py +123 -97
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +19 -18
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +24 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1471 -0
  8. algokit_utils/_legacy_v2/application_specification.py +21 -0
  9. algokit_utils/_legacy_v2/asset.py +168 -0
  10. algokit_utils/_legacy_v2/common.py +28 -0
  11. algokit_utils/_legacy_v2/deploy.py +822 -0
  12. algokit_utils/_legacy_v2/logic_error.py +14 -0
  13. algokit_utils/{models.py → _legacy_v2/models.py} +19 -142
  14. algokit_utils/_legacy_v2/network_clients.py +140 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +909 -0
  18. algokit_utils/accounts/kmd_account_manager.py +159 -0
  19. algokit_utils/algorand.py +265 -0
  20. algokit_utils/application_client.py +9 -1453
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +276 -0
  24. algokit_utils/applications/app_client.py +2056 -0
  25. algokit_utils/applications/app_deployer.py +600 -0
  26. algokit_utils/applications/app_factory.py +826 -0
  27. algokit_utils/applications/app_manager.py +470 -0
  28. algokit_utils/applications/app_spec/__init__.py +2 -0
  29. algokit_utils/applications/app_spec/arc32.py +207 -0
  30. algokit_utils/applications/app_spec/arc56.py +1023 -0
  31. algokit_utils/applications/enums.py +40 -0
  32. algokit_utils/asset.py +32 -168
  33. algokit_utils/assets/__init__.py +1 -0
  34. algokit_utils/assets/asset_manager.py +320 -0
  35. algokit_utils/beta/_utils.py +36 -0
  36. algokit_utils/beta/account_manager.py +4 -195
  37. algokit_utils/beta/algorand_client.py +4 -314
  38. algokit_utils/beta/client_manager.py +5 -74
  39. algokit_utils/beta/composer.py +5 -712
  40. algokit_utils/clients/__init__.py +2 -0
  41. algokit_utils/clients/client_manager.py +656 -0
  42. algokit_utils/clients/dispenser_api_client.py +192 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +71 -18
  45. algokit_utils/deploy.py +7 -892
  46. algokit_utils/dispenser_api.py +8 -176
  47. algokit_utils/errors/__init__.py +1 -0
  48. algokit_utils/errors/logic_error.py +121 -0
  49. algokit_utils/logic_error.py +7 -80
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +193 -0
  52. algokit_utils/models/amount.py +198 -0
  53. algokit_utils/models/application.py +61 -0
  54. algokit_utils/models/network.py +25 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +59 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -152
  59. algokit_utils/protocols/__init__.py +2 -0
  60. algokit_utils/protocols/account.py +22 -0
  61. algokit_utils/protocols/typed_clients.py +108 -0
  62. algokit_utils/transactions/__init__.py +3 -0
  63. algokit_utils/transactions/transaction_composer.py +2293 -0
  64. algokit_utils/transactions/transaction_creator.py +156 -0
  65. algokit_utils/transactions/transaction_sender.py +574 -0
  66. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/METADATA +12 -7
  67. algokit_utils-3.0.0b2.dist-info/RECORD +70 -0
  68. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/WHEEL +1 -1
  69. algokit_utils-3.0.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,600 @@
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
+ )
25
+ from algokit_utils.transactions.transaction_sender import (
26
+ AlgorandClientTransactionSender,
27
+ SendAppCreateTransactionResult,
28
+ SendAppTransactionResult,
29
+ SendAppUpdateTransactionResult,
30
+ )
31
+
32
+ __all__ = [
33
+ "APP_DEPLOY_NOTE_DAPP",
34
+ "AppDeployParams",
35
+ "AppDeployResult",
36
+ "AppDeployer",
37
+ "AppDeploymentMetaData",
38
+ "ApplicationLookup",
39
+ "ApplicationMetaData",
40
+ "ApplicationReference",
41
+ "OnSchemaBreak",
42
+ "OnUpdate",
43
+ "OperationPerformed",
44
+ ]
45
+
46
+
47
+ APP_DEPLOY_NOTE_DAPP: str = "ALGOKIT_DEPLOYER"
48
+
49
+ logger = config.logger
50
+
51
+
52
+ @dataclasses.dataclass
53
+ class AppDeploymentMetaData:
54
+ """Metadata about an application stored in a transaction note during creation."""
55
+
56
+ name: str
57
+ version: str
58
+ deletable: bool | None
59
+ updatable: bool | None
60
+
61
+ def dictify(self) -> dict[str, str | bool]:
62
+ return {k: v for k, v in asdict(self).items() if v is not None}
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class ApplicationReference:
67
+ """Information about an Algorand app"""
68
+
69
+ app_id: int
70
+ app_address: str
71
+
72
+
73
+ @dataclasses.dataclass(frozen=True)
74
+ class ApplicationMetaData:
75
+ """Complete metadata about a deployed app"""
76
+
77
+ reference: ApplicationReference
78
+ deploy_metadata: AppDeploymentMetaData
79
+ created_round: int
80
+ updated_round: int
81
+ deleted: bool = False
82
+
83
+ @property
84
+ def app_id(self) -> int:
85
+ return self.reference.app_id
86
+
87
+ @property
88
+ def app_address(self) -> str:
89
+ return self.reference.app_address
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ return self.deploy_metadata.name
94
+
95
+ @property
96
+ def version(self) -> str:
97
+ return self.deploy_metadata.version
98
+
99
+ @property
100
+ def deletable(self) -> bool | None:
101
+ return self.deploy_metadata.deletable
102
+
103
+ @property
104
+ def updatable(self) -> bool | None:
105
+ return self.deploy_metadata.updatable
106
+
107
+
108
+ @dataclasses.dataclass
109
+ class ApplicationLookup:
110
+ """Cache of {py:class}`ApplicationMetaData` for a specific `creator`
111
+
112
+ Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple
113
+ apps or discovering multiple app_ids
114
+ """
115
+
116
+ creator: str
117
+ apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict)
118
+
119
+
120
+ @dataclass(kw_only=True)
121
+ class AppDeployParams:
122
+ """Parameters for deploying an app"""
123
+
124
+ metadata: AppDeploymentMetaData
125
+ deploy_time_params: TealTemplateParams | None = None
126
+ on_schema_break: (Literal["replace", "fail", "append"] | OnSchemaBreak) | None = None
127
+ on_update: (Literal["update", "replace", "fail", "append"] | OnUpdate) | None = None
128
+ create_params: AppCreateParams | AppCreateMethodCallParams
129
+ update_params: AppUpdateParams | AppUpdateMethodCallParams
130
+ delete_params: AppDeleteParams | AppDeleteMethodCallParams
131
+ existing_deployments: ApplicationLookup | None = None
132
+ ignore_cache: bool = False
133
+ max_fee: int | None = None
134
+ send_params: SendParams | None = None
135
+
136
+
137
+ # Union type for all possible deploy results
138
+ @dataclass(frozen=True)
139
+ class AppDeployResult:
140
+ app: ApplicationMetaData
141
+ operation_performed: OperationPerformed
142
+ create_result: SendAppCreateTransactionResult[ABIReturn] | None = None
143
+ update_result: SendAppUpdateTransactionResult[ABIReturn] | None = None
144
+ delete_result: SendAppTransactionResult[ABIReturn] | None = None
145
+
146
+
147
+ class AppDeployer:
148
+ """Manages deployment and deployment metadata of applications"""
149
+
150
+ def __init__(
151
+ self,
152
+ app_manager: AppManager,
153
+ transaction_sender: AlgorandClientTransactionSender,
154
+ indexer: IndexerClient | None = None,
155
+ ):
156
+ self._app_manager = app_manager
157
+ self._transaction_sender = transaction_sender
158
+ self._indexer = indexer
159
+ self._app_lookups: dict[str, ApplicationLookup] = {}
160
+
161
+ def deploy(self, deployment: AppDeployParams) -> AppDeployResult:
162
+ # Create new instances with updated notes
163
+ send_params = deployment.send_params or SendParams()
164
+ suppress_log = send_params.get("suppress_log") or False
165
+
166
+ logger.info(
167
+ f"Idempotently deploying app \"{deployment.metadata.name}\" from creator "
168
+ f"{deployment.create_params.sender} using {len(deployment.create_params.approval_program)} bytes of "
169
+ f"{'teal code' if isinstance(deployment.create_params.approval_program, str) else 'AVM bytecode'} and "
170
+ f"{len(deployment.create_params.clear_state_program)} bytes of "
171
+ f"{'teal code' if isinstance(deployment.create_params.clear_state_program, str) else 'AVM bytecode'}",
172
+ suppress_log=suppress_log,
173
+ )
174
+ note = TransactionComposer.arc2_note(
175
+ {
176
+ "dapp_name": APP_DEPLOY_NOTE_DAPP,
177
+ "format": "j",
178
+ "data": deployment.metadata.dictify(),
179
+ }
180
+ )
181
+ create_params = dataclasses.replace(deployment.create_params, note=note)
182
+ update_params = dataclasses.replace(deployment.update_params, note=note)
183
+
184
+ deployment = dataclasses.replace(
185
+ deployment,
186
+ create_params=create_params,
187
+ update_params=update_params,
188
+ )
189
+
190
+ # Validate inputs
191
+ if (
192
+ deployment.existing_deployments
193
+ and deployment.existing_deployments.creator != deployment.create_params.sender
194
+ ):
195
+ raise ValueError(
196
+ f"Received invalid existingDeployments value for creator "
197
+ f"{deployment.existing_deployments.creator} when attempting to deploy "
198
+ f"for creator {deployment.create_params.sender}"
199
+ )
200
+
201
+ if not deployment.existing_deployments and not self._indexer:
202
+ raise ValueError(
203
+ "Didn't receive an indexer client when this AppManager was created, "
204
+ "but also didn't receive an existingDeployments cache - one of them must be provided"
205
+ )
206
+
207
+ # Compile code if needed
208
+ approval_program = deployment.create_params.approval_program
209
+ clear_program = deployment.create_params.clear_state_program
210
+
211
+ if isinstance(approval_program, str):
212
+ compiled_approval = self._app_manager.compile_teal_template(
213
+ approval_program,
214
+ deployment.deploy_time_params,
215
+ deployment.metadata.__dict__,
216
+ )
217
+ approval_program = compiled_approval.compiled_base64_to_bytes
218
+
219
+ if isinstance(clear_program, str):
220
+ compiled_clear = self._app_manager.compile_teal_template(
221
+ clear_program,
222
+ deployment.deploy_time_params,
223
+ )
224
+ clear_program = compiled_clear.compiled_base64_to_bytes
225
+
226
+ # Get existing app metadata
227
+ apps = deployment.existing_deployments or self.get_creator_apps_by_name(
228
+ creator_address=deployment.create_params.sender,
229
+ ignore_cache=deployment.ignore_cache,
230
+ )
231
+
232
+ existing_app = apps.apps.get(deployment.metadata.name)
233
+ if not existing_app or existing_app.deleted:
234
+ return self._create_app(
235
+ deployment=deployment,
236
+ approval_program=approval_program,
237
+ clear_program=clear_program,
238
+ )
239
+
240
+ # Check for changes
241
+ existing_app_record = self._app_manager.get_by_id(existing_app.app_id)
242
+
243
+ existing_approval = base64.b64encode(existing_app_record.approval_program).decode()
244
+ existing_clear = base64.b64encode(existing_app_record.clear_state_program).decode()
245
+
246
+ new_approval = base64.b64encode(approval_program).decode()
247
+ new_clear = base64.b64encode(clear_program).decode()
248
+
249
+ is_update = new_approval != existing_approval or new_clear != existing_clear
250
+ is_schema_break = (
251
+ existing_app_record.local_ints
252
+ < (deployment.create_params.schema.get("local_ints", 0) if deployment.create_params.schema else 0)
253
+ or existing_app_record.global_ints
254
+ < (deployment.create_params.schema.get("global_ints", 0) if deployment.create_params.schema else 0)
255
+ or existing_app_record.local_byte_slices
256
+ < (deployment.create_params.schema.get("local_byte_slices", 0) if deployment.create_params.schema else 0)
257
+ or existing_app_record.global_byte_slices
258
+ < (deployment.create_params.schema.get("global_byte_slices", 0) if deployment.create_params.schema else 0)
259
+ )
260
+
261
+ if is_schema_break:
262
+ logger.warning(
263
+ f"Detected a breaking app schema change in app {existing_app.app_id}:",
264
+ extra={
265
+ "from": {
266
+ "global_ints": existing_app_record.global_ints,
267
+ "global_byte_slices": existing_app_record.global_byte_slices,
268
+ "local_ints": existing_app_record.local_ints,
269
+ "local_byte_slices": existing_app_record.local_byte_slices,
270
+ },
271
+ "to": deployment.create_params.schema,
272
+ },
273
+ suppress_log=suppress_log,
274
+ )
275
+
276
+ return self._handle_schema_break(
277
+ deployment=deployment,
278
+ existing_app=existing_app,
279
+ approval_program=approval_program,
280
+ clear_program=clear_program,
281
+ )
282
+
283
+ if is_update:
284
+ return self._handle_update(
285
+ deployment=deployment,
286
+ existing_app=existing_app,
287
+ approval_program=approval_program,
288
+ clear_program=clear_program,
289
+ )
290
+
291
+ logger.debug("No detected changes in app, nothing to do.", suppress_log=suppress_log)
292
+ return AppDeployResult(
293
+ app=existing_app,
294
+ operation_performed=OperationPerformed.Nothing,
295
+ )
296
+
297
+ def _create_app(
298
+ self,
299
+ deployment: AppDeployParams,
300
+ approval_program: bytes,
301
+ clear_program: bytes,
302
+ ) -> AppDeployResult:
303
+ """Create a new application"""
304
+
305
+ if isinstance(deployment.create_params, AppCreateMethodCallParams):
306
+ create_result = self._transaction_sender.app_create_method_call(
307
+ AppCreateMethodCallParams(
308
+ **{
309
+ **asdict(deployment.create_params),
310
+ "approval_program": approval_program,
311
+ "clear_state_program": clear_program,
312
+ }
313
+ ),
314
+ send_params=deployment.send_params,
315
+ )
316
+ else:
317
+ create_result = self._transaction_sender.app_create(
318
+ AppCreateParams(
319
+ **{
320
+ **asdict(deployment.create_params),
321
+ "approval_program": approval_program,
322
+ "clear_state_program": clear_program,
323
+ }
324
+ ),
325
+ send_params=deployment.send_params,
326
+ )
327
+
328
+ app_metadata = ApplicationMetaData(
329
+ reference=ApplicationReference(
330
+ app_id=create_result.app_id, app_address=get_application_address(create_result.app_id)
331
+ ),
332
+ deploy_metadata=deployment.metadata,
333
+ created_round=create_result.confirmation.get("confirmed-round", 0)
334
+ if isinstance(create_result.confirmation, dict)
335
+ else 0,
336
+ updated_round=create_result.confirmation.get("confirmed-round", 0)
337
+ if isinstance(create_result.confirmation, dict)
338
+ else 0,
339
+ deleted=False,
340
+ )
341
+
342
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
343
+
344
+ return AppDeployResult(
345
+ app=app_metadata,
346
+ operation_performed=OperationPerformed.Create,
347
+ create_result=create_result,
348
+ )
349
+
350
+ def _replace_app(
351
+ self,
352
+ deployment: AppDeployParams,
353
+ existing_app: ApplicationMetaData,
354
+ approval_program: bytes,
355
+ clear_program: bytes,
356
+ ) -> AppDeployResult:
357
+ composer = self._transaction_sender.new_group()
358
+
359
+ # Add create transaction
360
+ if isinstance(deployment.create_params, AppCreateMethodCallParams):
361
+ composer.add_app_create_method_call(
362
+ AppCreateMethodCallParams(
363
+ **{
364
+ **deployment.create_params.__dict__,
365
+ "approval_program": approval_program,
366
+ "clear_state_program": clear_program,
367
+ }
368
+ )
369
+ )
370
+ else:
371
+ composer.add_app_create(
372
+ AppCreateParams(
373
+ **{
374
+ **deployment.create_params.__dict__,
375
+ "approval_program": approval_program,
376
+ "clear_state_program": clear_program,
377
+ }
378
+ )
379
+ )
380
+ create_txn_index = composer.count() - 1
381
+
382
+ # Add delete transaction
383
+ if isinstance(deployment.delete_params, AppDeleteMethodCallParams):
384
+ delete_call_params = AppDeleteMethodCallParams(
385
+ **{
386
+ **deployment.delete_params.__dict__,
387
+ "app_id": existing_app.app_id,
388
+ }
389
+ )
390
+ composer.add_app_delete_method_call(delete_call_params)
391
+ else:
392
+ delete_params = AppDeleteParams(
393
+ **{
394
+ **deployment.delete_params.__dict__,
395
+ "app_id": existing_app.app_id,
396
+ }
397
+ )
398
+ composer.add_app_delete(delete_params)
399
+ delete_txn_index = composer.count() - 1
400
+
401
+ result = composer.send()
402
+
403
+ create_result = SendAppCreateTransactionResult[ABIReturn].from_composer_result(result, create_txn_index)
404
+ delete_result = SendAppTransactionResult[ABIReturn].from_composer_result(result, delete_txn_index)
405
+
406
+ app_id = int(result.confirmations[0]["application-index"]) # type: ignore[call-overload]
407
+ app_metadata = ApplicationMetaData(
408
+ reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)),
409
+ deploy_metadata=deployment.metadata,
410
+ created_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload]
411
+ updated_round=result.confirmations[0]["confirmed-round"], # type: ignore[call-overload]
412
+ deleted=False,
413
+ )
414
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
415
+
416
+ return AppDeployResult(
417
+ app=app_metadata,
418
+ operation_performed=OperationPerformed.Replace,
419
+ create_result=create_result,
420
+ update_result=None,
421
+ delete_result=delete_result,
422
+ )
423
+
424
+ def _update_app(
425
+ self,
426
+ deployment: AppDeployParams,
427
+ existing_app: ApplicationMetaData,
428
+ approval_program: bytes,
429
+ clear_program: bytes,
430
+ ) -> AppDeployResult:
431
+ """Update an existing application"""
432
+
433
+ if isinstance(deployment.update_params, AppUpdateMethodCallParams):
434
+ result = self._transaction_sender.app_update_method_call(
435
+ AppUpdateMethodCallParams(
436
+ **{
437
+ **deployment.update_params.__dict__,
438
+ "app_id": existing_app.app_id,
439
+ "approval_program": approval_program,
440
+ "clear_state_program": clear_program,
441
+ }
442
+ ),
443
+ send_params=deployment.send_params,
444
+ )
445
+ else:
446
+ result = self._transaction_sender.app_update(
447
+ AppUpdateParams(
448
+ **{
449
+ **deployment.update_params.__dict__,
450
+ "app_id": existing_app.app_id,
451
+ "approval_program": approval_program,
452
+ "clear_state_program": clear_program,
453
+ }
454
+ ),
455
+ send_params=deployment.send_params,
456
+ )
457
+
458
+ app_metadata = ApplicationMetaData(
459
+ reference=ApplicationReference(app_id=existing_app.app_id, app_address=existing_app.app_address),
460
+ deploy_metadata=deployment.metadata,
461
+ created_round=existing_app.created_round,
462
+ updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0,
463
+ deleted=False,
464
+ )
465
+
466
+ self._update_app_lookup(deployment.create_params.sender, app_metadata)
467
+
468
+ return AppDeployResult(
469
+ app=app_metadata,
470
+ operation_performed=OperationPerformed.Update,
471
+ update_result=result,
472
+ )
473
+
474
+ def _handle_schema_break(
475
+ self,
476
+ deployment: AppDeployParams,
477
+ existing_app: ApplicationMetaData,
478
+ approval_program: bytes,
479
+ clear_program: bytes,
480
+ ) -> AppDeployResult:
481
+ if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail") or deployment.on_schema_break is None:
482
+ raise ValueError(
483
+ "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. "
484
+ "If you want to try deleting and recreating the app then "
485
+ "re-run with onSchemaBreak=OnSchemaBreak.ReplaceApp"
486
+ )
487
+
488
+ if deployment.on_schema_break in (OnSchemaBreak.AppendApp, "append"):
489
+ return self._create_app(deployment, approval_program, clear_program)
490
+
491
+ if existing_app.deletable:
492
+ return self._replace_app(deployment, existing_app, approval_program, clear_program)
493
+ else:
494
+ raise ValueError("App is not deletable but onSchemaBreak=ReplaceApp, " "cannot delete and recreate app")
495
+
496
+ def _handle_update(
497
+ self,
498
+ deployment: AppDeployParams,
499
+ existing_app: ApplicationMetaData,
500
+ approval_program: bytes,
501
+ clear_program: bytes,
502
+ ) -> AppDeployResult:
503
+ if deployment.on_update in (OnUpdate.Fail, "fail") or deployment.on_update is None:
504
+ raise ValueError(
505
+ "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail."
506
+ )
507
+
508
+ if deployment.on_update in (OnUpdate.AppendApp, "append"):
509
+ return self._create_app(deployment, approval_program, clear_program)
510
+
511
+ if deployment.on_update in (OnUpdate.UpdateApp, "update"):
512
+ if existing_app.updatable:
513
+ return self._update_app(deployment, existing_app, approval_program, clear_program)
514
+ else:
515
+ raise ValueError("App is not updatable but onUpdate=UpdateApp, cannot update app")
516
+
517
+ if deployment.on_update in (OnUpdate.ReplaceApp, "replace"):
518
+ if existing_app.deletable:
519
+ return self._replace_app(deployment, existing_app, approval_program, clear_program)
520
+ else:
521
+ raise ValueError("App is not deletable but onUpdate=ReplaceApp, " "cannot delete and recreate app")
522
+
523
+ raise ValueError(f"Unsupported onUpdate value: {deployment.on_update}")
524
+
525
+ def _update_app_lookup(self, sender: str, app_metadata: ApplicationMetaData) -> None:
526
+ """Update the app lookup cache"""
527
+
528
+ lookup = self._app_lookups.get(sender)
529
+ if not lookup:
530
+ self._app_lookups[sender] = ApplicationLookup(
531
+ creator=sender,
532
+ apps={app_metadata.name: app_metadata},
533
+ )
534
+ else:
535
+ lookup.apps[app_metadata.name] = app_metadata
536
+
537
+ def get_creator_apps_by_name(self, *, creator_address: str, ignore_cache: bool = False) -> ApplicationLookup:
538
+ """Get apps created by an account"""
539
+
540
+ if not ignore_cache and creator_address in self._app_lookups:
541
+ return self._app_lookups[creator_address]
542
+
543
+ if not self._indexer:
544
+ raise ValueError(
545
+ "Didn't receive an indexer client when this AppManager was created, "
546
+ "but received a call to get_creator_apps"
547
+ )
548
+
549
+ app_lookup: dict[str, ApplicationMetaData] = {}
550
+
551
+ # Get all apps created by account
552
+ created_apps = self._indexer.search_applications(creator=creator_address)
553
+
554
+ for app in created_apps["applications"]:
555
+ app_id = app["id"]
556
+
557
+ # Get creation transaction
558
+ creation_txns = self._indexer.search_transactions(
559
+ application_id=app_id,
560
+ min_round=app["created-at-round"],
561
+ address=creator_address,
562
+ address_role="sender",
563
+ note_prefix=APP_DEPLOY_NOTE_DAPP.encode(),
564
+ limit=1,
565
+ )
566
+
567
+ if not creation_txns["transactions"]:
568
+ continue
569
+
570
+ creation_txn = creation_txns["transactions"][0]
571
+
572
+ try:
573
+ note = base64.b64decode(creation_txn["note"]).decode()
574
+ if not note.startswith(f"{APP_DEPLOY_NOTE_DAPP}:j"):
575
+ continue
576
+
577
+ metadata = json.loads(note[len(APP_DEPLOY_NOTE_DAPP) + 2 :])
578
+
579
+ if metadata.get("name"):
580
+ app_lookup[metadata["name"]] = ApplicationMetaData(
581
+ reference=ApplicationReference(app_id=app_id, app_address=get_application_address(app_id)),
582
+ deploy_metadata=AppDeploymentMetaData(
583
+ name=metadata["name"],
584
+ version=metadata.get("version", "1.0"),
585
+ deletable=metadata.get("deletable"),
586
+ updatable=metadata.get("updatable"),
587
+ ),
588
+ created_round=creation_txn["confirmed-round"],
589
+ updated_round=creation_txn["confirmed-round"],
590
+ deleted=app.get("deleted", False),
591
+ )
592
+ except Exception as e:
593
+ logger.warning(
594
+ f"Error processing app {app_id} for creator {creator_address}: {e}",
595
+ )
596
+ continue
597
+
598
+ lookup = ApplicationLookup(creator=creator_address, apps=app_lookup)
599
+ self._app_lookups[creator_address] = lookup
600
+ return lookup