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