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
algokit_utils/deploy.py CHANGED
@@ -1,897 +1,10 @@
1
- import base64
2
- import dataclasses
3
- import json
4
- import logging
5
- import re
6
- from collections.abc import Iterable, Mapping, Sequence
7
- from enum import Enum
8
- from typing import TYPE_CHECKING, TypeAlias, TypedDict
1
+ import warnings
9
2
 
10
- from algosdk import transaction
11
- from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner
12
- from algosdk.logic import get_application_address
13
- from algosdk.transaction import StateSchema
14
-
15
- from algokit_utils.application_specification import (
16
- ApplicationSpecification,
17
- CallConfig,
18
- MethodConfigDict,
19
- OnCompleteActionName,
20
- )
21
- from algokit_utils.models import (
22
- ABIArgsDict,
23
- ABIMethod,
24
- Account,
25
- CreateCallParameters,
26
- TransactionResponse,
3
+ warnings.warn(
4
+ "The legacy v2 deploy module is deprecated and will be removed in a future version. "
5
+ "Refer to `AppFactory` and `AppDeployer` abstractions from `algokit_utils` module instead.",
6
+ DeprecationWarning,
7
+ stacklevel=2,
27
8
  )
28
9
 
29
- if TYPE_CHECKING:
30
- from algosdk.v2client.algod import AlgodClient
31
- from algosdk.v2client.indexer import IndexerClient
32
-
33
- from algokit_utils.application_client import ApplicationClient
34
-
35
-
36
- __all__ = [
37
- "UPDATABLE_TEMPLATE_NAME",
38
- "DELETABLE_TEMPLATE_NAME",
39
- "NOTE_PREFIX",
40
- "ABICallArgs",
41
- "ABICreateCallArgs",
42
- "ABICallArgsDict",
43
- "ABICreateCallArgsDict",
44
- "DeploymentFailedError",
45
- "AppReference",
46
- "AppDeployMetaData",
47
- "AppMetaData",
48
- "AppLookup",
49
- "DeployCallArgs",
50
- "DeployCreateCallArgs",
51
- "DeployCallArgsDict",
52
- "DeployCreateCallArgsDict",
53
- "Deployer",
54
- "DeployResponse",
55
- "OnUpdate",
56
- "OnSchemaBreak",
57
- "OperationPerformed",
58
- "TemplateValueDict",
59
- "TemplateValueMapping",
60
- "get_app_id_from_tx_id",
61
- "get_creator_apps",
62
- "replace_template_variables",
63
- ]
64
-
65
- logger = logging.getLogger(__name__)
66
-
67
- DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT = 1000
68
- _UPDATABLE = "UPDATABLE"
69
- _DELETABLE = "DELETABLE"
70
- UPDATABLE_TEMPLATE_NAME = f"TMPL_{_UPDATABLE}"
71
- """Template variable name used to control if a smart contract is updatable or not at deployment"""
72
- DELETABLE_TEMPLATE_NAME = f"TMPL_{_DELETABLE}"
73
- """Template variable name used to control if a smart contract is deletable or not at deployment"""
74
- _TOKEN_PATTERN = re.compile(r"TMPL_[A-Z_]+")
75
- TemplateValue: TypeAlias = int | str | bytes
76
- TemplateValueDict: TypeAlias = dict[str, TemplateValue]
77
- """Dictionary of `dict[str, int | str | bytes]` representing template variable names and values"""
78
- TemplateValueMapping: TypeAlias = Mapping[str, TemplateValue]
79
- """Mapping of `str` to `int | str | bytes` representing template variable names and values"""
80
-
81
- NOTE_PREFIX = "ALGOKIT_DEPLOYER:j"
82
- """ARC-0002 compliant note prefix for algokit_utils deployed applications"""
83
- # This prefix is also used to filter for parsable transaction notes in get_creator_apps.
84
- # However, as the note is base64 encoded first we need to consider it's base64 representation.
85
- # When base64 encoding bytes, 3 bytes are stored in every 4 characters.
86
- # So then we don't need to worry about the padding/changing characters of the prefix if it was followed by
87
- # additional characters, assert the NOTE_PREFIX length is a multiple of 3.
88
- assert len(NOTE_PREFIX) % 3 == 0
89
-
90
-
91
- class DeploymentFailedError(Exception):
92
- pass
93
-
94
-
95
- @dataclasses.dataclass
96
- class AppReference:
97
- """Information about an Algorand app"""
98
-
99
- app_id: int
100
- app_address: str
101
-
102
-
103
- @dataclasses.dataclass
104
- class AppDeployMetaData:
105
- """Metadata about an application stored in a transaction note during creation.
106
-
107
- The note is serialized as JSON and prefixed with {py:data}`NOTE_PREFIX` and stored in the transaction note field
108
- as part of {py:meth}`ApplicationClient.deploy`
109
- """
110
-
111
- name: str
112
- version: str
113
- deletable: bool | None
114
- updatable: bool | None
115
-
116
- @staticmethod
117
- def from_json(value: str) -> "AppDeployMetaData":
118
- json_value: dict = json.loads(value)
119
- json_value.setdefault("deletable", None)
120
- json_value.setdefault("updatable", None)
121
- return AppDeployMetaData(**json_value)
122
-
123
- @classmethod
124
- def from_b64(cls: type["AppDeployMetaData"], b64: str) -> "AppDeployMetaData":
125
- return cls.decode(base64.b64decode(b64))
126
-
127
- @classmethod
128
- def decode(cls: type["AppDeployMetaData"], value: bytes) -> "AppDeployMetaData":
129
- note = value.decode("utf-8")
130
- assert note.startswith(NOTE_PREFIX)
131
- return cls.from_json(note[len(NOTE_PREFIX) :])
132
-
133
- def encode(self) -> bytes:
134
- json_str = json.dumps(self.__dict__)
135
- return f"{NOTE_PREFIX}{json_str}".encode()
136
-
137
-
138
- @dataclasses.dataclass
139
- class AppMetaData(AppReference, AppDeployMetaData):
140
- """Metadata about a deployed app"""
141
-
142
- created_round: int
143
- updated_round: int
144
- created_metadata: AppDeployMetaData
145
- deleted: bool
146
-
147
-
148
- @dataclasses.dataclass
149
- class AppLookup:
150
- """Cache of {py:class}`AppMetaData` for a specific `creator`
151
-
152
- Can be used as an argument to {py:class}`ApplicationClient` to reduce the number of calls when deploying multiple
153
- apps or discovering multiple app_ids
154
- """
155
-
156
- creator: str
157
- apps: dict[str, AppMetaData] = dataclasses.field(default_factory=dict)
158
-
159
-
160
- def _sort_by_round(txn: dict) -> tuple[int, int]:
161
- confirmed = txn["confirmed-round"]
162
- offset = txn["intra-round-offset"]
163
- return confirmed, offset
164
-
165
-
166
- def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None:
167
- if not metadata_b64:
168
- return None
169
- # noinspection PyBroadException
170
- try:
171
- return AppDeployMetaData.from_b64(metadata_b64)
172
- except Exception:
173
- return None
174
-
175
-
176
- def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup:
177
- """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified
178
- creator that have a transaction note containing {py:class}`AppDeployMetaData`
179
- """
180
- apps: dict[str, AppMetaData] = {}
181
-
182
- creator_address = creator_account if isinstance(creator_account, str) else creator_account.address
183
- token = None
184
- # TODO: paginated indexer call instead of N + 1 calls
185
- while True:
186
- response = indexer.lookup_account_application_by_creator(
187
- creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token
188
- ) # type: ignore[no-untyped-call]
189
- if "message" in response: # an error occurred
190
- raise Exception(f"Error querying applications for {creator_address}: {response}")
191
- for app in response["applications"]:
192
- app_id = app["id"]
193
- app_created_at_round = app["created-at-round"]
194
- app_deleted = app.get("deleted", False)
195
- search_transactions_response = indexer.search_transactions(
196
- min_round=app_created_at_round,
197
- txn_type="appl",
198
- application_id=app_id,
199
- address=creator_address,
200
- address_role="sender",
201
- note_prefix=NOTE_PREFIX.encode("utf-8"),
202
- ) # type: ignore[no-untyped-call]
203
- transactions: list[dict] = search_transactions_response["transactions"]
204
- if not transactions:
205
- continue
206
-
207
- created_transaction = next(
208
- t
209
- for t in transactions
210
- if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address
211
- )
212
-
213
- transactions.sort(key=_sort_by_round, reverse=True)
214
- latest_transaction = transactions[0]
215
- app_updated_at_round = latest_transaction["confirmed-round"]
216
-
217
- create_metadata = _parse_note(created_transaction.get("note"))
218
- update_metadata = _parse_note(latest_transaction.get("note"))
219
-
220
- if create_metadata and create_metadata.name:
221
- apps[create_metadata.name] = AppMetaData(
222
- app_id=app_id,
223
- app_address=get_application_address(app_id),
224
- created_metadata=create_metadata,
225
- created_round=app_created_at_round,
226
- **(update_metadata or create_metadata).__dict__,
227
- updated_round=app_updated_at_round,
228
- deleted=app_deleted,
229
- )
230
-
231
- token = response.get("next-token")
232
- if not token:
233
- break
234
-
235
- return AppLookup(creator_address, apps)
236
-
237
-
238
- def _state_schema(schema: dict[str, int]) -> StateSchema:
239
- return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call]
240
-
241
-
242
- def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]:
243
- if to_schema.num_uints > from_schema.num_uints:
244
- yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}"
245
- if to_schema.num_byte_slices > from_schema.num_byte_slices:
246
- yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}"
247
-
248
-
249
- @dataclasses.dataclass(kw_only=True)
250
- class AppChanges:
251
- app_updated: bool
252
- schema_breaking_change: bool
253
- schema_change_description: str | None
254
-
255
-
256
- def check_for_app_changes( # noqa: PLR0913
257
- algod_client: "AlgodClient",
258
- *,
259
- new_approval: bytes,
260
- new_clear: bytes,
261
- new_global_schema: StateSchema,
262
- new_local_schema: StateSchema,
263
- app_id: int,
264
- ) -> AppChanges:
265
- application_info = algod_client.application_info(app_id)
266
- assert isinstance(application_info, dict)
267
- application_create_params = application_info["params"]
268
-
269
- current_approval = base64.b64decode(application_create_params["approval-program"])
270
- current_clear = base64.b64decode(application_create_params["clear-state-program"])
271
- current_global_schema = _state_schema(application_create_params["global-state-schema"])
272
- current_local_schema = _state_schema(application_create_params["local-state-schema"])
273
-
274
- app_updated = current_approval != new_approval or current_clear != new_clear
275
-
276
- schema_changes: list[str] = []
277
- schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema))
278
- schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema))
279
-
280
- return AppChanges(
281
- app_updated=app_updated,
282
- schema_breaking_change=bool(schema_changes),
283
- schema_change_description=", ".join(schema_changes),
284
- )
285
-
286
-
287
- def _is_valid_token_character(char: str) -> bool:
288
- return char.isalnum() or char == "_"
289
-
290
-
291
- def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]:
292
- result: list[str] = []
293
- match_count = 0
294
- token = f"TMPL_{template_variable}"
295
- token_idx_offset = len(value) - len(token)
296
- for line in program_lines:
297
- comment_idx = _find_unquoted_string(line, "//")
298
- if comment_idx is None:
299
- comment_idx = len(line)
300
- code = line[:comment_idx]
301
- comment = line[comment_idx:]
302
- trailing_idx = 0
303
- while True:
304
- token_idx = _find_template_token(code, token, trailing_idx)
305
- if token_idx is None:
306
- break
307
-
308
- trailing_idx = token_idx + len(token)
309
- prefix = code[:token_idx]
310
- suffix = code[trailing_idx:]
311
- code = f"{prefix}{value}{suffix}"
312
- match_count += 1
313
- trailing_idx += token_idx_offset
314
- result.append(code + comment)
315
- return result, match_count
316
-
317
-
318
- def add_deploy_template_variables(
319
- template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None
320
- ) -> None:
321
- if allow_update is not None:
322
- template_values[_UPDATABLE] = int(allow_update)
323
- if allow_delete is not None:
324
- template_values[_DELETABLE] = int(allow_delete)
325
-
326
-
327
- def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) -> int | None:
328
- """Find the first string within a line of TEAL. Only matches outside of quotes and base64 are returned.
329
- Returns None if not found"""
330
-
331
- if end < 0:
332
- end = len(line)
333
- idx = start
334
- in_quotes = in_base64 = False
335
- while idx < end:
336
- current_char = line[idx]
337
- match current_char:
338
- # enter base64
339
- case " " | "(" if not in_quotes and _last_token_base64(line, idx):
340
- in_base64 = True
341
- # exit base64
342
- case " " | ")" if not in_quotes and in_base64:
343
- in_base64 = False
344
- # escaped char
345
- case "\\" if in_quotes:
346
- # skip next character
347
- idx += 1
348
- # quote boundary
349
- case '"':
350
- in_quotes = not in_quotes
351
- # can test for match
352
- case _ if not in_quotes and not in_base64 and line.startswith(token, idx):
353
- # only match if not in quotes and string matches
354
- return idx
355
- idx += 1
356
- return None
357
-
358
-
359
- def _last_token_base64(line: str, idx: int) -> bool:
360
- try:
361
- *_, last = line[:idx].split()
362
- except ValueError:
363
- return False
364
- return last in ("base64", "b64")
365
-
366
-
367
- def _find_template_token(line: str, token: str, start: int = 0, end: int = -1) -> int | None:
368
- """Find the first template token within a line of TEAL. Only matches outside of quotes are returned.
369
- Only full token matches are returned, i.e. TMPL_STR will not match against TMPL_STRING
370
- Returns None if not found"""
371
- if end < 0:
372
- end = len(line)
373
-
374
- idx = start
375
- while idx < end:
376
- token_idx = _find_unquoted_string(line, token, idx, end)
377
- if token_idx is None:
378
- break
379
- trailing_idx = token_idx + len(token)
380
- if (token_idx == 0 or not _is_valid_token_character(line[token_idx - 1])) and ( # word boundary at start
381
- trailing_idx >= len(line) or not _is_valid_token_character(line[trailing_idx]) # word boundary at end
382
- ):
383
- return token_idx
384
- idx = trailing_idx
385
- return None
386
-
387
-
388
- def _strip_comment(line: str) -> str:
389
- comment_idx = _find_unquoted_string(line, "//")
390
- if comment_idx is None:
391
- return line
392
- return line[:comment_idx].rstrip()
393
-
394
-
395
- def strip_comments(program: str) -> str:
396
- return "\n".join(_strip_comment(line) for line in program.splitlines())
397
-
398
-
399
- def _has_token(program_without_comments: str, token: str) -> bool:
400
- for line in program_without_comments.splitlines():
401
- token_idx = _find_template_token(line, token)
402
- if token_idx is not None:
403
- return True
404
- return False
405
-
406
-
407
- def _find_tokens(stripped_approval_program: str) -> list[str]:
408
- return _TOKEN_PATTERN.findall(stripped_approval_program)
409
-
410
-
411
- def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None:
412
- approval_program = strip_comments(approval_program)
413
- if _has_token(approval_program, UPDATABLE_TEMPLATE_NAME) and _UPDATABLE not in template_values:
414
- raise DeploymentFailedError(
415
- "allow_update must be specified if deploy time configuration of update is being used"
416
- )
417
- if _has_token(approval_program, DELETABLE_TEMPLATE_NAME) and _DELETABLE not in template_values:
418
- raise DeploymentFailedError(
419
- "allow_delete must be specified if deploy time configuration of delete is being used"
420
- )
421
- all_tokens = _find_tokens(approval_program)
422
- missing_values = [token for token in all_tokens if token[len("TMPL_") :] not in template_values]
423
- if missing_values:
424
- raise DeploymentFailedError(f"The following template values were not provided: {', '.join(missing_values)}")
425
-
426
- for template_variable_name in template_values:
427
- tmpl_variable = f"TMPL_{template_variable_name}"
428
- if not _has_token(approval_program, tmpl_variable):
429
- if template_variable_name == _UPDATABLE:
430
- raise DeploymentFailedError(
431
- "allow_update must only be specified if deploy time configuration of update is being used"
432
- )
433
- if template_variable_name == _DELETABLE:
434
- raise DeploymentFailedError(
435
- "allow_delete must only be specified if deploy time configuration of delete is being used"
436
- )
437
- logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided")
438
-
439
-
440
- def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str:
441
- """Replaces `TMPL_*` variables in `program` with `template_values`
442
-
443
- ```{note}
444
- `template_values` keys should *NOT* be prefixed with `TMPL_`
445
- ```
446
- """
447
- program_lines = program.splitlines()
448
- for template_variable_name, template_value in template_values.items():
449
- match template_value:
450
- case int():
451
- value = str(template_value)
452
- case str():
453
- value = "0x" + template_value.encode("utf-8").hex()
454
- case bytes():
455
- value = "0x" + template_value.hex()
456
- case _:
457
- raise DeploymentFailedError(
458
- f"Unexpected template value type {template_variable_name}: {template_value.__class__}"
459
- )
460
-
461
- program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value)
462
-
463
- return "\n".join(program_lines)
464
-
465
-
466
- def has_template_vars(app_spec: ApplicationSpecification) -> bool:
467
- return "TMPL_" in strip_comments(app_spec.approval_program) or "TMPL_" in strip_comments(app_spec.clear_program)
468
-
469
-
470
- def get_deploy_control(
471
- app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete
472
- ) -> bool | None:
473
- if template_var not in strip_comments(app_spec.approval_program):
474
- return None
475
- return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any(
476
- h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER
477
- )
478
-
479
-
480
- def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig:
481
- def get(key: OnCompleteActionName) -> CallConfig:
482
- return method_config.get(key, CallConfig.NEVER)
483
-
484
- match on_complete:
485
- case transaction.OnComplete.NoOpOC:
486
- return get("no_op")
487
- case transaction.OnComplete.UpdateApplicationOC:
488
- return get("update_application")
489
- case transaction.OnComplete.DeleteApplicationOC:
490
- return get("delete_application")
491
- case transaction.OnComplete.OptInOC:
492
- return get("opt_in")
493
- case transaction.OnComplete.CloseOutOC:
494
- return get("close_out")
495
- case transaction.OnComplete.ClearStateOC:
496
- return get("clear_state")
497
-
498
-
499
- class OnUpdate(Enum):
500
- """Action to take if an Application has been updated"""
501
-
502
- Fail = 0
503
- """Fail the deployment"""
504
- UpdateApp = 1
505
- """Update the Application with the new approval and clear programs"""
506
- ReplaceApp = 2
507
- """Create a new Application and delete the old Application in a single transaction"""
508
- AppendApp = 3
509
- """Create a new application"""
510
-
511
-
512
- class OnSchemaBreak(Enum):
513
- """Action to take if an Application's schema has breaking changes"""
514
-
515
- Fail = 0
516
- """Fail the deployment"""
517
- ReplaceApp = 2
518
- """Create a new Application and delete the old Application in a single transaction"""
519
- AppendApp = 3
520
- """Create a new Application"""
521
-
522
-
523
- class OperationPerformed(Enum):
524
- """Describes the actions taken during deployment"""
525
-
526
- Nothing = 0
527
- """An existing Application was found"""
528
- Create = 1
529
- """No existing Application was found, created a new Application"""
530
- Update = 2
531
- """An existing Application was found, but was out of date, updated to latest version"""
532
- Replace = 3
533
- """An existing Application was found, but was out of date, created a new Application and deleted the original"""
534
-
535
-
536
- @dataclasses.dataclass(kw_only=True)
537
- class DeployResponse:
538
- """Describes the action taken during deployment, related transactions and the {py:class}`AppMetaData`"""
539
-
540
- app: AppMetaData
541
- create_response: TransactionResponse | None = None
542
- delete_response: TransactionResponse | None = None
543
- update_response: TransactionResponse | None = None
544
- action_taken: OperationPerformed = OperationPerformed.Nothing
545
-
546
-
547
- @dataclasses.dataclass(kw_only=True)
548
- class DeployCallArgs:
549
- """Parameters used to update or delete an application when calling
550
- {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
551
-
552
- suggested_params: transaction.SuggestedParams | None = None
553
- lease: bytes | str | None = None
554
- accounts: list[str] | None = None
555
- foreign_apps: list[int] | None = None
556
- foreign_assets: list[int] | None = None
557
- boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None
558
- rekey_to: str | None = None
559
-
560
-
561
- @dataclasses.dataclass(kw_only=True)
562
- class ABICall:
563
- method: ABIMethod | bool | None = None
564
- args: ABIArgsDict = dataclasses.field(default_factory=dict)
565
-
566
-
567
- @dataclasses.dataclass(kw_only=True)
568
- class DeployCreateCallArgs(DeployCallArgs):
569
- """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
570
-
571
- extra_pages: int | None = None
572
- on_complete: transaction.OnComplete | None = None
573
-
574
-
575
- @dataclasses.dataclass(kw_only=True)
576
- class ABICallArgs(DeployCallArgs, ABICall):
577
- """ABI Parameters used to update or delete an application when calling
578
- {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
579
-
580
-
581
- @dataclasses.dataclass(kw_only=True)
582
- class ABICreateCallArgs(DeployCreateCallArgs, ABICall):
583
- """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
584
-
585
-
586
- class DeployCallArgsDict(TypedDict, total=False):
587
- """Parameters used to update or delete an application when calling
588
- {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
589
-
590
- suggested_params: transaction.SuggestedParams
591
- lease: bytes | str
592
- accounts: list[str]
593
- foreign_apps: list[int]
594
- foreign_assets: list[int]
595
- boxes: Sequence[tuple[int, bytes | bytearray | str | int]]
596
- rekey_to: str
597
-
598
-
599
- class ABICallArgsDict(DeployCallArgsDict, TypedDict, total=False):
600
- """ABI Parameters used to update or delete an application when calling
601
- {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
602
-
603
- method: ABIMethod | bool
604
- args: ABIArgsDict
605
-
606
-
607
- class DeployCreateCallArgsDict(DeployCallArgsDict, TypedDict, total=False):
608
- """Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
609
-
610
- extra_pages: int | None
611
- on_complete: transaction.OnComplete
612
-
613
-
614
- class ABICreateCallArgsDict(DeployCreateCallArgsDict, TypedDict, total=False):
615
- """ABI Parameters used to create an application when calling {py:meth}`~algokit_utils.ApplicationClient.deploy`"""
616
-
617
- method: ABIMethod | bool
618
- args: ABIArgsDict
619
-
620
-
621
- @dataclasses.dataclass(kw_only=True)
622
- class Deployer:
623
- app_client: "ApplicationClient"
624
- creator: str
625
- signer: TransactionSigner
626
- sender: str
627
- existing_app_metadata_or_reference: AppReference | AppMetaData
628
- new_app_metadata: AppDeployMetaData
629
- on_update: OnUpdate
630
- on_schema_break: OnSchemaBreak
631
- create_args: ABICreateCallArgs | ABICreateCallArgsDict | DeployCreateCallArgs | None
632
- update_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None
633
- delete_args: ABICallArgs | ABICallArgsDict | DeployCallArgs | None
634
-
635
- def deploy(self) -> DeployResponse:
636
- """Ensures app associated with app client's creator is present and up to date"""
637
- assert self.app_client.approval
638
- assert self.app_client.clear
639
-
640
- if self.existing_app_metadata_or_reference.app_id == 0:
641
- logger.info(f"{self.new_app_metadata.name} not found in {self.creator} account, deploying app.")
642
- return self._create_app()
643
-
644
- assert isinstance(self.existing_app_metadata_or_reference, AppMetaData)
645
- logger.debug(
646
- f"{self.existing_app_metadata_or_reference.name} found in {self.creator} account, "
647
- f"with app id {self.existing_app_metadata_or_reference.app_id}, "
648
- f"version={self.existing_app_metadata_or_reference.version}."
649
- )
650
-
651
- app_changes = check_for_app_changes(
652
- self.app_client.algod_client,
653
- new_approval=self.app_client.approval.raw_binary,
654
- new_clear=self.app_client.clear.raw_binary,
655
- new_global_schema=self.app_client.app_spec.global_state_schema,
656
- new_local_schema=self.app_client.app_spec.local_state_schema,
657
- app_id=self.existing_app_metadata_or_reference.app_id,
658
- )
659
-
660
- if app_changes.schema_breaking_change:
661
- logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}")
662
- return self._deploy_breaking_change()
663
-
664
- if app_changes.app_updated:
665
- logger.info(f"Detected a TEAL update in app id {self.existing_app_metadata_or_reference.app_id}")
666
- return self._deploy_update()
667
-
668
- logger.info("No detected changes in app, nothing to do.")
669
- return DeployResponse(app=self.existing_app_metadata_or_reference)
670
-
671
- def _deploy_breaking_change(self) -> DeployResponse:
672
- assert isinstance(self.existing_app_metadata_or_reference, AppMetaData)
673
- if self.on_schema_break == OnSchemaBreak.Fail:
674
- raise DeploymentFailedError(
675
- "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. "
676
- "If you want to try deleting and recreating the app then "
677
- "re-run with on_schema_break=OnSchemaBreak.ReplaceApp"
678
- )
679
- if self.on_schema_break == OnSchemaBreak.AppendApp:
680
- logger.info("Schema break detected and on_schema_break=AppendApp, will attempt to create new app")
681
- return self._create_app()
682
-
683
- if self.existing_app_metadata_or_reference.deletable:
684
- logger.info(
685
- "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app"
686
- )
687
- elif self.existing_app_metadata_or_reference.deletable is False:
688
- logger.warning(
689
- "App is not deletable but on_schema_break=ReplaceApp, "
690
- "will attempt to delete app, delete will most likely fail"
691
- )
692
- else:
693
- logger.warning(
694
- "Cannot determine if App is deletable but on_schema_break=ReplaceApp, will attempt to delete app"
695
- )
696
- return self._create_and_delete_app()
697
-
698
- def _deploy_update(self) -> DeployResponse:
699
- assert isinstance(self.existing_app_metadata_or_reference, AppMetaData)
700
- if self.on_update == OnUpdate.Fail:
701
- raise DeploymentFailedError(
702
- "Update detected and on_update=Fail, stopping deployment. "
703
- "If you want to try updating the app then re-run with on_update=UpdateApp"
704
- )
705
- if self.on_update == OnUpdate.AppendApp:
706
- logger.info("Update detected and on_update=AppendApp, will attempt to create new app")
707
- return self._create_app()
708
- elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.UpdateApp:
709
- logger.info("App is updatable and on_update=UpdateApp, will update app")
710
- return self._update_app()
711
- elif self.existing_app_metadata_or_reference.updatable and self.on_update == OnUpdate.ReplaceApp:
712
- logger.warning(
713
- "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app"
714
- )
715
- return self._create_and_delete_app()
716
- elif self.on_update == OnUpdate.ReplaceApp:
717
- if self.existing_app_metadata_or_reference.updatable is False:
718
- logger.warning(
719
- "App is not updatable and on_update=ReplaceApp, "
720
- "will attempt to create new app and delete old app"
721
- )
722
- else:
723
- logger.warning(
724
- "Cannot determine if App is updatable and on_update=ReplaceApp, "
725
- "will attempt to create new app and delete old app"
726
- )
727
- return self._create_and_delete_app()
728
- else:
729
- if self.existing_app_metadata_or_reference.updatable is False:
730
- logger.warning(
731
- "App is not updatable but on_update=UpdateApp, "
732
- "will attempt to update app, update will most likely fail"
733
- )
734
- else:
735
- logger.warning(
736
- "Cannot determine if App is updatable and on_update=UpdateApp, will attempt to update app"
737
- )
738
- return self._update_app()
739
-
740
- def _create_app(self) -> DeployResponse:
741
- assert self.app_client.existing_deployments
742
-
743
- method, abi_args, parameters = _convert_deploy_args(
744
- self.create_args, self.new_app_metadata, self.signer, self.sender
745
- )
746
- create_response = self.app_client.create(
747
- method,
748
- parameters,
749
- **abi_args,
750
- )
751
- logger.info(
752
- f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, "
753
- f"with app id {self.app_client.app_id}."
754
- )
755
- assert create_response.confirmed_round is not None
756
- app_metadata = _create_metadata(self.new_app_metadata, self.app_client.app_id, create_response.confirmed_round)
757
- self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata
758
- return DeployResponse(app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create)
759
-
760
- def _create_and_delete_app(self) -> DeployResponse:
761
- assert self.app_client.existing_deployments
762
- assert isinstance(self.existing_app_metadata_or_reference, AppMetaData)
763
-
764
- logger.info(
765
- f"Replacing {self.existing_app_metadata_or_reference.name} "
766
- f"({self.existing_app_metadata_or_reference.version}) with "
767
- f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) in {self.creator} account."
768
- )
769
- atc = AtomicTransactionComposer()
770
- create_method, create_abi_args, create_parameters = _convert_deploy_args(
771
- self.create_args, self.new_app_metadata, self.signer, self.sender
772
- )
773
- self.app_client.compose_create(
774
- atc,
775
- create_method,
776
- create_parameters,
777
- **create_abi_args,
778
- )
779
- create_txn_index = len(atc.txn_list) - 1
780
- delete_method, delete_abi_args, delete_parameters = _convert_deploy_args(
781
- self.delete_args, self.new_app_metadata, self.signer, self.sender
782
- )
783
- self.app_client.compose_delete(
784
- atc,
785
- delete_method,
786
- delete_parameters,
787
- **delete_abi_args,
788
- )
789
- delete_txn_index = len(atc.txn_list) - 1
790
- create_delete_response = self.app_client.execute_atc(atc)
791
- create_response = TransactionResponse.from_atr(create_delete_response, create_txn_index)
792
- delete_response = TransactionResponse.from_atr(create_delete_response, delete_txn_index)
793
- self.app_client.app_id = get_app_id_from_tx_id(self.app_client.algod_client, create_response.tx_id)
794
- logger.info(
795
- f"{self.new_app_metadata.name} ({self.new_app_metadata.version}) deployed successfully, "
796
- f"with app id {self.app_client.app_id}."
797
- )
798
- logger.info(
799
- f"{self.existing_app_metadata_or_reference.name} "
800
- f"({self.existing_app_metadata_or_reference.version}) with app id "
801
- f"{self.existing_app_metadata_or_reference.app_id}, deleted successfully."
802
- )
803
-
804
- app_metadata = _create_metadata(
805
- self.new_app_metadata, self.app_client.app_id, create_delete_response.confirmed_round
806
- )
807
- self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata
808
-
809
- return DeployResponse(
810
- app=app_metadata,
811
- create_response=create_response,
812
- delete_response=delete_response,
813
- action_taken=OperationPerformed.Replace,
814
- )
815
-
816
- def _update_app(self) -> DeployResponse:
817
- assert self.app_client.existing_deployments
818
- assert isinstance(self.existing_app_metadata_or_reference, AppMetaData)
819
-
820
- logger.info(
821
- f"Updating {self.existing_app_metadata_or_reference.name} to {self.new_app_metadata.version} in "
822
- f"{self.creator} account, with app id {self.existing_app_metadata_or_reference.app_id}"
823
- )
824
- method, abi_args, parameters = _convert_deploy_args(
825
- self.update_args, self.new_app_metadata, self.signer, self.sender
826
- )
827
- update_response = self.app_client.update(
828
- method,
829
- parameters,
830
- **abi_args,
831
- )
832
- app_metadata = _create_metadata(
833
- self.new_app_metadata,
834
- self.app_client.app_id,
835
- self.existing_app_metadata_or_reference.created_round,
836
- updated_round=update_response.confirmed_round,
837
- original_metadata=self.existing_app_metadata_or_reference.created_metadata,
838
- )
839
- self.app_client.existing_deployments.apps[self.new_app_metadata.name] = app_metadata
840
- return DeployResponse(app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update)
841
-
842
-
843
- def _create_metadata(
844
- app_spec_note: AppDeployMetaData,
845
- app_id: int,
846
- created_round: int,
847
- updated_round: int | None = None,
848
- original_metadata: AppDeployMetaData | None = None,
849
- ) -> AppMetaData:
850
- return AppMetaData(
851
- app_id=app_id,
852
- app_address=get_application_address(app_id),
853
- created_metadata=original_metadata or app_spec_note,
854
- created_round=created_round,
855
- updated_round=updated_round or created_round,
856
- name=app_spec_note.name,
857
- version=app_spec_note.version,
858
- deletable=app_spec_note.deletable,
859
- updatable=app_spec_note.updatable,
860
- deleted=False,
861
- )
862
-
863
-
864
- def _convert_deploy_args(
865
- _args: DeployCallArgs | DeployCallArgsDict | None,
866
- note: AppDeployMetaData,
867
- signer: TransactionSigner | None,
868
- sender: str | None,
869
- ) -> tuple[ABIMethod | bool | None, ABIArgsDict, CreateCallParameters]:
870
- args = _args.__dict__ if isinstance(_args, DeployCallArgs) else dict(_args or {})
871
-
872
- # return most derived type, unused parameters are ignored
873
- parameters = CreateCallParameters(
874
- note=note.encode(),
875
- signer=signer,
876
- sender=sender,
877
- suggested_params=args.get("suggested_params"),
878
- lease=args.get("lease"),
879
- accounts=args.get("accounts"),
880
- foreign_assets=args.get("foreign_assets"),
881
- foreign_apps=args.get("foreign_apps"),
882
- boxes=args.get("boxes"),
883
- rekey_to=args.get("rekey_to"),
884
- extra_pages=args.get("extra_pages"),
885
- on_complete=args.get("on_complete"),
886
- )
887
-
888
- return args.get("method"), args.get("args") or {}, parameters
889
-
890
-
891
- def get_app_id_from_tx_id(algod_client: "AlgodClient", tx_id: str) -> int:
892
- """Finds the app_id for provided transaction id"""
893
- result = algod_client.pending_transaction_info(tx_id)
894
- assert isinstance(result, dict)
895
- app_id = result["application-index"]
896
- assert isinstance(app_id, int)
897
- return app_id
10
+ from algokit_utils._legacy_v2.deploy import * # noqa: F403, E402