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