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