algokit-utils 3.0.0b1__py3-none-any.whl → 3.0.0b3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of algokit-utils might be problematic. Click here for more details.

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