algokit-utils 3.0.0b1__py3-none-any.whl → 3.0.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of algokit-utils might be problematic. Click here for more details.
- algokit_utils/__init__.py +23 -183
- algokit_utils/_debugging.py +123 -97
- algokit_utils/_legacy_v2/__init__.py +177 -0
- algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +19 -18
- algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +24 -23
- algokit_utils/_legacy_v2/account.py +203 -0
- algokit_utils/_legacy_v2/application_client.py +1471 -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} +19 -142
- algokit_utils/_legacy_v2/network_clients.py +140 -0
- algokit_utils/account.py +12 -183
- algokit_utils/accounts/__init__.py +2 -0
- algokit_utils/accounts/account_manager.py +909 -0
- algokit_utils/accounts/kmd_account_manager.py +159 -0
- algokit_utils/algorand.py +265 -0
- algokit_utils/application_client.py +9 -1453
- algokit_utils/application_specification.py +39 -197
- algokit_utils/applications/__init__.py +7 -0
- algokit_utils/applications/abi.py +276 -0
- algokit_utils/applications/app_client.py +2056 -0
- algokit_utils/applications/app_deployer.py +600 -0
- algokit_utils/applications/app_factory.py +826 -0
- algokit_utils/applications/app_manager.py +470 -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 +1023 -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 +320 -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 +656 -0
- algokit_utils/clients/dispenser_api_client.py +192 -0
- algokit_utils/common.py +8 -26
- algokit_utils/config.py +71 -18
- algokit_utils/deploy.py +7 -892
- 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 -80
- algokit_utils/models/__init__.py +8 -0
- algokit_utils/models/account.py +193 -0
- algokit_utils/models/amount.py +198 -0
- algokit_utils/models/application.py +61 -0
- algokit_utils/models/network.py +25 -0
- algokit_utils/models/simulate.py +11 -0
- algokit_utils/models/state.py +59 -0
- algokit_utils/models/transaction.py +100 -0
- algokit_utils/network_clients.py +7 -152
- 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 +2293 -0
- algokit_utils/transactions/transaction_creator.py +156 -0
- algokit_utils/transactions/transaction_sender.py +574 -0
- {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/METADATA +12 -7
- algokit_utils-3.0.0b2.dist-info/RECORD +70 -0
- {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/WHEEL +1 -1
- algokit_utils-3.0.0b1.dist-info/RECORD +0 -24
- {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,2293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypedDict, Union, cast
|
|
10
|
+
|
|
11
|
+
import algosdk
|
|
12
|
+
import algosdk.atomic_transaction_composer
|
|
13
|
+
import algosdk.v2client.models
|
|
14
|
+
from algosdk import logic, transaction
|
|
15
|
+
from algosdk.atomic_transaction_composer import (
|
|
16
|
+
AtomicTransactionComposer,
|
|
17
|
+
SimulateAtomicTransactionResponse,
|
|
18
|
+
TransactionSigner,
|
|
19
|
+
TransactionWithSigner,
|
|
20
|
+
)
|
|
21
|
+
from algosdk.transaction import ApplicationCallTxn, OnComplete, SuggestedParams
|
|
22
|
+
from algosdk.v2client.algod import AlgodClient
|
|
23
|
+
from algosdk.v2client.models.simulate_request import SimulateRequest
|
|
24
|
+
from typing_extensions import deprecated
|
|
25
|
+
|
|
26
|
+
from algokit_utils.applications.abi import ABIReturn, ABIValue
|
|
27
|
+
from algokit_utils.applications.app_manager import AppManager
|
|
28
|
+
from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method
|
|
29
|
+
from algokit_utils.config import config
|
|
30
|
+
from algokit_utils.models.state import BoxIdentifier, BoxReference
|
|
31
|
+
from algokit_utils.models.transaction import SendParams, TransactionWrapper
|
|
32
|
+
from algokit_utils.protocols.account import TransactionSignerAccountProtocol
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
|
|
37
|
+
from algosdk.abi import Method
|
|
38
|
+
from algosdk.v2client.algod import AlgodClient
|
|
39
|
+
from algosdk.v2client.models import SimulateTraceConfig
|
|
40
|
+
|
|
41
|
+
from algokit_utils.models.amount import AlgoAmount
|
|
42
|
+
from algokit_utils.models.transaction import Arc2TransactionNote
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"AppCallMethodCallParams",
|
|
47
|
+
"AppCallParams",
|
|
48
|
+
"AppCreateMethodCallParams",
|
|
49
|
+
"AppCreateParams",
|
|
50
|
+
"AppCreateSchema",
|
|
51
|
+
"AppDeleteMethodCallParams",
|
|
52
|
+
"AppDeleteParams",
|
|
53
|
+
"AppMethodCallTransactionArgument",
|
|
54
|
+
"AppUpdateMethodCallParams",
|
|
55
|
+
"AppUpdateParams",
|
|
56
|
+
"AssetConfigParams",
|
|
57
|
+
"AssetCreateParams",
|
|
58
|
+
"AssetDestroyParams",
|
|
59
|
+
"AssetFreezeParams",
|
|
60
|
+
"AssetOptInParams",
|
|
61
|
+
"AssetOptOutParams",
|
|
62
|
+
"AssetTransferParams",
|
|
63
|
+
"BuiltTransactions",
|
|
64
|
+
"MethodCallParams",
|
|
65
|
+
"OfflineKeyRegistrationParams",
|
|
66
|
+
"OnlineKeyRegistrationParams",
|
|
67
|
+
"PaymentParams",
|
|
68
|
+
"SendAtomicTransactionComposerResults",
|
|
69
|
+
"TransactionComposer",
|
|
70
|
+
"TransactionComposerBuildResult",
|
|
71
|
+
"TxnParams",
|
|
72
|
+
"send_atomic_transaction_composer",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
logger = config.logger
|
|
77
|
+
|
|
78
|
+
MAX_TRANSACTION_GROUP_SIZE = 16
|
|
79
|
+
MAX_APP_CALL_FOREIGN_REFERENCES = 8
|
|
80
|
+
MAX_APP_CALL_ACCOUNT_REFERENCES = 4
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(kw_only=True, frozen=True)
|
|
84
|
+
class _CommonTxnParams:
|
|
85
|
+
sender: str
|
|
86
|
+
signer: TransactionSigner | TransactionSignerAccountProtocol | None = None
|
|
87
|
+
rekey_to: str | None = None
|
|
88
|
+
note: bytes | None = None
|
|
89
|
+
lease: bytes | None = None
|
|
90
|
+
static_fee: AlgoAmount | None = None
|
|
91
|
+
extra_fee: AlgoAmount | None = None
|
|
92
|
+
max_fee: AlgoAmount | None = None
|
|
93
|
+
validity_window: int | None = None
|
|
94
|
+
first_valid_round: int | None = None
|
|
95
|
+
last_valid_round: int | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(kw_only=True, frozen=True)
|
|
99
|
+
class AdditionalAtcContext:
|
|
100
|
+
max_fees: dict[int, AlgoAmount] | None = None
|
|
101
|
+
suggested_params: SuggestedParams | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(kw_only=True, frozen=True)
|
|
105
|
+
class PaymentParams(_CommonTxnParams):
|
|
106
|
+
"""Parameters for a payment transaction.
|
|
107
|
+
|
|
108
|
+
:ivar receiver: The account that will receive the ALGO
|
|
109
|
+
:ivar amount: Amount to send
|
|
110
|
+
:ivar close_remainder_to: If given, close the sender account and send the remaining balance to this address,
|
|
111
|
+
defaults to None
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
receiver: str
|
|
115
|
+
amount: AlgoAmount
|
|
116
|
+
close_remainder_to: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(kw_only=True, frozen=True)
|
|
120
|
+
class AssetCreateParams(_CommonTxnParams):
|
|
121
|
+
"""Parameters for creating a new asset.
|
|
122
|
+
|
|
123
|
+
:ivar total: The total amount of the smallest divisible unit to create
|
|
124
|
+
:ivar decimals: The amount of decimal places the asset should have, defaults to None
|
|
125
|
+
:ivar default_frozen: Whether the asset is frozen by default in the creator address, defaults to None
|
|
126
|
+
:ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None
|
|
127
|
+
:ivar reserve: The address that holds the uncirculated supply, defaults to None
|
|
128
|
+
:ivar freeze: The address that can freeze the asset in any account, defaults to None
|
|
129
|
+
:ivar clawback: The address that can clawback the asset from any account, defaults to None
|
|
130
|
+
:ivar unit_name: The short ticker name for the asset, defaults to None
|
|
131
|
+
:ivar asset_name: The full name of the asset, defaults to None
|
|
132
|
+
:ivar url: The metadata URL for the asset, defaults to None
|
|
133
|
+
:ivar metadata_hash: Hash of the metadata contained in the metadata URL, defaults to None
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
total: int
|
|
137
|
+
asset_name: str | None = None
|
|
138
|
+
unit_name: str | None = None
|
|
139
|
+
url: str | None = None
|
|
140
|
+
decimals: int | None = None
|
|
141
|
+
default_frozen: bool | None = None
|
|
142
|
+
manager: str | None = None
|
|
143
|
+
reserve: str | None = None
|
|
144
|
+
freeze: str | None = None
|
|
145
|
+
clawback: str | None = None
|
|
146
|
+
metadata_hash: bytes | None = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(kw_only=True, frozen=True)
|
|
150
|
+
class AssetConfigParams(_CommonTxnParams):
|
|
151
|
+
"""Parameters for configuring an existing asset.
|
|
152
|
+
|
|
153
|
+
:ivar asset_id: ID of the asset
|
|
154
|
+
:ivar manager: The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None
|
|
155
|
+
:ivar reserve: The address that holds the uncirculated supply, defaults to None
|
|
156
|
+
:ivar freeze: The address that can freeze the asset in any account, defaults to None
|
|
157
|
+
:ivar clawback: The address that can clawback the asset from any account, defaults to None
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
asset_id: int
|
|
161
|
+
manager: str | None = None
|
|
162
|
+
reserve: str | None = None
|
|
163
|
+
freeze: str | None = None
|
|
164
|
+
clawback: str | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(kw_only=True, frozen=True)
|
|
168
|
+
class AssetFreezeParams(_CommonTxnParams):
|
|
169
|
+
"""Parameters for freezing an asset.
|
|
170
|
+
|
|
171
|
+
:ivar asset_id: The ID of the asset
|
|
172
|
+
:ivar account: The account to freeze or unfreeze
|
|
173
|
+
:ivar frozen: Whether the assets in the account should be frozen
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
asset_id: int
|
|
177
|
+
account: str
|
|
178
|
+
frozen: bool
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(kw_only=True, frozen=True)
|
|
182
|
+
class AssetDestroyParams(_CommonTxnParams):
|
|
183
|
+
"""Parameters for destroying an asset.
|
|
184
|
+
|
|
185
|
+
:ivar asset_id: ID of the asset
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
asset_id: int
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(kw_only=True, frozen=True)
|
|
192
|
+
class OnlineKeyRegistrationParams(_CommonTxnParams):
|
|
193
|
+
"""Parameters for online key registration.
|
|
194
|
+
|
|
195
|
+
:ivar vote_key: The root participation public key
|
|
196
|
+
:ivar selection_key: The VRF public key
|
|
197
|
+
:ivar vote_first: The first round that the participation key is valid
|
|
198
|
+
:ivar vote_last: The last round that the participation key is valid
|
|
199
|
+
:ivar vote_key_dilution: The dilution for the 2-level participation key
|
|
200
|
+
:ivar state_proof_key: The 64 byte state proof public key commitment, defaults to None
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
vote_key: str
|
|
204
|
+
selection_key: str
|
|
205
|
+
vote_first: int
|
|
206
|
+
vote_last: int
|
|
207
|
+
vote_key_dilution: int
|
|
208
|
+
state_proof_key: bytes | None = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(kw_only=True, frozen=True)
|
|
212
|
+
class OfflineKeyRegistrationParams(_CommonTxnParams):
|
|
213
|
+
"""Parameters for offline key registration.
|
|
214
|
+
|
|
215
|
+
:ivar prevent_account_from_ever_participating_again: Whether to prevent the account from ever participating again
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
prevent_account_from_ever_participating_again: bool
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(kw_only=True, frozen=True)
|
|
222
|
+
class AssetTransferParams(_CommonTxnParams):
|
|
223
|
+
"""Parameters for transferring an asset.
|
|
224
|
+
|
|
225
|
+
:ivar asset_id: ID of the asset
|
|
226
|
+
:ivar amount: Amount of the asset to transfer (smallest divisible unit)
|
|
227
|
+
:ivar receiver: The account to send the asset to
|
|
228
|
+
:ivar clawback_target: The account to take the asset from, defaults to None
|
|
229
|
+
:ivar close_asset_to: The account to close the asset to, defaults to None
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
asset_id: int
|
|
233
|
+
amount: int
|
|
234
|
+
receiver: str
|
|
235
|
+
clawback_target: str | None = None
|
|
236
|
+
close_asset_to: str | None = None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass(kw_only=True, frozen=True)
|
|
240
|
+
class AssetOptInParams(_CommonTxnParams):
|
|
241
|
+
"""Parameters for opting into an asset.
|
|
242
|
+
|
|
243
|
+
:ivar asset_id: ID of the asset
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
asset_id: int
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(kw_only=True, frozen=True)
|
|
250
|
+
class AssetOptOutParams(_CommonTxnParams):
|
|
251
|
+
"""Parameters for opting out of an asset.
|
|
252
|
+
|
|
253
|
+
:ivar asset_id: ID of the asset
|
|
254
|
+
:ivar creator: The creator address of the asset
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
asset_id: int
|
|
258
|
+
creator: str
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(kw_only=True, frozen=True)
|
|
262
|
+
class AppCallParams(_CommonTxnParams):
|
|
263
|
+
"""Parameters for calling an application.
|
|
264
|
+
|
|
265
|
+
:ivar on_complete: The OnComplete action
|
|
266
|
+
:ivar app_id: ID of the application, defaults to None
|
|
267
|
+
:ivar approval_program: The program to execute for all OnCompletes other than ClearState, defaults to None
|
|
268
|
+
:ivar clear_state_program: The program to execute for ClearState OnComplete, defaults to None
|
|
269
|
+
:ivar schema: The state schema for the app. This is immutable, defaults to None
|
|
270
|
+
:ivar args: Application arguments, defaults to None
|
|
271
|
+
:ivar account_references: Account references, defaults to None
|
|
272
|
+
:ivar app_references: App references, defaults to None
|
|
273
|
+
:ivar asset_references: Asset references, defaults to None
|
|
274
|
+
:ivar extra_pages: Number of extra pages required for the programs, defaults to None
|
|
275
|
+
:ivar box_references: Box references, defaults to None
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
on_complete: OnComplete
|
|
279
|
+
app_id: int | None = None
|
|
280
|
+
approval_program: str | bytes | None = None
|
|
281
|
+
clear_state_program: str | bytes | None = None
|
|
282
|
+
schema: dict[str, int] | None = None
|
|
283
|
+
args: list[bytes] | None = None
|
|
284
|
+
account_references: list[str] | None = None
|
|
285
|
+
app_references: list[int] | None = None
|
|
286
|
+
asset_references: list[int] | None = None
|
|
287
|
+
extra_pages: int | None = None
|
|
288
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class AppCreateSchema(TypedDict):
|
|
292
|
+
global_ints: int
|
|
293
|
+
global_byte_slices: int
|
|
294
|
+
local_ints: int
|
|
295
|
+
local_byte_slices: int
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(kw_only=True, frozen=True)
|
|
299
|
+
class AppCreateParams(_CommonTxnParams):
|
|
300
|
+
"""Parameters for creating an application.
|
|
301
|
+
|
|
302
|
+
:ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string)
|
|
303
|
+
or compiled teal (bytes)
|
|
304
|
+
:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string)
|
|
305
|
+
or compiled teal (bytes)
|
|
306
|
+
:ivar schema: The state schema for the app. This is immutable, defaults to None
|
|
307
|
+
:ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None
|
|
308
|
+
:ivar args: Application arguments, defaults to None
|
|
309
|
+
:ivar account_references: Account references, defaults to None
|
|
310
|
+
:ivar app_references: App references, defaults to None
|
|
311
|
+
:ivar asset_references: Asset references, defaults to None
|
|
312
|
+
:ivar box_references: Box references, defaults to None
|
|
313
|
+
:ivar extra_program_pages: Number of extra pages required for the programs, defaults to None
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
approval_program: str | bytes
|
|
317
|
+
clear_state_program: str | bytes
|
|
318
|
+
schema: AppCreateSchema | None = None
|
|
319
|
+
on_complete: OnComplete | None = None
|
|
320
|
+
args: list[bytes] | None = None
|
|
321
|
+
account_references: list[str] | None = None
|
|
322
|
+
app_references: list[int] | None = None
|
|
323
|
+
asset_references: list[int] | None = None
|
|
324
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
325
|
+
extra_program_pages: int | None = None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass(kw_only=True, frozen=True)
|
|
329
|
+
class AppUpdateParams(_CommonTxnParams):
|
|
330
|
+
"""Parameters for updating an application.
|
|
331
|
+
|
|
332
|
+
:ivar app_id: ID of the application
|
|
333
|
+
:ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string)
|
|
334
|
+
or compiled teal (bytes)
|
|
335
|
+
:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string)
|
|
336
|
+
or compiled teal (bytes)
|
|
337
|
+
:ivar args: Application arguments, defaults to None
|
|
338
|
+
:ivar account_references: Account references, defaults to None
|
|
339
|
+
:ivar app_references: App references, defaults to None
|
|
340
|
+
:ivar asset_references: Asset references, defaults to None
|
|
341
|
+
:ivar box_references: Box references, defaults to None
|
|
342
|
+
:ivar on_complete: The OnComplete action, defaults to None
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
app_id: int
|
|
346
|
+
approval_program: str | bytes
|
|
347
|
+
clear_state_program: str | bytes
|
|
348
|
+
args: list[bytes] | None = None
|
|
349
|
+
account_references: list[str] | None = None
|
|
350
|
+
app_references: list[int] | None = None
|
|
351
|
+
asset_references: list[int] | None = None
|
|
352
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
353
|
+
on_complete: OnComplete | None = None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(kw_only=True, frozen=True)
|
|
357
|
+
class AppDeleteParams(_CommonTxnParams):
|
|
358
|
+
"""Parameters for deleting an application.
|
|
359
|
+
|
|
360
|
+
:ivar app_id: ID of the application
|
|
361
|
+
:ivar args: Application arguments, defaults to None
|
|
362
|
+
:ivar account_references: Account references, defaults to None
|
|
363
|
+
:ivar app_references: App references, defaults to None
|
|
364
|
+
:ivar asset_references: Asset references, defaults to None
|
|
365
|
+
:ivar box_references: Box references, defaults to None
|
|
366
|
+
:ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
app_id: int
|
|
370
|
+
args: list[bytes] | None = None
|
|
371
|
+
account_references: list[str] | None = None
|
|
372
|
+
app_references: list[int] | None = None
|
|
373
|
+
asset_references: list[int] | None = None
|
|
374
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
375
|
+
on_complete: OnComplete = OnComplete.DeleteApplicationOC
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dataclass(kw_only=True, frozen=True)
|
|
379
|
+
class _BaseAppMethodCall(_CommonTxnParams):
|
|
380
|
+
app_id: int
|
|
381
|
+
method: Method
|
|
382
|
+
args: list | None = None
|
|
383
|
+
account_references: list[str] | None = None
|
|
384
|
+
app_references: list[int] | None = None
|
|
385
|
+
asset_references: list[int] | None = None
|
|
386
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
387
|
+
schema: AppCreateSchema | None = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@dataclass(kw_only=True, frozen=True)
|
|
391
|
+
class AppMethodCallParams(_CommonTxnParams):
|
|
392
|
+
"""Parameters for calling an application method.
|
|
393
|
+
|
|
394
|
+
:ivar app_id: ID of the application
|
|
395
|
+
:ivar method: The ABI method to call
|
|
396
|
+
:ivar args: Arguments to the ABI method, defaults to None
|
|
397
|
+
:ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None
|
|
398
|
+
:ivar account_references: Account references, defaults to None
|
|
399
|
+
:ivar app_references: App references, defaults to None
|
|
400
|
+
:ivar asset_references: Asset references, defaults to None
|
|
401
|
+
:ivar box_references: Box references, defaults to None
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
app_id: int
|
|
405
|
+
method: Method
|
|
406
|
+
args: list[bytes] | None = None
|
|
407
|
+
on_complete: OnComplete | None = None
|
|
408
|
+
account_references: list[str] | None = None
|
|
409
|
+
app_references: list[int] | None = None
|
|
410
|
+
asset_references: list[int] | None = None
|
|
411
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@dataclass(kw_only=True, frozen=True)
|
|
415
|
+
class AppCallMethodCallParams(_BaseAppMethodCall):
|
|
416
|
+
"""Parameters for a regular ABI method call.
|
|
417
|
+
|
|
418
|
+
:ivar app_id: ID of the application
|
|
419
|
+
:ivar method: The ABI method to call
|
|
420
|
+
:ivar args: Arguments to the ABI method, either an ABI value, transaction with explicit signer,
|
|
421
|
+
transaction, another method call, or None
|
|
422
|
+
:ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
app_id: int
|
|
426
|
+
on_complete: OnComplete | None = None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@dataclass(kw_only=True, frozen=True)
|
|
430
|
+
class AppCreateMethodCallParams(_BaseAppMethodCall):
|
|
431
|
+
"""Parameters for an ABI method call that creates an application.
|
|
432
|
+
|
|
433
|
+
:ivar approval_program: The program to execute for all OnCompletes other than ClearState
|
|
434
|
+
:ivar clear_state_program: The program to execute for ClearState OnComplete
|
|
435
|
+
:ivar schema: The state schema for the app, defaults to None
|
|
436
|
+
:ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None
|
|
437
|
+
:ivar extra_program_pages: Number of extra pages required for the programs, defaults to None
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
approval_program: str | bytes
|
|
441
|
+
clear_state_program: str | bytes
|
|
442
|
+
schema: AppCreateSchema | None = None
|
|
443
|
+
on_complete: OnComplete | None = None
|
|
444
|
+
extra_program_pages: int | None = None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass(kw_only=True, frozen=True)
|
|
448
|
+
class AppUpdateMethodCallParams(_BaseAppMethodCall):
|
|
449
|
+
"""Parameters for an ABI method call that updates an application.
|
|
450
|
+
|
|
451
|
+
:ivar app_id: ID of the application
|
|
452
|
+
:ivar approval_program: The program to execute for all OnCompletes other than ClearState
|
|
453
|
+
:ivar clear_state_program: The program to execute for ClearState OnComplete
|
|
454
|
+
:ivar on_complete: The OnComplete action, defaults to UpdateApplicationOC
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
app_id: int
|
|
458
|
+
approval_program: str | bytes
|
|
459
|
+
clear_state_program: str | bytes
|
|
460
|
+
on_complete: OnComplete = OnComplete.UpdateApplicationOC
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@dataclass(kw_only=True, frozen=True)
|
|
464
|
+
class AppDeleteMethodCallParams(_BaseAppMethodCall):
|
|
465
|
+
"""Parameters for an ABI method call that deletes an application.
|
|
466
|
+
|
|
467
|
+
:ivar app_id: ID of the application
|
|
468
|
+
:ivar on_complete: The OnComplete action, defaults to DeleteApplicationOC
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
app_id: int
|
|
472
|
+
on_complete: OnComplete = OnComplete.DeleteApplicationOC
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
MethodCallParams = (
|
|
476
|
+
AppCallMethodCallParams | AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
AppMethodCallTransactionArgument = (
|
|
481
|
+
TransactionWithSigner
|
|
482
|
+
| algosdk.transaction.Transaction
|
|
483
|
+
| AppCreateMethodCallParams
|
|
484
|
+
| AppUpdateMethodCallParams
|
|
485
|
+
| AppCallMethodCallParams
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
TxnParams = Union[ # noqa: UP007
|
|
490
|
+
PaymentParams,
|
|
491
|
+
AssetCreateParams,
|
|
492
|
+
AssetConfigParams,
|
|
493
|
+
AssetFreezeParams,
|
|
494
|
+
AssetDestroyParams,
|
|
495
|
+
OnlineKeyRegistrationParams,
|
|
496
|
+
AssetTransferParams,
|
|
497
|
+
AssetOptInParams,
|
|
498
|
+
AssetOptOutParams,
|
|
499
|
+
AppCallParams,
|
|
500
|
+
AppCreateParams,
|
|
501
|
+
AppUpdateParams,
|
|
502
|
+
AppDeleteParams,
|
|
503
|
+
MethodCallParams,
|
|
504
|
+
OfflineKeyRegistrationParams,
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@dataclass(frozen=True, kw_only=True)
|
|
509
|
+
class TransactionContext:
|
|
510
|
+
"""Contextual information for a transaction."""
|
|
511
|
+
|
|
512
|
+
max_fee: AlgoAmount | None = None
|
|
513
|
+
abi_method: Method | None = None
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def empty() -> TransactionContext:
|
|
517
|
+
return TransactionContext(max_fee=None, abi_method=None)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class TransactionWithContext:
|
|
521
|
+
"""Combines Transaction with additional context."""
|
|
522
|
+
|
|
523
|
+
def __init__(self, txn: algosdk.transaction.Transaction, context: TransactionContext):
|
|
524
|
+
self.txn = txn
|
|
525
|
+
self.context = context
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TransactionWithSignerAndContext(TransactionWithSigner):
|
|
529
|
+
"""Combines TransactionWithSigner with additional context."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, txn: algosdk.transaction.Transaction, signer: TransactionSigner, context: TransactionContext):
|
|
532
|
+
super().__init__(txn, signer)
|
|
533
|
+
self.context = context
|
|
534
|
+
|
|
535
|
+
@staticmethod
|
|
536
|
+
def from_txn_with_context(
|
|
537
|
+
txn_with_context: TransactionWithContext, signer: TransactionSigner
|
|
538
|
+
) -> TransactionWithSignerAndContext:
|
|
539
|
+
return TransactionWithSignerAndContext(
|
|
540
|
+
txn=txn_with_context.txn, signer=signer, context=txn_with_context.context
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@dataclass(frozen=True)
|
|
545
|
+
class BuiltTransactions:
|
|
546
|
+
"""Set of transactions built by TransactionComposer.
|
|
547
|
+
|
|
548
|
+
:ivar transactions: The built transactions
|
|
549
|
+
:ivar method_calls: Any ABIMethod objects associated with any of the transactions in a map keyed by txn id
|
|
550
|
+
:ivar signers: Any TransactionSigner objects associated with any of the transactions in a map keyed by txn id
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
transactions: list[algosdk.transaction.Transaction]
|
|
554
|
+
method_calls: dict[int, Method]
|
|
555
|
+
signers: dict[int, TransactionSigner]
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@dataclass
|
|
559
|
+
class TransactionComposerBuildResult:
|
|
560
|
+
"""Result of building transactions with TransactionComposer.
|
|
561
|
+
|
|
562
|
+
:ivar atc: The AtomicTransactionComposer instance
|
|
563
|
+
:ivar transactions: The list of transactions with signers
|
|
564
|
+
:ivar method_calls: Map of transaction index to ABI method
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
atc: AtomicTransactionComposer
|
|
568
|
+
transactions: list[TransactionWithSigner]
|
|
569
|
+
method_calls: dict[int, Method]
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@dataclass
|
|
573
|
+
class SendAtomicTransactionComposerResults:
|
|
574
|
+
"""Results from sending an AtomicTransactionComposer transaction group.
|
|
575
|
+
|
|
576
|
+
:ivar group_id: The group ID if this was a transaction group
|
|
577
|
+
:ivar confirmations: The confirmation info for each transaction
|
|
578
|
+
:ivar tx_ids: The transaction IDs that were sent
|
|
579
|
+
:ivar transactions: The transactions that were sent
|
|
580
|
+
:ivar returns: The ABI return values from any ABI method calls
|
|
581
|
+
:ivar simulate_response: The simulation response if simulation was performed, defaults to None
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
group_id: str
|
|
585
|
+
confirmations: list[algosdk.v2client.algod.AlgodResponseType]
|
|
586
|
+
tx_ids: list[str]
|
|
587
|
+
transactions: list[TransactionWrapper]
|
|
588
|
+
returns: list[ABIReturn]
|
|
589
|
+
simulate_response: dict[str, Any] | None = None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@dataclass
|
|
593
|
+
class ExecutionInfoTxn:
|
|
594
|
+
unnamed_resources_accessed: dict | None = None
|
|
595
|
+
required_fee_delta: int = 0
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@dataclass
|
|
599
|
+
class ExecutionInfo:
|
|
600
|
+
"""Information about transaction execution from simulation."""
|
|
601
|
+
|
|
602
|
+
group_unnamed_resources_accessed: dict[str, Any] | None = None
|
|
603
|
+
txns: list[ExecutionInfoTxn] | None = None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@dataclass
|
|
607
|
+
class _TransactionWithPriority:
|
|
608
|
+
txn: algosdk.transaction.Transaction
|
|
609
|
+
priority: int
|
|
610
|
+
fee_delta: int
|
|
611
|
+
index: int
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
MAX_LEASE_LENGTH = 32
|
|
615
|
+
NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _encode_lease(lease: str | bytes | None) -> bytes | None:
|
|
619
|
+
if lease is None:
|
|
620
|
+
return None
|
|
621
|
+
elif isinstance(lease, bytes):
|
|
622
|
+
if not (1 <= len(lease) <= MAX_LEASE_LENGTH):
|
|
623
|
+
raise ValueError(
|
|
624
|
+
f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, "
|
|
625
|
+
f"but received bytes with length {len(lease)}"
|
|
626
|
+
)
|
|
627
|
+
if len(lease) == MAX_LEASE_LENGTH:
|
|
628
|
+
return lease
|
|
629
|
+
lease32 = bytearray(32)
|
|
630
|
+
lease32[: len(lease)] = lease
|
|
631
|
+
return bytes(lease32)
|
|
632
|
+
elif isinstance(lease, str):
|
|
633
|
+
encoded = lease.encode("utf-8")
|
|
634
|
+
if not (1 <= len(encoded) <= MAX_LEASE_LENGTH):
|
|
635
|
+
raise ValueError(
|
|
636
|
+
f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, "
|
|
637
|
+
f"but received '{lease}' with length {len(lease)}"
|
|
638
|
+
)
|
|
639
|
+
lease32 = bytearray(MAX_LEASE_LENGTH)
|
|
640
|
+
lease32[: len(encoded)] = encoded
|
|
641
|
+
return bytes(lease32)
|
|
642
|
+
else:
|
|
643
|
+
raise TypeError(f"Unknown lease type received of {type(lease)}")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _get_group_execution_info( # noqa: C901, PLR0912
|
|
647
|
+
atc: AtomicTransactionComposer,
|
|
648
|
+
algod: AlgodClient,
|
|
649
|
+
populate_app_call_resources: bool | None = None,
|
|
650
|
+
cover_app_call_inner_transaction_fees: bool | None = None,
|
|
651
|
+
additional_atc_context: AdditionalAtcContext | None = None,
|
|
652
|
+
) -> ExecutionInfo:
|
|
653
|
+
# Create simulation request
|
|
654
|
+
suggested_params = additional_atc_context.suggested_params if additional_atc_context else None
|
|
655
|
+
max_fees = additional_atc_context.max_fees if additional_atc_context else None
|
|
656
|
+
|
|
657
|
+
simulate_request = SimulateRequest(
|
|
658
|
+
txn_groups=[],
|
|
659
|
+
allow_unnamed_resources=True,
|
|
660
|
+
allow_empty_signatures=True,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Clone ATC with null signers
|
|
664
|
+
empty_signer_atc = atc.clone()
|
|
665
|
+
|
|
666
|
+
# Track app call indexes without max fees
|
|
667
|
+
app_call_indexes_without_max_fees = []
|
|
668
|
+
|
|
669
|
+
# Copy transactions with null signers
|
|
670
|
+
for i, txn in enumerate(empty_signer_atc.txn_list):
|
|
671
|
+
txn_with_signer = TransactionWithSigner(txn=txn.txn, signer=NULL_SIGNER)
|
|
672
|
+
|
|
673
|
+
if cover_app_call_inner_transaction_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn):
|
|
674
|
+
if not suggested_params:
|
|
675
|
+
raise ValueError("suggested_params required when cover_app_call_inner_transaction_fees enabled")
|
|
676
|
+
|
|
677
|
+
max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr]
|
|
678
|
+
if max_fee is None:
|
|
679
|
+
app_call_indexes_without_max_fees.append(i)
|
|
680
|
+
else:
|
|
681
|
+
txn_with_signer.txn.fee = max_fee
|
|
682
|
+
|
|
683
|
+
if cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees:
|
|
684
|
+
raise ValueError(
|
|
685
|
+
f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_transaction_fees` is enabled. " # noqa: E501
|
|
686
|
+
f"Required for transactions: {', '.join(str(i) for i in app_call_indexes_without_max_fees)}"
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
# Get fee parameters
|
|
690
|
+
per_byte_txn_fee = suggested_params.fee if suggested_params else 0
|
|
691
|
+
min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 # type: ignore[unused-ignore]
|
|
692
|
+
|
|
693
|
+
# Simulate transactions
|
|
694
|
+
result = empty_signer_atc.simulate(algod, simulate_request)
|
|
695
|
+
|
|
696
|
+
group_response = result.simulate_response["txn-groups"][0]
|
|
697
|
+
|
|
698
|
+
if group_response.get("failure-message"):
|
|
699
|
+
msg = group_response["failure-message"]
|
|
700
|
+
if cover_app_call_inner_transaction_fees and "fee too small" in msg:
|
|
701
|
+
raise ValueError(
|
|
702
|
+
"Fees were too small to resolve execution info via simulate. "
|
|
703
|
+
"You may need to increase an app call transaction maxFee."
|
|
704
|
+
)
|
|
705
|
+
failed_at = group_response.get("failed-at", [0])[0]
|
|
706
|
+
raise ValueError(
|
|
707
|
+
f"Error during resource population simulation in transaction {failed_at}: "
|
|
708
|
+
f"{group_response['failure-message']}"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Build execution info
|
|
712
|
+
txn_results = []
|
|
713
|
+
for i, txn_result_raw in enumerate(group_response["txn-results"]):
|
|
714
|
+
txn_result = txn_result_raw.get("txn-result")
|
|
715
|
+
if not txn_result:
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
original_txn = atc.build_group()[i].txn
|
|
719
|
+
|
|
720
|
+
required_fee_delta = 0
|
|
721
|
+
if cover_app_call_inner_transaction_fees:
|
|
722
|
+
# Calculate parent transaction fee
|
|
723
|
+
parent_per_byte_fee = per_byte_txn_fee * (original_txn.estimate_size() + 75)
|
|
724
|
+
parent_min_fee = max(parent_per_byte_fee, min_txn_fee)
|
|
725
|
+
parent_fee_delta = parent_min_fee - original_txn.fee
|
|
726
|
+
|
|
727
|
+
if isinstance(original_txn, algosdk.transaction.ApplicationCallTxn):
|
|
728
|
+
# Calculate inner transaction fees recursively
|
|
729
|
+
def calculate_inner_fee_delta(inner_txns: list[dict], acc: int = 0) -> int:
|
|
730
|
+
for inner_txn in reversed(inner_txns):
|
|
731
|
+
current_fee_delta = (
|
|
732
|
+
calculate_inner_fee_delta(inner_txn["inner-txns"], acc)
|
|
733
|
+
if inner_txn.get("inner-txns")
|
|
734
|
+
else acc
|
|
735
|
+
) + (min_txn_fee - inner_txn["txn"]["txn"].get("fee", 0))
|
|
736
|
+
acc = max(0, current_fee_delta)
|
|
737
|
+
return acc
|
|
738
|
+
|
|
739
|
+
inner_fee_delta = calculate_inner_fee_delta(txn_result.get("inner-txns", []))
|
|
740
|
+
required_fee_delta = inner_fee_delta + parent_fee_delta
|
|
741
|
+
else:
|
|
742
|
+
required_fee_delta = parent_fee_delta
|
|
743
|
+
|
|
744
|
+
txn_results.append(
|
|
745
|
+
ExecutionInfoTxn(
|
|
746
|
+
unnamed_resources_accessed=txn_result_raw.get("unnamed-resources-accessed")
|
|
747
|
+
if populate_app_call_resources
|
|
748
|
+
else None,
|
|
749
|
+
required_fee_delta=required_fee_delta,
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return ExecutionInfo(
|
|
754
|
+
group_unnamed_resources_accessed=group_response.get("unnamed-resources-accessed")
|
|
755
|
+
if populate_app_call_resources
|
|
756
|
+
else None,
|
|
757
|
+
txns=txn_results,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _find_available_transaction_index(
|
|
762
|
+
txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int
|
|
763
|
+
) -> int:
|
|
764
|
+
"""Find index of first transaction that can accommodate the new reference."""
|
|
765
|
+
|
|
766
|
+
def check_transaction(txn: TransactionWithSigner) -> bool:
|
|
767
|
+
# Skip if not an application call transaction
|
|
768
|
+
if txn.txn.type != "appl":
|
|
769
|
+
return False
|
|
770
|
+
|
|
771
|
+
# Get current counts (using get() with default 0 for Pythonic null handling)
|
|
772
|
+
accounts = len(getattr(txn.txn, "accounts", []) or [])
|
|
773
|
+
assets = len(getattr(txn.txn, "foreign_assets", []) or [])
|
|
774
|
+
apps = len(getattr(txn.txn, "foreign_apps", []) or [])
|
|
775
|
+
boxes = len(getattr(txn.txn, "boxes", []) or [])
|
|
776
|
+
|
|
777
|
+
# For account references, only check account limit
|
|
778
|
+
if reference_type == "account":
|
|
779
|
+
return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
|
|
780
|
+
|
|
781
|
+
# For asset holdings or local state, need space for both account and other reference
|
|
782
|
+
if reference_type in ("asset_holding", "app_local"):
|
|
783
|
+
return (
|
|
784
|
+
accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1
|
|
785
|
+
and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# For boxes with non-zero app ID, need space for box and app reference
|
|
789
|
+
if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0:
|
|
790
|
+
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1
|
|
791
|
+
|
|
792
|
+
# Default case - just check total references
|
|
793
|
+
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
|
|
794
|
+
|
|
795
|
+
# Return first matching index or -1 if none found
|
|
796
|
+
return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer:
|
|
800
|
+
"""Populate application call resources based on simulation results.
|
|
801
|
+
|
|
802
|
+
:param atc: The AtomicTransactionComposer containing transactions
|
|
803
|
+
:param algod: Algod client for simulation
|
|
804
|
+
:return: Modified AtomicTransactionComposer with populated resources
|
|
805
|
+
"""
|
|
806
|
+
return prepare_group_for_sending(atc, algod, populate_app_call_resources=True)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915
|
|
810
|
+
atc: AtomicTransactionComposer,
|
|
811
|
+
algod: AlgodClient,
|
|
812
|
+
populate_app_call_resources: bool | None = None,
|
|
813
|
+
cover_app_call_inner_transaction_fees: bool | None = None,
|
|
814
|
+
additional_atc_context: AdditionalAtcContext | None = None,
|
|
815
|
+
) -> AtomicTransactionComposer:
|
|
816
|
+
"""Prepare a transaction group for sending by handling execution info and resources.
|
|
817
|
+
|
|
818
|
+
:param atc: The AtomicTransactionComposer containing transactions
|
|
819
|
+
:param algod: Algod client for simulation
|
|
820
|
+
:param populate_app_call_resources: Whether to populate app call resources
|
|
821
|
+
:param cover_app_call_inner_transaction_fees: Whether to cover inner txn fees
|
|
822
|
+
:param additional_atc_context: Additional context for the AtomicTransactionComposer
|
|
823
|
+
:return: Modified AtomicTransactionComposer ready for sending
|
|
824
|
+
"""
|
|
825
|
+
# Get execution info via simulation
|
|
826
|
+
execution_info = _get_group_execution_info(
|
|
827
|
+
atc, algod, populate_app_call_resources, cover_app_call_inner_transaction_fees, additional_atc_context
|
|
828
|
+
)
|
|
829
|
+
max_fees = additional_atc_context.max_fees if additional_atc_context else None
|
|
830
|
+
|
|
831
|
+
group = atc.build_group()
|
|
832
|
+
|
|
833
|
+
# Handle transaction fees if needed
|
|
834
|
+
if cover_app_call_inner_transaction_fees:
|
|
835
|
+
# Sort transactions by fee priority
|
|
836
|
+
txns_with_priority: list[_TransactionWithPriority] = []
|
|
837
|
+
for i, txn_info in enumerate(execution_info.txns or []):
|
|
838
|
+
if not txn_info:
|
|
839
|
+
continue
|
|
840
|
+
txn = group[i].txn
|
|
841
|
+
max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr]
|
|
842
|
+
immutable_fee = max_fee is not None and max_fee == txn.fee
|
|
843
|
+
priority_multiplier = (
|
|
844
|
+
1000
|
|
845
|
+
if (
|
|
846
|
+
txn_info.required_fee_delta > 0
|
|
847
|
+
and (immutable_fee or not isinstance(txn, algosdk.transaction.ApplicationCallTxn))
|
|
848
|
+
)
|
|
849
|
+
else 1
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
txns_with_priority.append(
|
|
853
|
+
_TransactionWithPriority(
|
|
854
|
+
txn=txn,
|
|
855
|
+
index=i,
|
|
856
|
+
fee_delta=txn_info.required_fee_delta,
|
|
857
|
+
priority=txn_info.required_fee_delta * priority_multiplier
|
|
858
|
+
if txn_info.required_fee_delta > 0
|
|
859
|
+
else -1,
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Sort by priority descending
|
|
864
|
+
txns_with_priority.sort(key=lambda x: x.priority, reverse=True)
|
|
865
|
+
|
|
866
|
+
# Calculate surplus fees and additional fees needed
|
|
867
|
+
surplus_fees = sum(
|
|
868
|
+
txn_info.required_fee_delta * -1
|
|
869
|
+
for txn_info in execution_info.txns or []
|
|
870
|
+
if txn_info is not None and txn_info.required_fee_delta < 0
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
additional_fees = {}
|
|
874
|
+
|
|
875
|
+
# Distribute surplus fees to cover deficits
|
|
876
|
+
for txn_obj in txns_with_priority:
|
|
877
|
+
if txn_obj.fee_delta > 0:
|
|
878
|
+
if surplus_fees >= txn_obj.fee_delta:
|
|
879
|
+
surplus_fees -= txn_obj.fee_delta
|
|
880
|
+
else:
|
|
881
|
+
additional_fees[txn_obj.index] = txn_obj.fee_delta - surplus_fees
|
|
882
|
+
surplus_fees = 0
|
|
883
|
+
|
|
884
|
+
def populate_group_resource( # noqa: PLR0915, PLR0912, C901
|
|
885
|
+
txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str
|
|
886
|
+
) -> None:
|
|
887
|
+
"""Helper function to populate group-level resources."""
|
|
888
|
+
|
|
889
|
+
def is_appl_below_limit(t: TransactionWithSigner) -> bool:
|
|
890
|
+
if not isinstance(t.txn, transaction.ApplicationCallTxn):
|
|
891
|
+
return False
|
|
892
|
+
|
|
893
|
+
accounts = len(getattr(t.txn, "accounts", []) or [])
|
|
894
|
+
assets = len(getattr(t.txn, "foreign_assets", []) or [])
|
|
895
|
+
apps = len(getattr(t.txn, "foreign_apps", []) or [])
|
|
896
|
+
boxes = len(getattr(t.txn, "boxes", []) or [])
|
|
897
|
+
|
|
898
|
+
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
|
|
899
|
+
|
|
900
|
+
# Handle asset holding and app local references first
|
|
901
|
+
if ref_type in ("assetHolding", "appLocal"):
|
|
902
|
+
ref_dict = cast(dict[str, Any], reference)
|
|
903
|
+
account = ref_dict["account"]
|
|
904
|
+
|
|
905
|
+
# First try to find transaction with account already available
|
|
906
|
+
txn_idx = next(
|
|
907
|
+
(
|
|
908
|
+
i
|
|
909
|
+
for i, t in enumerate(txns)
|
|
910
|
+
if is_appl_below_limit(t)
|
|
911
|
+
and isinstance(t.txn, transaction.ApplicationCallTxn)
|
|
912
|
+
and (
|
|
913
|
+
account in (getattr(t.txn, "accounts", []) or [])
|
|
914
|
+
or account
|
|
915
|
+
in (
|
|
916
|
+
logic.get_application_address(app_id)
|
|
917
|
+
for app_id in (getattr(t.txn, "foreign_apps", []) or [])
|
|
918
|
+
)
|
|
919
|
+
or any(str(account) in str(v) for v in t.txn.__dict__.values())
|
|
920
|
+
)
|
|
921
|
+
),
|
|
922
|
+
-1,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if txn_idx >= 0:
|
|
926
|
+
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
|
|
927
|
+
if ref_type == "assetHolding":
|
|
928
|
+
asset_id = ref_dict["asset"]
|
|
929
|
+
app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id]
|
|
930
|
+
else:
|
|
931
|
+
app_id = ref_dict["app"]
|
|
932
|
+
app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id]
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
# Try to find transaction that already has the app/asset available
|
|
936
|
+
txn_idx = next(
|
|
937
|
+
(
|
|
938
|
+
i
|
|
939
|
+
for i, t in enumerate(txns)
|
|
940
|
+
if is_appl_below_limit(t)
|
|
941
|
+
and isinstance(t.txn, transaction.ApplicationCallTxn)
|
|
942
|
+
and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES
|
|
943
|
+
and (
|
|
944
|
+
(
|
|
945
|
+
ref_type == "assetHolding"
|
|
946
|
+
and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or [])
|
|
947
|
+
)
|
|
948
|
+
or (
|
|
949
|
+
ref_type == "appLocal"
|
|
950
|
+
and (
|
|
951
|
+
ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or [])
|
|
952
|
+
or t.txn.index == ref_dict["app"]
|
|
953
|
+
)
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
),
|
|
957
|
+
-1,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
if txn_idx >= 0:
|
|
961
|
+
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
|
|
962
|
+
accounts = list(getattr(app_txn, "accounts", []) or [])
|
|
963
|
+
accounts.append(account)
|
|
964
|
+
app_txn.accounts = accounts
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
# Handle box references
|
|
968
|
+
if ref_type == "box":
|
|
969
|
+
box_ref = (reference["app"], base64.b64decode(reference["name"])) # type: ignore[index]
|
|
970
|
+
|
|
971
|
+
# Try to find transaction that already has the app available
|
|
972
|
+
txn_idx = next(
|
|
973
|
+
(
|
|
974
|
+
i
|
|
975
|
+
for i, t in enumerate(txns)
|
|
976
|
+
if is_appl_below_limit(t)
|
|
977
|
+
and isinstance(t.txn, transaction.ApplicationCallTxn)
|
|
978
|
+
and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0])
|
|
979
|
+
),
|
|
980
|
+
-1,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
if txn_idx >= 0:
|
|
984
|
+
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
|
|
985
|
+
boxes = list(getattr(app_txn, "boxes", []) or [])
|
|
986
|
+
boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type]
|
|
987
|
+
app_txn.boxes = boxes
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
# Find available transaction for the resource
|
|
991
|
+
txn_idx = _find_available_transaction_index(txns, ref_type, reference)
|
|
992
|
+
|
|
993
|
+
if txn_idx == -1:
|
|
994
|
+
raise ValueError("No more transactions below reference limit. Add another app call to the group.")
|
|
995
|
+
|
|
996
|
+
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
|
|
997
|
+
|
|
998
|
+
if ref_type == "account":
|
|
999
|
+
accounts = list(getattr(app_txn, "accounts", []) or [])
|
|
1000
|
+
accounts.append(cast(str, reference))
|
|
1001
|
+
app_txn.accounts = accounts
|
|
1002
|
+
elif ref_type == "app":
|
|
1003
|
+
app_id = int(cast(str | int, reference))
|
|
1004
|
+
foreign_apps = list(getattr(app_txn, "foreign_apps", []) or [])
|
|
1005
|
+
foreign_apps.append(app_id)
|
|
1006
|
+
app_txn.foreign_apps = foreign_apps
|
|
1007
|
+
elif ref_type == "box":
|
|
1008
|
+
boxes = list(getattr(app_txn, "boxes", []) or [])
|
|
1009
|
+
boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type]
|
|
1010
|
+
app_txn.boxes = boxes
|
|
1011
|
+
if box_ref[0] != 0:
|
|
1012
|
+
foreign_apps = list(getattr(app_txn, "foreign_apps", []) or [])
|
|
1013
|
+
foreign_apps.append(box_ref[0])
|
|
1014
|
+
app_txn.foreign_apps = foreign_apps
|
|
1015
|
+
elif ref_type == "asset":
|
|
1016
|
+
asset_id = int(cast(str | int, reference))
|
|
1017
|
+
foreign_assets = list(getattr(app_txn, "foreign_assets", []) or [])
|
|
1018
|
+
foreign_assets.append(asset_id)
|
|
1019
|
+
app_txn.foreign_assets = foreign_assets
|
|
1020
|
+
elif ref_type == "assetHolding":
|
|
1021
|
+
ref_dict = cast(dict[str, Any], reference)
|
|
1022
|
+
foreign_assets = list(getattr(app_txn, "foreign_assets", []) or [])
|
|
1023
|
+
foreign_assets.append(ref_dict["asset"])
|
|
1024
|
+
app_txn.foreign_assets = foreign_assets
|
|
1025
|
+
accounts = list(getattr(app_txn, "accounts", []) or [])
|
|
1026
|
+
accounts.append(ref_dict["account"])
|
|
1027
|
+
app_txn.accounts = accounts
|
|
1028
|
+
elif ref_type == "appLocal":
|
|
1029
|
+
ref_dict = cast(dict[str, Any], reference)
|
|
1030
|
+
foreign_apps = list(getattr(app_txn, "foreign_apps", []) or [])
|
|
1031
|
+
foreign_apps.append(ref_dict["app"])
|
|
1032
|
+
app_txn.foreign_apps = foreign_apps
|
|
1033
|
+
accounts = list(getattr(app_txn, "accounts", []) or [])
|
|
1034
|
+
accounts.append(ref_dict["account"])
|
|
1035
|
+
app_txn.accounts = accounts
|
|
1036
|
+
|
|
1037
|
+
# Process transaction-level resources
|
|
1038
|
+
for i, txn_info in enumerate(execution_info.txns or []):
|
|
1039
|
+
if not txn_info:
|
|
1040
|
+
continue
|
|
1041
|
+
|
|
1042
|
+
# Validate no unexpected resources
|
|
1043
|
+
is_app_txn = isinstance(group[i].txn, algosdk.transaction.ApplicationCallTxn)
|
|
1044
|
+
resources = txn_info.unnamed_resources_accessed
|
|
1045
|
+
if resources and is_app_txn:
|
|
1046
|
+
app_txn = group[i].txn
|
|
1047
|
+
if resources.get("boxes") or resources.get("extra-box-refs"):
|
|
1048
|
+
raise ValueError("Unexpected boxes at transaction level")
|
|
1049
|
+
if resources.get("appLocals"):
|
|
1050
|
+
raise ValueError("Unexpected app local at transaction level")
|
|
1051
|
+
if resources.get("assetHoldings"):
|
|
1052
|
+
raise ValueError("Unexpected asset holding at transaction level")
|
|
1053
|
+
|
|
1054
|
+
# Update application call fields
|
|
1055
|
+
accounts = list(getattr(app_txn, "accounts", []) or [])
|
|
1056
|
+
foreign_apps = list(getattr(app_txn, "foreign_apps", []) or [])
|
|
1057
|
+
foreign_assets = list(getattr(app_txn, "foreign_assets", []) or [])
|
|
1058
|
+
boxes = list(getattr(app_txn, "boxes", []) or [])
|
|
1059
|
+
|
|
1060
|
+
# Add new resources
|
|
1061
|
+
accounts.extend(resources.get("accounts", []))
|
|
1062
|
+
foreign_apps.extend(resources.get("apps", []))
|
|
1063
|
+
foreign_assets.extend(resources.get("assets", []))
|
|
1064
|
+
boxes.extend(resources.get("boxes", []))
|
|
1065
|
+
|
|
1066
|
+
# Validate limits
|
|
1067
|
+
if len(accounts) > MAX_APP_CALL_ACCOUNT_REFERENCES:
|
|
1068
|
+
raise ValueError(
|
|
1069
|
+
f"Account reference limit of {MAX_APP_CALL_ACCOUNT_REFERENCES} exceeded in transaction {i}"
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
total_refs = len(accounts) + len(foreign_assets) + len(foreign_apps) + len(boxes)
|
|
1073
|
+
if total_refs > MAX_APP_CALL_FOREIGN_REFERENCES:
|
|
1074
|
+
raise ValueError(
|
|
1075
|
+
f"Resource reference limit of {MAX_APP_CALL_FOREIGN_REFERENCES} exceeded in transaction {i}"
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
# Update transaction
|
|
1079
|
+
app_txn.accounts = accounts # type: ignore[attr-defined]
|
|
1080
|
+
app_txn.foreign_apps = foreign_apps # type: ignore[attr-defined]
|
|
1081
|
+
app_txn.foreign_assets = foreign_assets # type: ignore[attr-defined]
|
|
1082
|
+
app_txn.boxes = boxes # type: ignore[attr-defined]
|
|
1083
|
+
|
|
1084
|
+
# Update fees if needed
|
|
1085
|
+
if cover_app_call_inner_transaction_fees and i in additional_fees:
|
|
1086
|
+
cur_txn = group[i].txn
|
|
1087
|
+
additional_fee = additional_fees[i]
|
|
1088
|
+
if not isinstance(cur_txn, algosdk.transaction.ApplicationCallTxn):
|
|
1089
|
+
raise ValueError(
|
|
1090
|
+
f"An additional fee of {additional_fee} µALGO is required for non app call transaction {i}"
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
transaction_fee = cur_txn.fee + additional_fee
|
|
1094
|
+
max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr]
|
|
1095
|
+
|
|
1096
|
+
if max_fee is None or transaction_fee > max_fee:
|
|
1097
|
+
raise ValueError(
|
|
1098
|
+
f"Calculated transaction fee {transaction_fee} µALGO is greater "
|
|
1099
|
+
f"than max of {max_fee or 'undefined'} "
|
|
1100
|
+
f"for transaction {i}"
|
|
1101
|
+
)
|
|
1102
|
+
cur_txn.fee = transaction_fee
|
|
1103
|
+
|
|
1104
|
+
# Process group-level resources
|
|
1105
|
+
group_resources = execution_info.group_unnamed_resources_accessed
|
|
1106
|
+
if group_resources:
|
|
1107
|
+
# Handle cross-reference resources first
|
|
1108
|
+
for app_local in group_resources.get("appLocals", []):
|
|
1109
|
+
populate_group_resource(group, app_local, "appLocal")
|
|
1110
|
+
# Remove processed resources
|
|
1111
|
+
if "accounts" in group_resources:
|
|
1112
|
+
group_resources["accounts"] = [
|
|
1113
|
+
acc for acc in group_resources["accounts"] if acc != app_local["account"]
|
|
1114
|
+
]
|
|
1115
|
+
if "apps" in group_resources:
|
|
1116
|
+
group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(app_local["app"])]
|
|
1117
|
+
|
|
1118
|
+
for asset_holding in group_resources.get("assetHoldings", []):
|
|
1119
|
+
populate_group_resource(group, asset_holding, "assetHolding")
|
|
1120
|
+
# Remove processed resources
|
|
1121
|
+
if "accounts" in group_resources:
|
|
1122
|
+
group_resources["accounts"] = [
|
|
1123
|
+
acc for acc in group_resources["accounts"] if acc != asset_holding["account"]
|
|
1124
|
+
]
|
|
1125
|
+
if "assets" in group_resources:
|
|
1126
|
+
group_resources["assets"] = [
|
|
1127
|
+
asset for asset in group_resources["assets"] if int(asset) != int(asset_holding["asset"])
|
|
1128
|
+
]
|
|
1129
|
+
|
|
1130
|
+
# Handle remaining resources
|
|
1131
|
+
for account in group_resources.get("accounts", []):
|
|
1132
|
+
populate_group_resource(group, account, "account")
|
|
1133
|
+
|
|
1134
|
+
for box in group_resources.get("boxes", []):
|
|
1135
|
+
populate_group_resource(group, box, "box")
|
|
1136
|
+
if "apps" in group_resources:
|
|
1137
|
+
group_resources["apps"] = [app for app in group_resources["apps"] if int(app) != int(box["app"])]
|
|
1138
|
+
|
|
1139
|
+
for asset in group_resources.get("assets", []):
|
|
1140
|
+
populate_group_resource(group, asset, "asset")
|
|
1141
|
+
|
|
1142
|
+
for app in group_resources.get("apps", []):
|
|
1143
|
+
populate_group_resource(group, app, "app")
|
|
1144
|
+
|
|
1145
|
+
# Handle extra box references
|
|
1146
|
+
extra_box_refs = group_resources.get("extra-box-refs", 0)
|
|
1147
|
+
for _ in range(extra_box_refs):
|
|
1148
|
+
populate_group_resource(group, {"app": 0, "name": ""}, "box")
|
|
1149
|
+
|
|
1150
|
+
# Create new ATC with updated transactions
|
|
1151
|
+
new_atc = AtomicTransactionComposer()
|
|
1152
|
+
for txn_with_signer in group:
|
|
1153
|
+
txn_with_signer.txn.group = None
|
|
1154
|
+
new_atc.add_transaction(txn_with_signer)
|
|
1155
|
+
new_atc.method_dict = deepcopy(atc.method_dict)
|
|
1156
|
+
|
|
1157
|
+
return new_atc
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def send_atomic_transaction_composer( # noqa: C901, PLR0912
|
|
1161
|
+
atc: AtomicTransactionComposer,
|
|
1162
|
+
algod: AlgodClient,
|
|
1163
|
+
*,
|
|
1164
|
+
max_rounds_to_wait: int | None = 5,
|
|
1165
|
+
skip_waiting: bool = False,
|
|
1166
|
+
suppress_log: bool | None = None,
|
|
1167
|
+
populate_app_call_resources: bool | None = None,
|
|
1168
|
+
cover_app_call_inner_transaction_fees: bool | None = None,
|
|
1169
|
+
additional_atc_context: AdditionalAtcContext | None = None,
|
|
1170
|
+
) -> SendAtomicTransactionComposerResults:
|
|
1171
|
+
"""Send an AtomicTransactionComposer transaction group.
|
|
1172
|
+
|
|
1173
|
+
Executes a group of transactions atomically using the AtomicTransactionComposer.
|
|
1174
|
+
|
|
1175
|
+
:param atc: The AtomicTransactionComposer instance containing the transaction group to send
|
|
1176
|
+
:param algod: The Algod client to use for sending the transactions
|
|
1177
|
+
:param max_rounds_to_wait: Maximum number of rounds to wait for confirmation, defaults to 5
|
|
1178
|
+
:param skip_waiting: If True, don't wait for transaction confirmation, defaults to False
|
|
1179
|
+
:param suppress_log: If True, suppress logging, defaults to None
|
|
1180
|
+
:param populate_app_call_resources: If True, populate app call resources, defaults to None
|
|
1181
|
+
:param cover_app_call_inner_transaction_fees: If True, cover app call inner transaction fees, defaults to None
|
|
1182
|
+
:param additional_atc_context: Additional context for the AtomicTransactionComposer
|
|
1183
|
+
:return: Results from sending the transaction group
|
|
1184
|
+
:raises Exception: If there is an error sending the transactions
|
|
1185
|
+
:raises error: If there is an error from the Algorand node
|
|
1186
|
+
"""
|
|
1187
|
+
from algokit_utils._debugging import simulate_and_persist_response, simulate_response
|
|
1188
|
+
|
|
1189
|
+
try:
|
|
1190
|
+
# Build transactions
|
|
1191
|
+
transactions_with_signer = atc.build_group()
|
|
1192
|
+
|
|
1193
|
+
populate_app_call_resources = (
|
|
1194
|
+
populate_app_call_resources
|
|
1195
|
+
if populate_app_call_resources is not None
|
|
1196
|
+
else config.populate_app_call_resource
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
if (populate_app_call_resources or cover_app_call_inner_transaction_fees) and any(
|
|
1200
|
+
isinstance(t.txn, algosdk.transaction.ApplicationCallTxn) for t in transactions_with_signer
|
|
1201
|
+
):
|
|
1202
|
+
atc = prepare_group_for_sending(
|
|
1203
|
+
atc,
|
|
1204
|
+
algod,
|
|
1205
|
+
populate_app_call_resources,
|
|
1206
|
+
cover_app_call_inner_transaction_fees,
|
|
1207
|
+
additional_atc_context,
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
transactions_to_send = [t.txn for t in transactions_with_signer]
|
|
1211
|
+
|
|
1212
|
+
# Get group ID if multiple transactions
|
|
1213
|
+
group_id = None
|
|
1214
|
+
if len(transactions_to_send) > 1:
|
|
1215
|
+
group_id = (
|
|
1216
|
+
base64.b64encode(transactions_to_send[0].group).decode("utf-8") if transactions_to_send[0].group else ""
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
if not suppress_log:
|
|
1220
|
+
logger.info(
|
|
1221
|
+
f"Sending group of {len(transactions_to_send)} transactions ({group_id})",
|
|
1222
|
+
suppress_log=suppress_log or False,
|
|
1223
|
+
)
|
|
1224
|
+
logger.debug(
|
|
1225
|
+
f"Transaction IDs ({group_id}): {[t.get_txid() for t in transactions_to_send]}",
|
|
1226
|
+
suppress_log=suppress_log or False,
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
# Simulate if debug enabled
|
|
1230
|
+
if config.debug and config.trace_all and config.project_root:
|
|
1231
|
+
simulate_and_persist_response(
|
|
1232
|
+
atc,
|
|
1233
|
+
config.project_root,
|
|
1234
|
+
algod,
|
|
1235
|
+
config.trace_buffer_size_mb,
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
# Execute transactions
|
|
1239
|
+
result = atc.execute(algod, wait_rounds=max_rounds_to_wait or 5)
|
|
1240
|
+
|
|
1241
|
+
# Log results
|
|
1242
|
+
if not suppress_log:
|
|
1243
|
+
if len(transactions_to_send) > 1:
|
|
1244
|
+
logger.info(
|
|
1245
|
+
f"Group transaction ({group_id}) sent with {len(transactions_to_send)} transactions",
|
|
1246
|
+
suppress_log=suppress_log or False,
|
|
1247
|
+
)
|
|
1248
|
+
else:
|
|
1249
|
+
logger.info(
|
|
1250
|
+
f"Sent transaction ID {transactions_to_send[0].get_txid()}",
|
|
1251
|
+
suppress_log=suppress_log or False,
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
# Get confirmations if not skipping
|
|
1255
|
+
confirmations = None
|
|
1256
|
+
if not skip_waiting:
|
|
1257
|
+
confirmations = [algod.pending_transaction_info(t.get_txid()) for t in transactions_to_send]
|
|
1258
|
+
|
|
1259
|
+
# Return results
|
|
1260
|
+
return SendAtomicTransactionComposerResults(
|
|
1261
|
+
group_id=group_id or "",
|
|
1262
|
+
confirmations=confirmations or [],
|
|
1263
|
+
tx_ids=[t.get_txid() for t in transactions_to_send],
|
|
1264
|
+
transactions=[TransactionWrapper(t) for t in transactions_to_send],
|
|
1265
|
+
returns=[ABIReturn(r) for r in result.abi_results],
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
except Exception as e:
|
|
1269
|
+
# Handle error with debug info if enabled
|
|
1270
|
+
if config.debug:
|
|
1271
|
+
logger.error(
|
|
1272
|
+
"Received error executing Atomic Transaction Composer and debug flag enabled; "
|
|
1273
|
+
"attempting simulation to get more information",
|
|
1274
|
+
suppress_log=suppress_log or False,
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
simulate = None
|
|
1278
|
+
if config.project_root and not config.trace_all:
|
|
1279
|
+
# Only simulate if trace_all is disabled and project_root is set
|
|
1280
|
+
simulate = simulate_and_persist_response(atc, config.project_root, algod, config.trace_buffer_size_mb)
|
|
1281
|
+
else:
|
|
1282
|
+
simulate = simulate_response(atc, algod)
|
|
1283
|
+
|
|
1284
|
+
traces = []
|
|
1285
|
+
if simulate and simulate.failed_at:
|
|
1286
|
+
for txn_group in simulate.simulate_response["txn-groups"]:
|
|
1287
|
+
app_budget = txn_group.get("app-budget-added")
|
|
1288
|
+
app_budget_consumed = txn_group.get("app-budget-consumed")
|
|
1289
|
+
failure_message = txn_group.get("failure-message")
|
|
1290
|
+
txn_result = txn_group.get("txn-results", [{}])[0]
|
|
1291
|
+
exec_trace = txn_result.get("exec-trace", {})
|
|
1292
|
+
|
|
1293
|
+
traces.append(
|
|
1294
|
+
{
|
|
1295
|
+
"trace": exec_trace,
|
|
1296
|
+
"app_budget": app_budget,
|
|
1297
|
+
"app_budget_consumed": app_budget_consumed,
|
|
1298
|
+
"failure_message": failure_message,
|
|
1299
|
+
}
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
error = Exception(f"Transaction failed: {e}")
|
|
1303
|
+
error.traces = traces # type: ignore[attr-defined]
|
|
1304
|
+
raise error from e
|
|
1305
|
+
|
|
1306
|
+
logger.error(
|
|
1307
|
+
"Received error executing Atomic Transaction Composer, for more information enable the debug flag",
|
|
1308
|
+
suppress_log=suppress_log or False,
|
|
1309
|
+
)
|
|
1310
|
+
raise e
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
class TransactionComposer:
|
|
1314
|
+
"""A class for composing and managing Algorand transactions.
|
|
1315
|
+
|
|
1316
|
+
Provides a high-level interface for building and executing transaction groups using the Algosdk library.
|
|
1317
|
+
Supports various transaction types including payments, asset operations, application calls, and key registrations.
|
|
1318
|
+
|
|
1319
|
+
:param algod: An instance of AlgodClient used to get suggested params and send transactions
|
|
1320
|
+
:param get_signer: A function that takes an address and returns a TransactionSigner for that address
|
|
1321
|
+
:param get_suggested_params: Optional function to get suggested transaction parameters,
|
|
1322
|
+
defaults to using algod.suggested_params()
|
|
1323
|
+
:param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10
|
|
1324
|
+
:param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None
|
|
1325
|
+
"""
|
|
1326
|
+
|
|
1327
|
+
def __init__(
|
|
1328
|
+
self,
|
|
1329
|
+
algod: AlgodClient,
|
|
1330
|
+
get_signer: Callable[[str], TransactionSigner],
|
|
1331
|
+
get_suggested_params: Callable[[], algosdk.transaction.SuggestedParams] | None = None,
|
|
1332
|
+
default_validity_window: int | None = None,
|
|
1333
|
+
app_manager: AppManager | None = None,
|
|
1334
|
+
):
|
|
1335
|
+
# Map of transaction index in the atc to a max logical fee.
|
|
1336
|
+
# This is set using the value of either maxFee or staticFee.
|
|
1337
|
+
self._txn_max_fees: dict[int, AlgoAmount] = {}
|
|
1338
|
+
self._txns: list[TransactionWithSigner | TxnParams | AtomicTransactionComposer] = []
|
|
1339
|
+
self._atc: AtomicTransactionComposer = AtomicTransactionComposer()
|
|
1340
|
+
self._algod: AlgodClient = algod
|
|
1341
|
+
self._default_get_send_params = lambda: self._algod.suggested_params()
|
|
1342
|
+
self._get_suggested_params = get_suggested_params or self._default_get_send_params
|
|
1343
|
+
self._get_signer: Callable[[str], TransactionSigner] = get_signer
|
|
1344
|
+
self._default_validity_window: int = default_validity_window or 10
|
|
1345
|
+
self._default_validity_window_is_explicit: bool = default_validity_window is not None
|
|
1346
|
+
self._app_manager = app_manager or AppManager(algod)
|
|
1347
|
+
|
|
1348
|
+
def add_transaction(
|
|
1349
|
+
self, transaction: algosdk.transaction.Transaction, signer: TransactionSigner | None = None
|
|
1350
|
+
) -> TransactionComposer:
|
|
1351
|
+
"""Add a raw transaction to the composer.
|
|
1352
|
+
|
|
1353
|
+
:param transaction: The transaction to add
|
|
1354
|
+
:param signer: Optional transaction signer, defaults to getting signer from transaction sender
|
|
1355
|
+
:return: The transaction composer instance for chaining
|
|
1356
|
+
"""
|
|
1357
|
+
self._txns.append(TransactionWithSigner(txn=transaction, signer=signer or self._get_signer(transaction.sender)))
|
|
1358
|
+
return self
|
|
1359
|
+
|
|
1360
|
+
def add_payment(self, params: PaymentParams) -> TransactionComposer:
|
|
1361
|
+
"""Add a payment transaction.
|
|
1362
|
+
|
|
1363
|
+
:param params: The payment transaction parameters
|
|
1364
|
+
:return: The transaction composer instance for chaining
|
|
1365
|
+
"""
|
|
1366
|
+
self._txns.append(params)
|
|
1367
|
+
return self
|
|
1368
|
+
|
|
1369
|
+
def add_asset_create(self, params: AssetCreateParams) -> TransactionComposer:
|
|
1370
|
+
"""Add an asset creation transaction.
|
|
1371
|
+
|
|
1372
|
+
:param params: The asset creation parameters
|
|
1373
|
+
:return: The transaction composer instance for chaining
|
|
1374
|
+
"""
|
|
1375
|
+
self._txns.append(params)
|
|
1376
|
+
return self
|
|
1377
|
+
|
|
1378
|
+
def add_asset_config(self, params: AssetConfigParams) -> TransactionComposer:
|
|
1379
|
+
"""Add an asset configuration transaction.
|
|
1380
|
+
|
|
1381
|
+
:param params: The asset configuration parameters
|
|
1382
|
+
:return: The transaction composer instance for chaining
|
|
1383
|
+
"""
|
|
1384
|
+
self._txns.append(params)
|
|
1385
|
+
return self
|
|
1386
|
+
|
|
1387
|
+
def add_asset_freeze(self, params: AssetFreezeParams) -> TransactionComposer:
|
|
1388
|
+
"""Add an asset freeze transaction.
|
|
1389
|
+
|
|
1390
|
+
:param params: The asset freeze parameters
|
|
1391
|
+
:return: The transaction composer instance for chaining
|
|
1392
|
+
"""
|
|
1393
|
+
self._txns.append(params)
|
|
1394
|
+
return self
|
|
1395
|
+
|
|
1396
|
+
def add_asset_destroy(self, params: AssetDestroyParams) -> TransactionComposer:
|
|
1397
|
+
"""Add an asset destruction transaction.
|
|
1398
|
+
|
|
1399
|
+
:param params: The asset destruction parameters
|
|
1400
|
+
:return: The transaction composer instance for chaining
|
|
1401
|
+
"""
|
|
1402
|
+
self._txns.append(params)
|
|
1403
|
+
return self
|
|
1404
|
+
|
|
1405
|
+
def add_asset_transfer(self, params: AssetTransferParams) -> TransactionComposer:
|
|
1406
|
+
"""Add an asset transfer transaction.
|
|
1407
|
+
|
|
1408
|
+
:param params: The asset transfer parameters
|
|
1409
|
+
:return: The transaction composer instance for chaining
|
|
1410
|
+
"""
|
|
1411
|
+
self._txns.append(params)
|
|
1412
|
+
return self
|
|
1413
|
+
|
|
1414
|
+
def add_asset_opt_in(self, params: AssetOptInParams) -> TransactionComposer:
|
|
1415
|
+
"""Add an asset opt-in transaction.
|
|
1416
|
+
|
|
1417
|
+
:param params: The asset opt-in parameters
|
|
1418
|
+
:return: The transaction composer instance for chaining
|
|
1419
|
+
"""
|
|
1420
|
+
self._txns.append(params)
|
|
1421
|
+
return self
|
|
1422
|
+
|
|
1423
|
+
def add_asset_opt_out(self, params: AssetOptOutParams) -> TransactionComposer:
|
|
1424
|
+
"""Add an asset opt-out transaction.
|
|
1425
|
+
|
|
1426
|
+
:param params: The asset opt-out parameters
|
|
1427
|
+
:return: The transaction composer instance for chaining
|
|
1428
|
+
"""
|
|
1429
|
+
self._txns.append(params)
|
|
1430
|
+
return self
|
|
1431
|
+
|
|
1432
|
+
def add_app_create(self, params: AppCreateParams) -> TransactionComposer:
|
|
1433
|
+
"""Add an application creation transaction.
|
|
1434
|
+
|
|
1435
|
+
:param params: The application creation parameters
|
|
1436
|
+
:return: The transaction composer instance for chaining
|
|
1437
|
+
"""
|
|
1438
|
+
self._txns.append(params)
|
|
1439
|
+
return self
|
|
1440
|
+
|
|
1441
|
+
def add_app_update(self, params: AppUpdateParams) -> TransactionComposer:
|
|
1442
|
+
"""Add an application update transaction.
|
|
1443
|
+
|
|
1444
|
+
:param params: The application update parameters
|
|
1445
|
+
:return: The transaction composer instance for chaining
|
|
1446
|
+
"""
|
|
1447
|
+
self._txns.append(params)
|
|
1448
|
+
return self
|
|
1449
|
+
|
|
1450
|
+
def add_app_delete(self, params: AppDeleteParams) -> TransactionComposer:
|
|
1451
|
+
"""Add an application deletion transaction.
|
|
1452
|
+
|
|
1453
|
+
:param params: The application deletion parameters
|
|
1454
|
+
:return: The transaction composer instance for chaining
|
|
1455
|
+
"""
|
|
1456
|
+
self._txns.append(params)
|
|
1457
|
+
return self
|
|
1458
|
+
|
|
1459
|
+
def add_app_call(self, params: AppCallParams) -> TransactionComposer:
|
|
1460
|
+
"""Add an application call transaction.
|
|
1461
|
+
|
|
1462
|
+
:param params: The application call parameters
|
|
1463
|
+
:return: The transaction composer instance for chaining
|
|
1464
|
+
"""
|
|
1465
|
+
self._txns.append(params)
|
|
1466
|
+
return self
|
|
1467
|
+
|
|
1468
|
+
def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> TransactionComposer:
|
|
1469
|
+
"""Add an application creation method call transaction.
|
|
1470
|
+
|
|
1471
|
+
:param params: The application creation method call parameters
|
|
1472
|
+
:return: The transaction composer instance for chaining
|
|
1473
|
+
"""
|
|
1474
|
+
self._txns.append(params)
|
|
1475
|
+
return self
|
|
1476
|
+
|
|
1477
|
+
def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> TransactionComposer:
|
|
1478
|
+
"""Add an application update method call transaction.
|
|
1479
|
+
|
|
1480
|
+
:param params: The application update method call parameters
|
|
1481
|
+
:return: The transaction composer instance for chaining
|
|
1482
|
+
"""
|
|
1483
|
+
self._txns.append(params)
|
|
1484
|
+
return self
|
|
1485
|
+
|
|
1486
|
+
def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> TransactionComposer:
|
|
1487
|
+
"""Add an application deletion method call transaction.
|
|
1488
|
+
|
|
1489
|
+
:param params: The application deletion method call parameters
|
|
1490
|
+
:return: The transaction composer instance for chaining
|
|
1491
|
+
"""
|
|
1492
|
+
self._txns.append(params)
|
|
1493
|
+
return self
|
|
1494
|
+
|
|
1495
|
+
def add_app_call_method_call(self, params: AppCallMethodCallParams) -> TransactionComposer:
|
|
1496
|
+
"""Add an application call method call transaction.
|
|
1497
|
+
|
|
1498
|
+
:param params: The application call method call parameters
|
|
1499
|
+
:return: The transaction composer instance for chaining
|
|
1500
|
+
"""
|
|
1501
|
+
self._txns.append(params)
|
|
1502
|
+
return self
|
|
1503
|
+
|
|
1504
|
+
def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> TransactionComposer:
|
|
1505
|
+
"""Add an online key registration transaction.
|
|
1506
|
+
|
|
1507
|
+
:param params: The online key registration parameters
|
|
1508
|
+
:return: The transaction composer instance for chaining
|
|
1509
|
+
"""
|
|
1510
|
+
self._txns.append(params)
|
|
1511
|
+
return self
|
|
1512
|
+
|
|
1513
|
+
def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> TransactionComposer:
|
|
1514
|
+
"""Add an offline key registration transaction.
|
|
1515
|
+
|
|
1516
|
+
:param params: The offline key registration parameters
|
|
1517
|
+
:return: The transaction composer instance for chaining
|
|
1518
|
+
"""
|
|
1519
|
+
self._txns.append(params)
|
|
1520
|
+
return self
|
|
1521
|
+
|
|
1522
|
+
def add_atc(self, atc: AtomicTransactionComposer) -> TransactionComposer:
|
|
1523
|
+
"""Add an existing AtomicTransactionComposer's transactions.
|
|
1524
|
+
|
|
1525
|
+
:param atc: The AtomicTransactionComposer to add
|
|
1526
|
+
:return: The transaction composer instance for chaining
|
|
1527
|
+
"""
|
|
1528
|
+
self._txns.append(atc)
|
|
1529
|
+
return self
|
|
1530
|
+
|
|
1531
|
+
def count(self) -> int:
|
|
1532
|
+
"""Get the total number of transactions.
|
|
1533
|
+
|
|
1534
|
+
:return: The number of transactions
|
|
1535
|
+
"""
|
|
1536
|
+
return len(self.build_transactions().transactions)
|
|
1537
|
+
|
|
1538
|
+
def build(self) -> TransactionComposerBuildResult:
|
|
1539
|
+
"""Build the transaction group.
|
|
1540
|
+
|
|
1541
|
+
:return: The built transaction group result
|
|
1542
|
+
"""
|
|
1543
|
+
if self._atc.get_status() == algosdk.atomic_transaction_composer.AtomicTransactionComposerStatus.BUILDING:
|
|
1544
|
+
suggested_params = self._get_suggested_params()
|
|
1545
|
+
txn_with_signers: list[TransactionWithSignerAndContext] = []
|
|
1546
|
+
|
|
1547
|
+
for txn in self._txns:
|
|
1548
|
+
txn_with_signers.extend(self._build_txn(txn, suggested_params))
|
|
1549
|
+
|
|
1550
|
+
for ts in txn_with_signers:
|
|
1551
|
+
self._atc.add_transaction(ts)
|
|
1552
|
+
if ts.context.abi_method:
|
|
1553
|
+
self._atc.method_dict[len(self._atc.txn_list) - 1] = ts.context.abi_method
|
|
1554
|
+
if ts.context.max_fee:
|
|
1555
|
+
self._txn_max_fees[len(self._atc.txn_list) - 1] = ts.context.max_fee
|
|
1556
|
+
|
|
1557
|
+
return TransactionComposerBuildResult(
|
|
1558
|
+
atc=self._atc,
|
|
1559
|
+
transactions=self._atc.build_group(),
|
|
1560
|
+
method_calls=self._atc.method_dict,
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
def rebuild(self) -> TransactionComposerBuildResult:
|
|
1564
|
+
"""Rebuild the transaction group from scratch.
|
|
1565
|
+
|
|
1566
|
+
:return: The rebuilt transaction group result
|
|
1567
|
+
"""
|
|
1568
|
+
self._atc = AtomicTransactionComposer()
|
|
1569
|
+
return self.build()
|
|
1570
|
+
|
|
1571
|
+
def build_transactions(self) -> BuiltTransactions:
|
|
1572
|
+
"""Build and return the transactions without executing them.
|
|
1573
|
+
|
|
1574
|
+
:return: The built transactions result
|
|
1575
|
+
"""
|
|
1576
|
+
suggested_params = self._get_suggested_params()
|
|
1577
|
+
|
|
1578
|
+
transactions: list[algosdk.transaction.Transaction] = []
|
|
1579
|
+
method_calls: dict[int, Method] = {}
|
|
1580
|
+
signers: dict[int, TransactionSigner] = {}
|
|
1581
|
+
|
|
1582
|
+
idx = 0
|
|
1583
|
+
|
|
1584
|
+
for txn in self._txns:
|
|
1585
|
+
txn_with_signers: list[TransactionWithSigner] = []
|
|
1586
|
+
|
|
1587
|
+
if isinstance(txn, MethodCallParams):
|
|
1588
|
+
txn_with_signers.extend(self._build_method_call(txn, suggested_params))
|
|
1589
|
+
else:
|
|
1590
|
+
txn_with_signers.extend(self._build_txn(txn, suggested_params))
|
|
1591
|
+
|
|
1592
|
+
for ts in txn_with_signers:
|
|
1593
|
+
transactions.append(ts.txn)
|
|
1594
|
+
if ts.signer and ts.signer != NULL_SIGNER:
|
|
1595
|
+
signers[idx] = ts.signer
|
|
1596
|
+
if isinstance(ts, TransactionWithSignerAndContext) and ts.context.abi_method:
|
|
1597
|
+
method_calls[idx] = ts.context.abi_method
|
|
1598
|
+
idx += 1
|
|
1599
|
+
|
|
1600
|
+
return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
|
|
1601
|
+
|
|
1602
|
+
@deprecated("Use send() instead")
|
|
1603
|
+
def execute(
|
|
1604
|
+
self,
|
|
1605
|
+
*,
|
|
1606
|
+
max_rounds_to_wait: int | None = None,
|
|
1607
|
+
) -> SendAtomicTransactionComposerResults:
|
|
1608
|
+
return self.send(SendParams(max_rounds_to_wait=max_rounds_to_wait))
|
|
1609
|
+
|
|
1610
|
+
def send(
|
|
1611
|
+
self,
|
|
1612
|
+
params: SendParams | None = None,
|
|
1613
|
+
) -> SendAtomicTransactionComposerResults:
|
|
1614
|
+
"""Send the transaction group to the network.
|
|
1615
|
+
|
|
1616
|
+
:param params: Parameters for the send operation
|
|
1617
|
+
:return: The transaction send results
|
|
1618
|
+
:raises Exception: If the transaction fails
|
|
1619
|
+
"""
|
|
1620
|
+
group = self.build().transactions
|
|
1621
|
+
|
|
1622
|
+
if not params:
|
|
1623
|
+
has_app_call = any(isinstance(txn.txn, ApplicationCallTxn) for txn in group)
|
|
1624
|
+
params = SendParams() if has_app_call else SendParams()
|
|
1625
|
+
|
|
1626
|
+
cover_app_call_inner_transaction_fees = params.get("cover_app_call_inner_transaction_fees")
|
|
1627
|
+
populate_app_call_resources = params.get("populate_app_call_resources")
|
|
1628
|
+
wait_rounds = params.get("max_rounds_to_wait")
|
|
1629
|
+
sp = self._get_suggested_params() if not wait_rounds or cover_app_call_inner_transaction_fees else None
|
|
1630
|
+
|
|
1631
|
+
if wait_rounds is None:
|
|
1632
|
+
last_round = max(txn.txn.last_valid_round for txn in group)
|
|
1633
|
+
assert sp is not None
|
|
1634
|
+
first_round = sp.first
|
|
1635
|
+
wait_rounds = last_round - first_round + 1
|
|
1636
|
+
|
|
1637
|
+
try:
|
|
1638
|
+
return send_atomic_transaction_composer(
|
|
1639
|
+
self._atc,
|
|
1640
|
+
self._algod,
|
|
1641
|
+
max_rounds_to_wait=wait_rounds,
|
|
1642
|
+
suppress_log=params.get("suppress_log"),
|
|
1643
|
+
populate_app_call_resources=populate_app_call_resources,
|
|
1644
|
+
cover_app_call_inner_transaction_fees=cover_app_call_inner_transaction_fees,
|
|
1645
|
+
additional_atc_context=AdditionalAtcContext(
|
|
1646
|
+
suggested_params=sp,
|
|
1647
|
+
max_fees=self._txn_max_fees,
|
|
1648
|
+
),
|
|
1649
|
+
)
|
|
1650
|
+
except algosdk.error.AlgodHTTPError as e:
|
|
1651
|
+
raise Exception(f"Transaction failed: {e}") from e
|
|
1652
|
+
|
|
1653
|
+
def _handle_simulate_error(self, simulate_response: SimulateAtomicTransactionResponse) -> None:
|
|
1654
|
+
# const failedGroup = simulateResponse?.txnGroups[0]
|
|
1655
|
+
failed_group = simulate_response.simulate_response.get("txn-groups", [{}])[0]
|
|
1656
|
+
failure_message = failed_group.get("failure-message")
|
|
1657
|
+
failed_at = [str(x) for x in failed_group.get("failed-at", [])]
|
|
1658
|
+
if failure_message:
|
|
1659
|
+
error_message = (
|
|
1660
|
+
f"Transaction failed at transaction(s) {', '.join(failed_at) if failed_at else 'N/A'} in the group. "
|
|
1661
|
+
f"{failure_message}"
|
|
1662
|
+
)
|
|
1663
|
+
raise Exception(error_message)
|
|
1664
|
+
|
|
1665
|
+
def simulate(
|
|
1666
|
+
self,
|
|
1667
|
+
allow_more_logs: bool | None = None,
|
|
1668
|
+
allow_empty_signatures: bool | None = None,
|
|
1669
|
+
allow_unnamed_resources: bool | None = None,
|
|
1670
|
+
extra_opcode_budget: int | None = None,
|
|
1671
|
+
exec_trace_config: SimulateTraceConfig | None = None,
|
|
1672
|
+
simulation_round: int | None = None,
|
|
1673
|
+
skip_signatures: bool | None = None,
|
|
1674
|
+
) -> SendAtomicTransactionComposerResults:
|
|
1675
|
+
"""Simulate transaction group execution with configurable validation rules.
|
|
1676
|
+
|
|
1677
|
+
:param allow_more_logs: Whether to allow more logs than the standard limit
|
|
1678
|
+
:param allow_empty_signatures: Whether to allow transactions with empty signatures
|
|
1679
|
+
:param allow_unnamed_resources: Whether to allow unnamed resources
|
|
1680
|
+
:param extra_opcode_budget: Additional opcode budget to allocate
|
|
1681
|
+
:param exec_trace_config: Configuration for execution tracing
|
|
1682
|
+
:param simulation_round: Round number to simulate at
|
|
1683
|
+
:param skip_signatures: Whether to skip signature validation
|
|
1684
|
+
:return: The simulation results
|
|
1685
|
+
"""
|
|
1686
|
+
from algokit_utils._debugging import simulate_and_persist_response, simulate_response
|
|
1687
|
+
|
|
1688
|
+
atc = AtomicTransactionComposer() if skip_signatures else self._atc
|
|
1689
|
+
|
|
1690
|
+
if skip_signatures:
|
|
1691
|
+
allow_empty_signatures = True
|
|
1692
|
+
transactions = self.build_transactions()
|
|
1693
|
+
for txn in transactions.transactions:
|
|
1694
|
+
atc.add_transaction(TransactionWithSigner(txn=txn, signer=NULL_SIGNER))
|
|
1695
|
+
atc.method_dict = transactions.method_calls
|
|
1696
|
+
else:
|
|
1697
|
+
self.build()
|
|
1698
|
+
|
|
1699
|
+
if config.debug and config.project_root and config.trace_all:
|
|
1700
|
+
response = simulate_and_persist_response(
|
|
1701
|
+
atc,
|
|
1702
|
+
config.project_root,
|
|
1703
|
+
self._algod,
|
|
1704
|
+
config.trace_buffer_size_mb,
|
|
1705
|
+
allow_more_logs,
|
|
1706
|
+
allow_empty_signatures,
|
|
1707
|
+
allow_unnamed_resources,
|
|
1708
|
+
extra_opcode_budget,
|
|
1709
|
+
exec_trace_config,
|
|
1710
|
+
simulation_round,
|
|
1711
|
+
)
|
|
1712
|
+
self._handle_simulate_error(response)
|
|
1713
|
+
return SendAtomicTransactionComposerResults(
|
|
1714
|
+
confirmations=response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][
|
|
1715
|
+
"txn-results"
|
|
1716
|
+
],
|
|
1717
|
+
transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list],
|
|
1718
|
+
tx_ids=response.tx_ids,
|
|
1719
|
+
group_id=atc.txn_list[-1].txn.group or "",
|
|
1720
|
+
simulate_response=response.simulate_response,
|
|
1721
|
+
returns=[ABIReturn(r) for r in response.abi_results],
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
response = simulate_response(
|
|
1725
|
+
atc,
|
|
1726
|
+
self._algod,
|
|
1727
|
+
allow_more_logs,
|
|
1728
|
+
allow_empty_signatures,
|
|
1729
|
+
allow_unnamed_resources,
|
|
1730
|
+
extra_opcode_budget,
|
|
1731
|
+
exec_trace_config,
|
|
1732
|
+
simulation_round,
|
|
1733
|
+
)
|
|
1734
|
+
self._handle_simulate_error(response)
|
|
1735
|
+
confirmation_results = response.simulate_response.get("txn-groups", [{"txn-results": [{"txn-result": {}}]}])[0][
|
|
1736
|
+
"txn-results"
|
|
1737
|
+
]
|
|
1738
|
+
|
|
1739
|
+
return SendAtomicTransactionComposerResults(
|
|
1740
|
+
confirmations=[txn["txn-result"] for txn in confirmation_results],
|
|
1741
|
+
transactions=[TransactionWrapper(txn.txn) for txn in atc.txn_list],
|
|
1742
|
+
tx_ids=response.tx_ids,
|
|
1743
|
+
group_id=atc.txn_list[-1].txn.group or "",
|
|
1744
|
+
simulate_response=response.simulate_response,
|
|
1745
|
+
returns=[ABIReturn(r) for r in response.abi_results],
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
@staticmethod
|
|
1749
|
+
def arc2_note(note: Arc2TransactionNote) -> bytes:
|
|
1750
|
+
"""Create an encoded transaction note that follows the ARC-2 spec.
|
|
1751
|
+
|
|
1752
|
+
https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md
|
|
1753
|
+
|
|
1754
|
+
:param note: The ARC-2 note to encode
|
|
1755
|
+
:return: The encoded note bytes
|
|
1756
|
+
:raises ValueError: If the dapp_name is invalid
|
|
1757
|
+
"""
|
|
1758
|
+
|
|
1759
|
+
pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$"
|
|
1760
|
+
if not re.match(pattern, note["dapp_name"]):
|
|
1761
|
+
raise ValueError(
|
|
1762
|
+
"dapp_name must be 5-32 chars, start with alphanumeric, "
|
|
1763
|
+
"and contain only alphanumeric, _, /, @, ., or -"
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
data = note["data"]
|
|
1767
|
+
if note["format"] == "j" and isinstance(data, (dict | list)):
|
|
1768
|
+
# Ensure JSON data uses double quotes
|
|
1769
|
+
data = json.dumps(data)
|
|
1770
|
+
|
|
1771
|
+
arc2_payload = f"{note['dapp_name']}:{note['format']}{data}"
|
|
1772
|
+
return arc2_payload.encode("utf-8")
|
|
1773
|
+
|
|
1774
|
+
def _build_atc(self, atc: AtomicTransactionComposer) -> list[TransactionWithSignerAndContext]:
|
|
1775
|
+
group = atc.build_group()
|
|
1776
|
+
|
|
1777
|
+
txn_with_signers = []
|
|
1778
|
+
for idx, ts in enumerate(group):
|
|
1779
|
+
ts.txn.group = None
|
|
1780
|
+
if atc.method_dict.get(idx):
|
|
1781
|
+
txn_with_signers.append(
|
|
1782
|
+
TransactionWithSignerAndContext(
|
|
1783
|
+
txn=ts.txn,
|
|
1784
|
+
signer=ts.signer,
|
|
1785
|
+
context=TransactionContext(abi_method=atc.method_dict.get(idx)),
|
|
1786
|
+
)
|
|
1787
|
+
)
|
|
1788
|
+
else:
|
|
1789
|
+
txn_with_signers.append(
|
|
1790
|
+
TransactionWithSignerAndContext(
|
|
1791
|
+
txn=ts.txn,
|
|
1792
|
+
signer=ts.signer,
|
|
1793
|
+
context=TransactionContext(abi_method=None),
|
|
1794
|
+
)
|
|
1795
|
+
)
|
|
1796
|
+
|
|
1797
|
+
return txn_with_signers
|
|
1798
|
+
|
|
1799
|
+
def _common_txn_build_step( # noqa: C901
|
|
1800
|
+
self,
|
|
1801
|
+
build_txn: Callable[[dict], algosdk.transaction.Transaction],
|
|
1802
|
+
params: _CommonTxnParams,
|
|
1803
|
+
txn_params: dict,
|
|
1804
|
+
) -> TransactionWithContext:
|
|
1805
|
+
# Clone suggested params
|
|
1806
|
+
txn_params["sp"] = (
|
|
1807
|
+
algosdk.transaction.SuggestedParams(**txn_params["sp"].__dict__) if "sp" in txn_params else None
|
|
1808
|
+
)
|
|
1809
|
+
|
|
1810
|
+
if params.lease:
|
|
1811
|
+
txn_params["lease"] = _encode_lease(params.lease)
|
|
1812
|
+
if params.rekey_to:
|
|
1813
|
+
txn_params["rekey_to"] = params.rekey_to
|
|
1814
|
+
if params.note:
|
|
1815
|
+
txn_params["note"] = params.note
|
|
1816
|
+
|
|
1817
|
+
if txn_params["sp"]:
|
|
1818
|
+
if params.first_valid_round:
|
|
1819
|
+
txn_params["sp"].first = params.first_valid_round
|
|
1820
|
+
|
|
1821
|
+
if params.last_valid_round:
|
|
1822
|
+
txn_params["sp"].last = params.last_valid_round
|
|
1823
|
+
else:
|
|
1824
|
+
# If the validity window isn't set in this transaction or by default and we are pointing at
|
|
1825
|
+
# LocalNet set a bigger window to avoid dead transactions
|
|
1826
|
+
from algokit_utils.clients import ClientManager
|
|
1827
|
+
|
|
1828
|
+
is_localnet = ClientManager.genesis_id_is_localnet(txn_params["sp"].gen)
|
|
1829
|
+
window = params.validity_window or (
|
|
1830
|
+
1000
|
|
1831
|
+
if is_localnet and not self._default_validity_window_is_explicit
|
|
1832
|
+
else self._default_validity_window
|
|
1833
|
+
)
|
|
1834
|
+
txn_params["sp"].last = txn_params["sp"].first + window
|
|
1835
|
+
|
|
1836
|
+
if params.static_fee is not None and txn_params["sp"]:
|
|
1837
|
+
txn_params["sp"].fee = params.static_fee.micro_algos
|
|
1838
|
+
txn_params["sp"].flat_fee = True
|
|
1839
|
+
|
|
1840
|
+
if isinstance(txn_params.get("method"), Arc56Method):
|
|
1841
|
+
txn_params["method"] = txn_params["method"].to_abi_method()
|
|
1842
|
+
|
|
1843
|
+
txn = build_txn(txn_params)
|
|
1844
|
+
|
|
1845
|
+
if params.extra_fee:
|
|
1846
|
+
txn.fee += params.extra_fee.micro_algos
|
|
1847
|
+
|
|
1848
|
+
if params.max_fee and txn.fee > params.max_fee.micro_algos:
|
|
1849
|
+
raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}")
|
|
1850
|
+
use_max_fee = params.max_fee and params.max_fee.micro_algo > (
|
|
1851
|
+
params.static_fee.micro_algo if params.static_fee else 0
|
|
1852
|
+
)
|
|
1853
|
+
logical_max_fee = params.max_fee if use_max_fee else params.static_fee
|
|
1854
|
+
|
|
1855
|
+
return TransactionWithContext(
|
|
1856
|
+
txn=txn,
|
|
1857
|
+
context=TransactionContext(max_fee=logical_max_fee),
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
def _build_method_call( # noqa: C901, PLR0912, PLR0915
|
|
1861
|
+
self, params: MethodCallParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
1862
|
+
) -> list[TransactionWithSignerAndContext]:
|
|
1863
|
+
method_args: list[ABIValue | TransactionWithSigner] = []
|
|
1864
|
+
txns_for_group: list[TransactionWithSignerAndContext] = []
|
|
1865
|
+
|
|
1866
|
+
if params.args:
|
|
1867
|
+
for arg in reversed(params.args):
|
|
1868
|
+
if arg is None and len(txns_for_group) > 0:
|
|
1869
|
+
# Pull last transaction from group as placeholder
|
|
1870
|
+
placeholder_transaction = txns_for_group.pop()
|
|
1871
|
+
method_args.append(placeholder_transaction)
|
|
1872
|
+
continue
|
|
1873
|
+
if self._is_abi_value(arg):
|
|
1874
|
+
method_args.append(arg)
|
|
1875
|
+
continue
|
|
1876
|
+
|
|
1877
|
+
if isinstance(arg, TransactionWithSigner):
|
|
1878
|
+
method_args.append(arg)
|
|
1879
|
+
continue
|
|
1880
|
+
|
|
1881
|
+
if isinstance(arg, algosdk.transaction.Transaction):
|
|
1882
|
+
# Wrap in TransactionWithSigner
|
|
1883
|
+
signer = (
|
|
1884
|
+
params.signer.signer
|
|
1885
|
+
if isinstance(params.signer, TransactionSignerAccountProtocol)
|
|
1886
|
+
else params.signer
|
|
1887
|
+
)
|
|
1888
|
+
method_args.append(
|
|
1889
|
+
TransactionWithSignerAndContext(
|
|
1890
|
+
txn=arg,
|
|
1891
|
+
signer=signer if signer is not None else self._get_signer(params.sender),
|
|
1892
|
+
context=TransactionContext(abi_method=None),
|
|
1893
|
+
)
|
|
1894
|
+
)
|
|
1895
|
+
continue
|
|
1896
|
+
match arg:
|
|
1897
|
+
case (
|
|
1898
|
+
AppCreateMethodCallParams()
|
|
1899
|
+
| AppCallMethodCallParams()
|
|
1900
|
+
| AppUpdateMethodCallParams()
|
|
1901
|
+
| AppDeleteMethodCallParams()
|
|
1902
|
+
):
|
|
1903
|
+
temp_txn_with_signers = self._build_method_call(arg, suggested_params)
|
|
1904
|
+
# Add all transactions except the last one in reverse order
|
|
1905
|
+
txns_for_group.extend(temp_txn_with_signers[:-1])
|
|
1906
|
+
# Add the last transaction to method_args
|
|
1907
|
+
method_args.append(temp_txn_with_signers[-1])
|
|
1908
|
+
continue
|
|
1909
|
+
case AppCallParams():
|
|
1910
|
+
txn = self._build_app_call(arg, suggested_params)
|
|
1911
|
+
case PaymentParams():
|
|
1912
|
+
txn = self._build_payment(arg, suggested_params)
|
|
1913
|
+
case AssetOptInParams():
|
|
1914
|
+
txn = self._build_asset_transfer(
|
|
1915
|
+
AssetTransferParams(**arg.__dict__, receiver=arg.sender, amount=0), suggested_params
|
|
1916
|
+
)
|
|
1917
|
+
case AssetCreateParams():
|
|
1918
|
+
txn = self._build_asset_create(arg, suggested_params)
|
|
1919
|
+
case AssetConfigParams():
|
|
1920
|
+
txn = self._build_asset_config(arg, suggested_params)
|
|
1921
|
+
case AssetDestroyParams():
|
|
1922
|
+
txn = self._build_asset_destroy(arg, suggested_params)
|
|
1923
|
+
case AssetFreezeParams():
|
|
1924
|
+
txn = self._build_asset_freeze(arg, suggested_params)
|
|
1925
|
+
case AssetTransferParams():
|
|
1926
|
+
txn = self._build_asset_transfer(arg, suggested_params)
|
|
1927
|
+
case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams():
|
|
1928
|
+
txn = self._build_key_reg(arg, suggested_params)
|
|
1929
|
+
case _:
|
|
1930
|
+
raise ValueError(f"Unsupported method arg transaction type: {arg!s}")
|
|
1931
|
+
|
|
1932
|
+
signer = (
|
|
1933
|
+
params.signer.signer
|
|
1934
|
+
if isinstance(params.signer, TransactionSignerAccountProtocol)
|
|
1935
|
+
else params.signer
|
|
1936
|
+
)
|
|
1937
|
+
method_args.append(
|
|
1938
|
+
TransactionWithSignerAndContext(
|
|
1939
|
+
txn=txn.txn,
|
|
1940
|
+
signer=signer or self._get_signer(params.sender),
|
|
1941
|
+
context=TransactionContext(abi_method=params.method),
|
|
1942
|
+
)
|
|
1943
|
+
)
|
|
1944
|
+
|
|
1945
|
+
continue
|
|
1946
|
+
|
|
1947
|
+
method_atc = AtomicTransactionComposer()
|
|
1948
|
+
max_fees: dict[int, AlgoAmount] = {}
|
|
1949
|
+
|
|
1950
|
+
# Process in reverse order
|
|
1951
|
+
for arg in reversed(txns_for_group):
|
|
1952
|
+
atc_index = method_atc.get_tx_count() - 1
|
|
1953
|
+
|
|
1954
|
+
if isinstance(arg, TransactionWithSignerAndContext) and arg.context:
|
|
1955
|
+
if arg.context.abi_method:
|
|
1956
|
+
method_atc.method_dict[atc_index] = arg.context.abi_method
|
|
1957
|
+
|
|
1958
|
+
if arg.context.max_fee is not None:
|
|
1959
|
+
max_fees[atc_index] = arg.context.max_fee
|
|
1960
|
+
|
|
1961
|
+
# Process method args that are transactions with ABI method info
|
|
1962
|
+
for i, arg in enumerate(reversed([a for a in method_args if isinstance(a, TransactionWithSignerAndContext)])):
|
|
1963
|
+
atc_index = method_atc.get_tx_count() + i
|
|
1964
|
+
if arg.context:
|
|
1965
|
+
if arg.context.abi_method:
|
|
1966
|
+
method_atc.method_dict[atc_index] = arg.context.abi_method
|
|
1967
|
+
if arg.context.max_fee is not None:
|
|
1968
|
+
max_fees[atc_index] = arg.context.max_fee
|
|
1969
|
+
|
|
1970
|
+
app_id = params.app_id or 0
|
|
1971
|
+
approval_program = getattr(params, "approval_program", None)
|
|
1972
|
+
clear_program = getattr(params, "clear_state_program", None)
|
|
1973
|
+
extra_pages = None
|
|
1974
|
+
|
|
1975
|
+
if app_id == 0:
|
|
1976
|
+
extra_pages = getattr(params, "extra_program_pages", None)
|
|
1977
|
+
if extra_pages is None and approval_program is not None:
|
|
1978
|
+
approval_len, clear_len = len(approval_program), len(clear_program or b"")
|
|
1979
|
+
extra_pages = (
|
|
1980
|
+
int(math.floor((approval_len + clear_len) / algosdk.constants.APP_PAGE_MAX_SIZE))
|
|
1981
|
+
if approval_len
|
|
1982
|
+
else 0
|
|
1983
|
+
)
|
|
1984
|
+
|
|
1985
|
+
txn_params = {
|
|
1986
|
+
"app_id": app_id,
|
|
1987
|
+
"method": params.method,
|
|
1988
|
+
"sender": params.sender,
|
|
1989
|
+
"sp": suggested_params,
|
|
1990
|
+
"signer": params.signer
|
|
1991
|
+
if params.signer is not None
|
|
1992
|
+
else self._get_signer(params.sender) or algosdk.atomic_transaction_composer.EmptySigner(),
|
|
1993
|
+
"method_args": list(reversed(method_args)),
|
|
1994
|
+
"on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC,
|
|
1995
|
+
"boxes": [AppManager.get_box_reference(ref) for ref in params.box_references]
|
|
1996
|
+
if params.box_references
|
|
1997
|
+
else None,
|
|
1998
|
+
"foreign_apps": params.app_references,
|
|
1999
|
+
"foreign_assets": params.asset_references,
|
|
2000
|
+
"accounts": params.account_references,
|
|
2001
|
+
"global_schema": algosdk.transaction.StateSchema(
|
|
2002
|
+
num_uints=params.schema.get("global_ints", 0),
|
|
2003
|
+
num_byte_slices=params.schema.get("global_byte_slices", 0),
|
|
2004
|
+
)
|
|
2005
|
+
if params.schema
|
|
2006
|
+
else None,
|
|
2007
|
+
"local_schema": algosdk.transaction.StateSchema(
|
|
2008
|
+
num_uints=params.schema.get("local_ints", 0),
|
|
2009
|
+
num_byte_slices=params.schema.get("local_byte_slices", 0),
|
|
2010
|
+
)
|
|
2011
|
+
if params.schema
|
|
2012
|
+
else None,
|
|
2013
|
+
"approval_program": approval_program,
|
|
2014
|
+
"clear_program": clear_program,
|
|
2015
|
+
"extra_pages": extra_pages,
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
def _add_method_call_and_return_txn(x: dict) -> algosdk.transaction.Transaction:
|
|
2019
|
+
method_atc.add_method_call(**x)
|
|
2020
|
+
return method_atc.build_group()[-1].txn
|
|
2021
|
+
|
|
2022
|
+
result = self._common_txn_build_step(lambda x: _add_method_call_and_return_txn(x), params, txn_params)
|
|
2023
|
+
|
|
2024
|
+
build_atc_resp = self._build_atc(method_atc)
|
|
2025
|
+
response = []
|
|
2026
|
+
for i, v in enumerate(build_atc_resp):
|
|
2027
|
+
max_fee = result.context.max_fee if i == method_atc.get_tx_count() - 1 else max_fees.get(i)
|
|
2028
|
+
context = TransactionContext(abi_method=v.context.abi_method, max_fee=max_fee)
|
|
2029
|
+
response.append(TransactionWithSignerAndContext(txn=v.txn, signer=v.signer, context=context))
|
|
2030
|
+
|
|
2031
|
+
return response
|
|
2032
|
+
|
|
2033
|
+
def _build_payment(
|
|
2034
|
+
self, params: PaymentParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2035
|
+
) -> TransactionWithContext:
|
|
2036
|
+
txn_params = {
|
|
2037
|
+
"sender": params.sender,
|
|
2038
|
+
"sp": suggested_params,
|
|
2039
|
+
"receiver": params.receiver,
|
|
2040
|
+
"amt": params.amount.micro_algos,
|
|
2041
|
+
"close_remainder_to": params.close_remainder_to,
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.PaymentTxn(**x), params, txn_params)
|
|
2045
|
+
|
|
2046
|
+
def _build_asset_create(
|
|
2047
|
+
self, params: AssetCreateParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2048
|
+
) -> TransactionWithContext:
|
|
2049
|
+
txn_params = {
|
|
2050
|
+
"sender": params.sender,
|
|
2051
|
+
"sp": suggested_params,
|
|
2052
|
+
"total": params.total,
|
|
2053
|
+
"default_frozen": params.default_frozen or False,
|
|
2054
|
+
"unit_name": params.unit_name or "",
|
|
2055
|
+
"asset_name": params.asset_name or "",
|
|
2056
|
+
"manager": params.manager,
|
|
2057
|
+
"reserve": params.reserve,
|
|
2058
|
+
"freeze": params.freeze,
|
|
2059
|
+
"clawback": params.clawback,
|
|
2060
|
+
"url": params.url or "",
|
|
2061
|
+
"metadata_hash": params.metadata_hash,
|
|
2062
|
+
"decimals": params.decimals or 0,
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.AssetCreateTxn(**x), params, txn_params)
|
|
2066
|
+
|
|
2067
|
+
def _build_app_call(
|
|
2068
|
+
self,
|
|
2069
|
+
params: AppCallParams | AppUpdateParams | AppCreateParams | AppDeleteParams,
|
|
2070
|
+
suggested_params: algosdk.transaction.SuggestedParams,
|
|
2071
|
+
) -> TransactionWithContext:
|
|
2072
|
+
app_id = getattr(params, "app_id", 0)
|
|
2073
|
+
|
|
2074
|
+
approval_program = None
|
|
2075
|
+
clear_program = None
|
|
2076
|
+
|
|
2077
|
+
if isinstance(params, AppUpdateParams | AppCreateParams):
|
|
2078
|
+
if isinstance(params.approval_program, str):
|
|
2079
|
+
approval_program = self._app_manager.compile_teal(params.approval_program).compiled_base64_to_bytes
|
|
2080
|
+
elif isinstance(params.approval_program, bytes):
|
|
2081
|
+
approval_program = params.approval_program
|
|
2082
|
+
|
|
2083
|
+
if isinstance(params.clear_state_program, str):
|
|
2084
|
+
clear_program = self._app_manager.compile_teal(params.clear_state_program).compiled_base64_to_bytes
|
|
2085
|
+
elif isinstance(params.clear_state_program, bytes):
|
|
2086
|
+
clear_program = params.clear_state_program
|
|
2087
|
+
|
|
2088
|
+
approval_program_len = len(approval_program) if approval_program else 0
|
|
2089
|
+
clear_program_len = len(clear_program) if clear_program else 0
|
|
2090
|
+
|
|
2091
|
+
sdk_params = {
|
|
2092
|
+
"sender": params.sender,
|
|
2093
|
+
"sp": suggested_params,
|
|
2094
|
+
"app_args": params.args,
|
|
2095
|
+
"on_complete": params.on_complete or algosdk.transaction.OnComplete.NoOpOC,
|
|
2096
|
+
"accounts": params.account_references,
|
|
2097
|
+
"foreign_apps": params.app_references,
|
|
2098
|
+
"foreign_assets": params.asset_references,
|
|
2099
|
+
"boxes": params.box_references,
|
|
2100
|
+
"approval_program": approval_program,
|
|
2101
|
+
"clear_program": clear_program,
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
txn_params = {**sdk_params, "index": app_id}
|
|
2105
|
+
|
|
2106
|
+
if not app_id and isinstance(params, AppCreateParams):
|
|
2107
|
+
if not sdk_params["approval_program"] or not sdk_params["clear_program"]:
|
|
2108
|
+
raise ValueError("approval_program and clear_program are required for application creation")
|
|
2109
|
+
|
|
2110
|
+
if not params.schema:
|
|
2111
|
+
raise ValueError("schema is required for application creation")
|
|
2112
|
+
|
|
2113
|
+
txn_params = {
|
|
2114
|
+
**txn_params,
|
|
2115
|
+
"global_schema": algosdk.transaction.StateSchema(
|
|
2116
|
+
num_uints=params.schema.get("global_ints", 0),
|
|
2117
|
+
num_byte_slices=params.schema.get("global_byte_slices", 0),
|
|
2118
|
+
),
|
|
2119
|
+
"local_schema": algosdk.transaction.StateSchema(
|
|
2120
|
+
num_uints=params.schema.get("local_ints", 0),
|
|
2121
|
+
num_byte_slices=params.schema.get("local_byte_slices", 0),
|
|
2122
|
+
),
|
|
2123
|
+
"extra_pages": params.extra_program_pages
|
|
2124
|
+
or math.floor((approval_program_len + clear_program_len) / algosdk.constants.APP_PAGE_MAX_SIZE)
|
|
2125
|
+
if params.extra_program_pages
|
|
2126
|
+
else 0,
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.ApplicationCallTxn(**x), params, txn_params)
|
|
2130
|
+
|
|
2131
|
+
def _build_asset_config(
|
|
2132
|
+
self, params: AssetConfigParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2133
|
+
) -> TransactionWithContext:
|
|
2134
|
+
txn_params = {
|
|
2135
|
+
"sender": params.sender,
|
|
2136
|
+
"sp": suggested_params,
|
|
2137
|
+
"index": params.asset_id,
|
|
2138
|
+
"manager": params.manager,
|
|
2139
|
+
"reserve": params.reserve,
|
|
2140
|
+
"freeze": params.freeze,
|
|
2141
|
+
"clawback": params.clawback,
|
|
2142
|
+
"strict_empty_address_check": False,
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.AssetConfigTxn(**x), params, txn_params)
|
|
2146
|
+
|
|
2147
|
+
def _build_asset_destroy(
|
|
2148
|
+
self, params: AssetDestroyParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2149
|
+
) -> TransactionWithContext:
|
|
2150
|
+
txn_params = {
|
|
2151
|
+
"sender": params.sender,
|
|
2152
|
+
"sp": suggested_params,
|
|
2153
|
+
"index": params.asset_id,
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.AssetDestroyTxn(**x), params, txn_params)
|
|
2157
|
+
|
|
2158
|
+
def _build_asset_freeze(
|
|
2159
|
+
self, params: AssetFreezeParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2160
|
+
) -> TransactionWithContext:
|
|
2161
|
+
txn_params = {
|
|
2162
|
+
"sender": params.sender,
|
|
2163
|
+
"sp": suggested_params,
|
|
2164
|
+
"index": params.asset_id,
|
|
2165
|
+
"target": params.account,
|
|
2166
|
+
"new_freeze_state": params.frozen,
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.AssetFreezeTxn(**x), params, txn_params)
|
|
2170
|
+
|
|
2171
|
+
def _build_asset_transfer(
|
|
2172
|
+
self, params: AssetTransferParams, suggested_params: algosdk.transaction.SuggestedParams
|
|
2173
|
+
) -> TransactionWithContext:
|
|
2174
|
+
txn_params = {
|
|
2175
|
+
"sender": params.sender,
|
|
2176
|
+
"sp": suggested_params,
|
|
2177
|
+
"receiver": params.receiver,
|
|
2178
|
+
"amt": params.amount,
|
|
2179
|
+
"index": params.asset_id,
|
|
2180
|
+
"close_assets_to": params.close_asset_to,
|
|
2181
|
+
"revocation_target": params.clawback_target,
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.AssetTransferTxn(**x), params, txn_params)
|
|
2185
|
+
|
|
2186
|
+
def _build_key_reg(
|
|
2187
|
+
self,
|
|
2188
|
+
params: OnlineKeyRegistrationParams | OfflineKeyRegistrationParams,
|
|
2189
|
+
suggested_params: algosdk.transaction.SuggestedParams,
|
|
2190
|
+
) -> TransactionWithContext:
|
|
2191
|
+
if isinstance(params, OnlineKeyRegistrationParams):
|
|
2192
|
+
txn_params = {
|
|
2193
|
+
"sender": params.sender,
|
|
2194
|
+
"sp": suggested_params,
|
|
2195
|
+
"votekey": params.vote_key,
|
|
2196
|
+
"selkey": params.selection_key,
|
|
2197
|
+
"votefst": params.vote_first,
|
|
2198
|
+
"votelst": params.vote_last,
|
|
2199
|
+
"votekd": params.vote_key_dilution,
|
|
2200
|
+
"rekey_to": params.rekey_to,
|
|
2201
|
+
"nonpart": False,
|
|
2202
|
+
"sprfkey": params.state_proof_key,
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return self._common_txn_build_step(lambda x: algosdk.transaction.KeyregTxn(**x), params, txn_params)
|
|
2206
|
+
|
|
2207
|
+
return self._common_txn_build_step(
|
|
2208
|
+
lambda x: algosdk.transaction.KeyregTxn(**x),
|
|
2209
|
+
params,
|
|
2210
|
+
{
|
|
2211
|
+
"sender": params.sender,
|
|
2212
|
+
"sp": suggested_params,
|
|
2213
|
+
"nonpart": params.prevent_account_from_ever_participating_again,
|
|
2214
|
+
"votekey": None,
|
|
2215
|
+
"selkey": None,
|
|
2216
|
+
"votefst": None,
|
|
2217
|
+
"votelst": None,
|
|
2218
|
+
"votekd": None,
|
|
2219
|
+
},
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
def _is_abi_value(self, x: bool | float | str | bytes | list | TxnParams) -> bool:
|
|
2223
|
+
if isinstance(x, list | tuple):
|
|
2224
|
+
return len(x) == 0 or all(self._is_abi_value(item) for item in x)
|
|
2225
|
+
|
|
2226
|
+
return isinstance(x, bool | int | float | str | bytes)
|
|
2227
|
+
|
|
2228
|
+
def _build_txn( # noqa: C901, PLR0912, PLR0911
|
|
2229
|
+
self,
|
|
2230
|
+
txn: TransactionWithSigner | TxnParams | AtomicTransactionComposer,
|
|
2231
|
+
suggested_params: algosdk.transaction.SuggestedParams,
|
|
2232
|
+
) -> list[TransactionWithSignerAndContext]:
|
|
2233
|
+
match txn:
|
|
2234
|
+
case TransactionWithSigner():
|
|
2235
|
+
return [
|
|
2236
|
+
TransactionWithSignerAndContext(txn=txn.txn, signer=txn.signer, context=TransactionContext.empty())
|
|
2237
|
+
]
|
|
2238
|
+
case AtomicTransactionComposer():
|
|
2239
|
+
return self._build_atc(txn)
|
|
2240
|
+
case algosdk.transaction.Transaction():
|
|
2241
|
+
signer = self._get_signer(txn.sender)
|
|
2242
|
+
return [TransactionWithSignerAndContext(txn=txn, signer=signer, context=TransactionContext.empty())]
|
|
2243
|
+
case (
|
|
2244
|
+
AppCreateMethodCallParams()
|
|
2245
|
+
| AppCallMethodCallParams()
|
|
2246
|
+
| AppUpdateMethodCallParams()
|
|
2247
|
+
| AppDeleteMethodCallParams()
|
|
2248
|
+
):
|
|
2249
|
+
return self._build_method_call(txn, suggested_params)
|
|
2250
|
+
|
|
2251
|
+
signer = txn.signer.signer if isinstance(txn.signer, TransactionSignerAccountProtocol) else txn.signer # type: ignore[assignment]
|
|
2252
|
+
signer = signer or self._get_signer(txn.sender)
|
|
2253
|
+
|
|
2254
|
+
match txn:
|
|
2255
|
+
case PaymentParams():
|
|
2256
|
+
payment = self._build_payment(txn, suggested_params)
|
|
2257
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(payment, signer)]
|
|
2258
|
+
case AssetCreateParams():
|
|
2259
|
+
asset_create = self._build_asset_create(txn, suggested_params)
|
|
2260
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_create, signer)]
|
|
2261
|
+
case AppCallParams() | AppUpdateParams() | AppCreateParams() | AppDeleteParams():
|
|
2262
|
+
app_call = self._build_app_call(txn, suggested_params)
|
|
2263
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(app_call, signer)]
|
|
2264
|
+
case AssetConfigParams():
|
|
2265
|
+
asset_config = self._build_asset_config(txn, suggested_params)
|
|
2266
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_config, signer)]
|
|
2267
|
+
case AssetDestroyParams():
|
|
2268
|
+
asset_destroy = self._build_asset_destroy(txn, suggested_params)
|
|
2269
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_destroy, signer)]
|
|
2270
|
+
case AssetFreezeParams():
|
|
2271
|
+
asset_freeze = self._build_asset_freeze(txn, suggested_params)
|
|
2272
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_freeze, signer)]
|
|
2273
|
+
case AssetTransferParams():
|
|
2274
|
+
asset_transfer = self._build_asset_transfer(txn, suggested_params)
|
|
2275
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)]
|
|
2276
|
+
case AssetOptInParams():
|
|
2277
|
+
asset_transfer = self._build_asset_transfer(
|
|
2278
|
+
AssetTransferParams(**txn.__dict__, receiver=txn.sender, amount=0), suggested_params
|
|
2279
|
+
)
|
|
2280
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)]
|
|
2281
|
+
case AssetOptOutParams():
|
|
2282
|
+
txn_dict = txn.__dict__
|
|
2283
|
+
creator = txn_dict.pop("creator")
|
|
2284
|
+
asset_transfer = self._build_asset_transfer(
|
|
2285
|
+
AssetTransferParams(**txn_dict, receiver=txn.sender, amount=0, close_asset_to=creator),
|
|
2286
|
+
suggested_params,
|
|
2287
|
+
)
|
|
2288
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(asset_transfer, signer)]
|
|
2289
|
+
case OnlineKeyRegistrationParams() | OfflineKeyRegistrationParams():
|
|
2290
|
+
key_reg = self._build_key_reg(txn, suggested_params)
|
|
2291
|
+
return [TransactionWithSignerAndContext.from_txn_with_context(key_reg, signer)]
|
|
2292
|
+
case _:
|
|
2293
|
+
raise ValueError(f"Unsupported txn: {txn}")
|