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

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

Potentially problematic release.


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

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