-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathtransaction_composer.py
2501 lines (2122 loc) · 102 KB
/
transaction_composer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import base64
import json
import re
from copy import deepcopy
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, TypedDict, Union, cast
import algosdk
import algosdk.atomic_transaction_composer
import algosdk.v2client.models
from algosdk import logic, transaction
from algosdk.atomic_transaction_composer import (
AtomicTransactionComposer,
SimulateAtomicTransactionResponse,
TransactionSigner,
TransactionWithSigner,
)
from algosdk.transaction import ApplicationCallTxn, OnComplete, SuggestedParams
from algosdk.v2client.algod import AlgodClient
from algosdk.v2client.models.simulate_request import SimulateRequest
from typing_extensions import deprecated
from algokit_utils.applications.abi import ABIReturn, ABIValue
from algokit_utils.applications.app_manager import AppManager
from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method
from algokit_utils.config import config
from algokit_utils.models.state import BoxIdentifier, BoxReference
from algokit_utils.models.transaction import SendParams, TransactionWrapper
from algokit_utils.protocols.account import TransactionSignerAccountProtocol
if TYPE_CHECKING:
from collections.abc import Callable
from algosdk.abi import Method
from algosdk.v2client.algod import AlgodClient
from algosdk.v2client.models import SimulateTraceConfig
from algokit_utils.models.amount import AlgoAmount
from algokit_utils.models.transaction import Arc2TransactionNote
__all__ = [
"AppCallMethodCallParams",
"AppCallParams",
"AppCreateMethodCallParams",
"AppCreateParams",
"AppCreateSchema",
"AppDeleteMethodCallParams",
"AppDeleteParams",
"AppMethodCallTransactionArgument",
"AppUpdateMethodCallParams",
"AppUpdateParams",
"AssetConfigParams",
"AssetCreateParams",
"AssetDestroyParams",
"AssetFreezeParams",
"AssetOptInParams",
"AssetOptOutParams",
"AssetTransferParams",
"BuiltTransactions",
"MethodCallParams",
"OfflineKeyRegistrationParams",
"OnlineKeyRegistrationParams",
"PaymentParams",
"SendAtomicTransactionComposerResults",
"TransactionComposer",
"TransactionComposerBuildResult",
"TxnParams",
"calculate_extra_program_pages",
"populate_app_call_resources",
"prepare_group_for_sending",
"send_atomic_transaction_composer",
]
MAX_TRANSACTION_GROUP_SIZE = 16
MAX_APP_CALL_FOREIGN_REFERENCES = 8
MAX_APP_CALL_ACCOUNT_REFERENCES = 4
@dataclass(kw_only=True, frozen=True)
class _CommonTxnParams:
sender: str
"""The account that will send the transaction"""
signer: TransactionSigner | TransactionSignerAccountProtocol | None = None
"""The signer for the transaction, defaults to None"""
rekey_to: str | None = None
"""The account to rekey to, defaults to None"""
note: bytes | None = None
"""The note for the transaction, defaults to None"""
lease: bytes | None = None
"""The lease for the transaction, defaults to None"""
static_fee: AlgoAmount | None = None
"""The static fee for the transaction, defaults to None"""
extra_fee: AlgoAmount | None = None
"""The extra fee for the transaction, defaults to None"""
max_fee: AlgoAmount | None = None
"""The maximum fee for the transaction, defaults to None"""
validity_window: int | None = None
"""The validity window for the transaction, defaults to None"""
first_valid_round: int | None = None
"""The first valid round for the transaction, defaults to None"""
last_valid_round: int | None = None
"""The last valid round for the transaction, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AdditionalAtcContext:
max_fees: dict[int, AlgoAmount] | None = None
"""The maximum fees for each transaction, defaults to None"""
suggested_params: SuggestedParams | None = None
"""The suggested parameters for the transaction, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class PaymentParams(_CommonTxnParams):
"""Parameters for a payment transaction."""
receiver: str
"""The account that will receive the ALGO"""
amount: AlgoAmount
"""Amount to send"""
close_remainder_to: str | None = None
"""If given, close the sender account and send the remaining balance to this address, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AssetCreateParams(_CommonTxnParams):
"""Parameters for creating a new asset."""
total: int
"""The total amount of the smallest divisible unit to create"""
asset_name: str | None = None
"""The full name of the asset"""
unit_name: str | None = None
"""The short ticker name for the asset"""
url: str | None = None
"""The metadata URL for the asset"""
decimals: int | None = None
"""The amount of decimal places the asset should have"""
default_frozen: bool | None = None
"""Whether the asset is frozen by default in the creator address"""
manager: str | None = None
"""The address that can change the manager, reserve, clawback, and freeze addresses"""
reserve: str | None = None
"""The address that holds the uncirculated supply"""
freeze: str | None = None
"""The address that can freeze the asset in any account"""
clawback: str | None = None
"""The address that can clawback the asset from any account"""
metadata_hash: bytes | None = None
"""Hash of the metadata contained in the metadata URL"""
@dataclass(kw_only=True, frozen=True)
class AssetConfigParams(_CommonTxnParams):
"""Parameters for configuring an existing asset."""
asset_id: int
"""The ID of the asset"""
manager: str | None = None
"""The address that can change the manager, reserve, clawback, and freeze addresses, defaults to None"""
reserve: str | None = None
"""The address that holds the uncirculated supply, defaults to None"""
freeze: str | None = None
"""The address that can freeze the asset in any account, defaults to None"""
clawback: str | None = None
"""The address that can clawback the asset from any account, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AssetFreezeParams(_CommonTxnParams):
"""Parameters for freezing an asset."""
asset_id: int
"""The ID of the asset"""
account: str
"""The account to freeze or unfreeze"""
frozen: bool
"""Whether the assets in the account should be frozen"""
@dataclass(kw_only=True, frozen=True)
class AssetDestroyParams(_CommonTxnParams):
"""Parameters for destroying an asset."""
asset_id: int
"""The ID of the asset"""
@dataclass(kw_only=True, frozen=True)
class OnlineKeyRegistrationParams(_CommonTxnParams):
"""Parameters for online key registration."""
vote_key: str
"""The root participation public key"""
selection_key: str
"""The VRF public key"""
vote_first: int
"""The first round that the participation key is valid"""
vote_last: int
"""The last round that the participation key is valid"""
vote_key_dilution: int
"""The dilution for the 2-level participation key"""
state_proof_key: bytes | None = None
"""The 64 byte state proof public key commitment, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class OfflineKeyRegistrationParams(_CommonTxnParams):
"""Parameters for offline key registration."""
prevent_account_from_ever_participating_again: bool
"""Whether to prevent the account from ever participating again"""
@dataclass(kw_only=True, frozen=True)
class AssetTransferParams(_CommonTxnParams):
"""Parameters for transferring an asset."""
asset_id: int
"""The ID of the asset"""
amount: int
"""The amount of the asset to transfer (smallest divisible unit)"""
receiver: str
"""The account to send the asset to"""
clawback_target: str | None = None
"""The account to take the asset from, defaults to None"""
close_asset_to: str | None = None
"""The account to close the asset to, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AssetOptInParams(_CommonTxnParams):
"""Parameters for opting into an asset."""
asset_id: int
"""The ID of the asset"""
@dataclass(kw_only=True, frozen=True)
class AssetOptOutParams(_CommonTxnParams):
"""Parameters for opting out of an asset."""
asset_id: int
"""The ID of the asset"""
creator: str
"""The creator address of the asset"""
@dataclass(kw_only=True, frozen=True)
class AppCallParams(_CommonTxnParams):
"""Parameters for calling an application."""
on_complete: OnComplete
"""The OnComplete action, defaults to None"""
app_id: int | None = None
"""The ID of the application, defaults to None"""
approval_program: str | bytes | None = None
"""The program to execute for all OnCompletes other than ClearState, defaults to None"""
clear_state_program: str | bytes | None = None
"""The program to execute for ClearState OnComplete, defaults to None"""
schema: dict[str, int] | None = None
"""The state schema for the app, defaults to None"""
args: list[bytes] | None = None
"""Application arguments, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
extra_pages: int | None = None
"""Number of extra pages required for the programs, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
class AppCreateSchema(TypedDict):
global_ints: int
"""The number of global ints in the schema"""
global_byte_slices: int
"""The number of global byte slices in the schema"""
local_ints: int
"""The number of local ints in the schema"""
local_byte_slices: int
"""The number of local byte slices in the schema"""
@dataclass(kw_only=True, frozen=True)
class AppCreateParams(_CommonTxnParams):
"""Parameters for creating an application."""
approval_program: str | bytes
"""The program to execute for all OnCompletes other than ClearState"""
clear_state_program: str | bytes
"""The program to execute for ClearState OnComplete"""
schema: AppCreateSchema | None = None
"""The state schema for the app, defaults to None"""
on_complete: OnComplete | None = None
"""The OnComplete action, defaults to None"""
args: list[bytes] | None = None
"""Application arguments, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
extra_program_pages: int | None = None
"""Number of extra pages required for the programs, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppUpdateParams(_CommonTxnParams):
"""Parameters for updating an application."""
app_id: int
"""The ID of the application"""
approval_program: str | bytes
"""The program to execute for all OnCompletes other than ClearState"""
clear_state_program: str | bytes
"""The program to execute for ClearState OnComplete"""
args: list[bytes] | None = None
"""Application arguments, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
on_complete: OnComplete | None = None
"""The OnComplete action, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppDeleteParams(_CommonTxnParams):
"""Parameters for deleting an application."""
app_id: int
"""The ID of the application"""
args: list[bytes] | None = None
"""Application arguments, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
on_complete: OnComplete = OnComplete.DeleteApplicationOC
"""The OnComplete action, defaults to DeleteApplicationOC"""
@dataclass(kw_only=True, frozen=True)
class _BaseAppMethodCall(_CommonTxnParams):
app_id: int
"""The ID of the application"""
method: Method
"""The ABI method to call"""
args: list | None = None
"""Arguments to the ABI method, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
schema: AppCreateSchema | None = None
"""The state schema for the app, defaults to None"""
on_complete: OnComplete | None = None
"""The OnComplete action, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppMethodCallParams(_CommonTxnParams):
"""Parameters for calling an application method."""
app_id: int
"""The ID of the application"""
method: Method
"""The ABI method to call"""
args: list[bytes] | None = None
"""Arguments to the ABI method, defaults to None"""
on_complete: OnComplete | None = None
"""The OnComplete action, defaults to None"""
account_references: list[str] | None = None
"""Account references, defaults to None"""
app_references: list[int] | None = None
"""App references, defaults to None"""
asset_references: list[int] | None = None
"""Asset references, defaults to None"""
box_references: list[BoxReference | BoxIdentifier] | None = None
"""Box references, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppCallMethodCallParams(_BaseAppMethodCall):
"""Parameters for a regular ABI method call."""
app_id: int
"""The ID of the application"""
on_complete: OnComplete | None = None
"""The OnComplete action, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppCreateMethodCallParams(_BaseAppMethodCall):
"""Parameters for an ABI method call that creates an application."""
approval_program: str | bytes
"""The program to execute for all OnCompletes other than ClearState"""
clear_state_program: str | bytes
"""The program to execute for ClearState OnComplete"""
schema: AppCreateSchema | None = None
"""The state schema for the app, defaults to None"""
on_complete: OnComplete | None = None
"""The OnComplete action (cannot be ClearState), defaults to None"""
extra_program_pages: int | None = None
"""Number of extra pages required for the programs, defaults to None"""
@dataclass(kw_only=True, frozen=True)
class AppUpdateMethodCallParams(_BaseAppMethodCall):
"""Parameters for an ABI method call that updates an application."""
app_id: int
"""The ID of the application"""
approval_program: str | bytes
"""The program to execute for all OnCompletes other than ClearState"""
clear_state_program: str | bytes
"""The program to execute for ClearState OnComplete"""
on_complete: OnComplete = OnComplete.UpdateApplicationOC
"""The OnComplete action"""
@dataclass(kw_only=True, frozen=True)
class AppDeleteMethodCallParams(_BaseAppMethodCall):
"""Parameters for an ABI method call that deletes an application."""
app_id: int
"""The ID of the application"""
on_complete: OnComplete = OnComplete.DeleteApplicationOC
"""The OnComplete action"""
MethodCallParams = (
AppCallMethodCallParams | AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams
)
AppMethodCallTransactionArgument = (
TransactionWithSigner
| algosdk.transaction.Transaction
| AppCreateMethodCallParams
| AppUpdateMethodCallParams
| AppCallMethodCallParams
)
TxnParams = Union[ # noqa: UP007
PaymentParams,
AssetCreateParams,
AssetConfigParams,
AssetFreezeParams,
AssetDestroyParams,
OnlineKeyRegistrationParams,
AssetTransferParams,
AssetOptInParams,
AssetOptOutParams,
AppCallParams,
AppCreateParams,
AppUpdateParams,
AppDeleteParams,
MethodCallParams,
OfflineKeyRegistrationParams,
]
@dataclass(frozen=True, kw_only=True)
class TransactionContext:
"""Contextual information for a transaction."""
max_fee: AlgoAmount | None = None
abi_method: Method | None = None
@staticmethod
def empty() -> TransactionContext:
return TransactionContext(max_fee=None, abi_method=None)
class TransactionWithContext:
"""Combines Transaction with additional context."""
def __init__(self, txn: algosdk.transaction.Transaction, context: TransactionContext):
self.txn = txn
self.context = context
class TransactionWithSignerAndContext(TransactionWithSigner):
"""Combines TransactionWithSigner with additional context."""
def __init__(self, txn: algosdk.transaction.Transaction, signer: TransactionSigner, context: TransactionContext):
super().__init__(txn, signer)
self.context = context
@staticmethod
def from_txn_with_context(
txn_with_context: TransactionWithContext, signer: TransactionSigner
) -> TransactionWithSignerAndContext:
return TransactionWithSignerAndContext(
txn=txn_with_context.txn, signer=signer, context=txn_with_context.context
)
@dataclass(frozen=True)
class BuiltTransactions:
"""Set of transactions built by TransactionComposer."""
transactions: list[algosdk.transaction.Transaction]
"""The built transactions"""
method_calls: dict[int, Method]
"""Map of transaction index to ABI method"""
signers: dict[int, TransactionSigner]
"""Map of transaction index to TransactionSigner"""
@dataclass
class TransactionComposerBuildResult:
"""Result of building transactions with TransactionComposer."""
atc: AtomicTransactionComposer
"""The AtomicTransactionComposer instance"""
transactions: list[TransactionWithSigner]
"""The list of transactions with signers"""
method_calls: dict[int, Method]
"""Map of transaction index to ABI method"""
@dataclass
class SendAtomicTransactionComposerResults:
"""Results from sending an AtomicTransactionComposer transaction group."""
group_id: str
"""The group ID if this was a transaction group"""
confirmations: list[algosdk.v2client.algod.AlgodResponseType]
"""The confirmation info for each transaction"""
tx_ids: list[str]
"""The transaction IDs that were sent"""
transactions: list[TransactionWrapper]
"""The transactions that were sent"""
returns: list[ABIReturn]
"""The ABI return values from any ABI method calls"""
simulate_response: dict[str, Any] | None = None
"""The simulation response if simulation was performed, defaults to None"""
class UnnamedResourcesAccessed:
"""Information about unnamed resource access."""
def __init__(self, resources_accessed: dict[str, Any] | None = None):
resources = resources_accessed or {}
if not isinstance(resources, dict):
raise TypeError(f"Expected dictionary object, got {type(resources_accessed)}")
self.accounts: list[str] | None = resources.get("accounts", None)
self.app_locals: list[dict[str, Any]] | None = resources.get("app-locals", None)
self.apps: list[int] | None = resources.get("apps", None)
self.asset_holdings: list[dict[str, Any]] | None = resources.get("asset-holdings", None)
self.assets: list[int] | None = resources.get("assets", None)
self.boxes: list[dict[str, Any]] | None = resources.get("boxes", None)
self.extra_box_refs: int | None = resources.get("extra-box-refs", None)
@dataclass
class ExecutionInfoTxn:
"""Execution info for a transaction."""
unnamed_resources_accessed: UnnamedResourcesAccessed | None = None
"""The unnamed resources accessed in the transaction"""
required_fee_delta: int = 0
"""The required fee delta for the transaction"""
@dataclass
class ExecutionInfo:
"""Information about transaction execution from simulation."""
group_unnamed_resources_accessed: UnnamedResourcesAccessed | None = None
"""The unnamed resources accessed in the group"""
txns: list[ExecutionInfoTxn] | None = None
"""The execution info for each transaction"""
@dataclass
class _TransactionWithPriority:
txn: algosdk.transaction.Transaction
priority: int
fee_delta: int
index: int
MAX_LEASE_LENGTH = 32
NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner()
def _encode_lease(lease: str | bytes | None) -> bytes | None:
if lease is None:
return None
elif isinstance(lease, bytes):
if not (1 <= len(lease) <= MAX_LEASE_LENGTH):
raise ValueError(
f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, "
f"but received bytes with length {len(lease)}"
)
if len(lease) == MAX_LEASE_LENGTH:
return lease
lease32 = bytearray(32)
lease32[: len(lease)] = lease
return bytes(lease32)
elif isinstance(lease, str):
encoded = lease.encode("utf-8")
if not (1 <= len(encoded) <= MAX_LEASE_LENGTH):
raise ValueError(
f"Received invalid lease; expected something with length between 1 and {MAX_LEASE_LENGTH}, "
f"but received '{lease}' with length {len(lease)}"
)
lease32 = bytearray(MAX_LEASE_LENGTH)
lease32[: len(encoded)] = encoded
return bytes(lease32)
else:
raise TypeError(f"Unknown lease type received of {type(lease)}")
def _get_group_execution_info( # noqa: C901, PLR0912
atc: AtomicTransactionComposer,
algod: AlgodClient,
populate_app_call_resources: bool | None = None,
cover_app_call_inner_transaction_fees: bool | None = None,
additional_atc_context: AdditionalAtcContext | None = None,
) -> ExecutionInfo:
# Create simulation request
suggested_params = additional_atc_context.suggested_params if additional_atc_context else None
max_fees = additional_atc_context.max_fees if additional_atc_context else None
simulate_request = SimulateRequest(
txn_groups=[],
allow_unnamed_resources=True,
allow_empty_signatures=True,
)
# Clone ATC with null signers
empty_signer_atc = atc.clone()
# Track app call indexes without max fees
app_call_indexes_without_max_fees = []
# Copy transactions with null signers
for i, txn in enumerate(empty_signer_atc.txn_list):
txn_with_signer = TransactionWithSigner(txn=txn.txn, signer=NULL_SIGNER)
if cover_app_call_inner_transaction_fees and isinstance(txn.txn, algosdk.transaction.ApplicationCallTxn):
if not suggested_params:
raise ValueError("suggested_params required when cover_app_call_inner_transaction_fees enabled")
max_fee = max_fees.get(i).micro_algo if max_fees and i in max_fees else None # type: ignore[union-attr]
if max_fee is None:
app_call_indexes_without_max_fees.append(i)
else:
txn_with_signer.txn.fee = max_fee
if cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees:
raise ValueError(
f"Please provide a `max_fee` for each app call transaction when `cover_app_call_inner_transaction_fees` is enabled. " # noqa: E501
f"Required for transactions: {', '.join(str(i) for i in app_call_indexes_without_max_fees)}"
)
# Get fee parameters
per_byte_txn_fee = suggested_params.fee if suggested_params else 0
min_txn_fee = int(suggested_params.min_fee) if suggested_params else 1000 # type: ignore[unused-ignore]
# Simulate transactions
result = empty_signer_atc.simulate(algod, simulate_request)
group_response = result.simulate_response["txn-groups"][0]
if group_response.get("failure-message"):
msg = group_response["failure-message"]
if cover_app_call_inner_transaction_fees and "fee too small" in msg:
raise ValueError(
"Fees were too small to resolve execution info via simulate. "
"You may need to increase an app call transaction maxFee."
)
failed_at = group_response.get("failed-at", [0])[0]
raise ValueError(
f"Error resolving execution info via simulate in transaction {failed_at}: "
f"{group_response['failure-message']}"
)
# Build execution info
txn_results = []
for i, txn_result_raw in enumerate(group_response["txn-results"]):
txn_result = txn_result_raw.get("txn-result")
if not txn_result:
continue
original_txn = atc.build_group()[i].txn
required_fee_delta = 0
if cover_app_call_inner_transaction_fees:
# Calculate parent transaction fee
parent_per_byte_fee = per_byte_txn_fee * (original_txn.estimate_size() + 75)
parent_min_fee = max(parent_per_byte_fee, min_txn_fee)
parent_fee_delta = parent_min_fee - original_txn.fee
if isinstance(original_txn, algosdk.transaction.ApplicationCallTxn):
# Calculate inner transaction fees recursively
def calculate_inner_fee_delta(inner_txns: list[dict], acc: int = 0) -> int:
for inner_txn in reversed(inner_txns):
current_fee_delta = (
calculate_inner_fee_delta(inner_txn["inner-txns"], acc)
if inner_txn.get("inner-txns")
else acc
) + (min_txn_fee - inner_txn["txn"]["txn"].get("fee", 0))
acc = max(0, current_fee_delta)
return acc
inner_fee_delta = calculate_inner_fee_delta(txn_result.get("inner-txns", []))
required_fee_delta = inner_fee_delta + parent_fee_delta
else:
required_fee_delta = parent_fee_delta
txn_results.append(
ExecutionInfoTxn(
unnamed_resources_accessed=UnnamedResourcesAccessed(txn_result_raw.get("unnamed-resources-accessed"))
if populate_app_call_resources
else None,
required_fee_delta=required_fee_delta,
)
)
return ExecutionInfo(
group_unnamed_resources_accessed=UnnamedResourcesAccessed(group_response.get("unnamed-resources-accessed"))
if populate_app_call_resources
else None,
txns=txn_results,
)
def _find_available_transaction_index(
txns: list[TransactionWithSigner], reference_type: str, reference: str | dict[str, Any] | int
) -> int:
"""Find index of first transaction that can accommodate the new reference."""
def check_transaction(txn: TransactionWithSigner) -> bool:
# Skip if not an application call transaction
if txn.txn.type != "appl":
return False
# Get current counts (using get() with default 0 for Pythonic null handling)
accounts = len(getattr(txn.txn, "accounts", []) or [])
assets = len(getattr(txn.txn, "foreign_assets", []) or [])
apps = len(getattr(txn.txn, "foreign_apps", []) or [])
boxes = len(getattr(txn.txn, "boxes", []) or [])
# For account references, only check account limit
if reference_type == "account":
return accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
# For asset holdings or local state, need space for both account and other reference
if reference_type in ("asset_holding", "app_local"):
return (
accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1
and accounts < MAX_APP_CALL_ACCOUNT_REFERENCES
)
# For boxes with non-zero app ID, need space for box and app reference
if reference_type == "box" and reference and int(getattr(reference, "app", 0)) != 0:
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES - 1
# Default case - just check total references
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
# Return first matching index or -1 if none found
return next((i for i, txn in enumerate(txns) if check_transaction(txn)), -1)
def calculate_extra_program_pages(approval: bytes | None, clear: bytes | None) -> int:
"""Calculate minimum number of extra_pages required for provided approval and clear programs"""
total = len(approval or b"") + len(clear or b"")
return max(0, (total - 1) // algosdk.constants.APP_PAGE_MAX_SIZE)
def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer:
"""Populate application call resources based on simulation results.
:param atc: The AtomicTransactionComposer containing transactions
:param algod: Algod client for simulation
:return: Modified AtomicTransactionComposer with populated resources
"""
return prepare_group_for_sending(atc, algod, populate_app_call_resources=True)
def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915
atc: AtomicTransactionComposer,
algod: AlgodClient,
populate_app_call_resources: bool | None = None,
cover_app_call_inner_transaction_fees: bool | None = None,
additional_atc_context: AdditionalAtcContext | None = None,
) -> AtomicTransactionComposer:
"""Take an existing Atomic Transaction Composer and return a new one with changes applied to the transactions
based on the supplied parameters to prepare it for sending.
Please note, that before calling `.execute()` on the returned ATC, you must call `.build_group()`.
:param atc: The AtomicTransactionComposer containing transactions
:param algod: Algod client for simulation
:param populate_app_call_resources: Whether to populate app call resources
:param cover_app_call_inner_transaction_fees: Whether to cover inner txn fees
:param additional_atc_context: Additional context for the AtomicTransactionComposer
:return: Modified AtomicTransactionComposer ready for sending
"""
# Get execution info via simulation
execution_info = _get_group_execution_info(
atc, algod, populate_app_call_resources, cover_app_call_inner_transaction_fees, additional_atc_context
)
max_fees = additional_atc_context.max_fees if additional_atc_context else None
group = atc.build_group()
# Handle transaction fees if needed
if cover_app_call_inner_transaction_fees:
# Sort transactions by fee priority
txns_with_priority: list[_TransactionWithPriority] = []
for i, txn_info in enumerate(execution_info.txns or []):
if not txn_info:
continue
txn = group[i].txn
max_fee = max_fees.get(i).micro_algo if max_fees and i in max_fees else None # type: ignore[union-attr]
immutable_fee = max_fee is not None and max_fee == txn.fee
priority_multiplier = (
1000
if (
txn_info.required_fee_delta > 0
and (immutable_fee or not isinstance(txn, algosdk.transaction.ApplicationCallTxn))
)
else 1
)
txns_with_priority.append(
_TransactionWithPriority(
txn=txn,
index=i,
fee_delta=txn_info.required_fee_delta,
priority=txn_info.required_fee_delta * priority_multiplier
if txn_info.required_fee_delta > 0
else -1,
)
)
# Sort by priority descending
txns_with_priority.sort(key=lambda x: x.priority, reverse=True)
# Calculate surplus fees and additional fees needed
surplus_fees = sum(
txn_info.required_fee_delta * -1
for txn_info in execution_info.txns or []
if txn_info is not None and txn_info.required_fee_delta < 0
)
additional_fees = {}
# Distribute surplus fees to cover deficits
for txn_obj in txns_with_priority:
if txn_obj.fee_delta > 0:
if surplus_fees >= txn_obj.fee_delta:
surplus_fees -= txn_obj.fee_delta
else:
additional_fees[txn_obj.index] = txn_obj.fee_delta - surplus_fees
surplus_fees = 0
def populate_group_resource( # noqa: PLR0915, PLR0912, C901
txns: list[TransactionWithSigner], reference: str | dict[str, Any] | int, ref_type: str
) -> None:
"""Helper function to populate group-level resources."""
def is_appl_below_limit(t: TransactionWithSigner) -> bool:
if not isinstance(t.txn, transaction.ApplicationCallTxn):
return False
accounts = len(getattr(t.txn, "accounts", []) or [])
assets = len(getattr(t.txn, "foreign_assets", []) or [])
apps = len(getattr(t.txn, "foreign_apps", []) or [])
boxes = len(getattr(t.txn, "boxes", []) or [])
return accounts + assets + apps + boxes < MAX_APP_CALL_FOREIGN_REFERENCES
# Handle asset holding and app local references first
if ref_type in ("assetHolding", "appLocal"):
ref_dict = cast(dict[str, Any], reference)
account = ref_dict["account"]
# First try to find transaction with account already available
txn_idx = next(
(
i
for i, t in enumerate(txns)
if is_appl_below_limit(t)
and isinstance(t.txn, transaction.ApplicationCallTxn)
and (
account in (getattr(t.txn, "accounts", []) or [])
or account
in (
logic.get_application_address(app_id)
for app_id in (getattr(t.txn, "foreign_apps", []) or [])
)
or any(str(account) in str(v) for v in t.txn.__dict__.values())
)
),
-1,
)
if txn_idx >= 0:
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
if ref_type == "assetHolding":
asset_id = ref_dict["asset"]
app_txn.foreign_assets = [*list(getattr(app_txn, "foreign_assets", []) or []), asset_id]
else:
app_id = ref_dict["app"]
app_txn.foreign_apps = [*list(getattr(app_txn, "foreign_apps", []) or []), app_id]
return
# Try to find transaction that already has the app/asset available
txn_idx = next(
(
i
for i, t in enumerate(txns)
if is_appl_below_limit(t)
and isinstance(t.txn, transaction.ApplicationCallTxn)
and len(getattr(t.txn, "accounts", []) or []) < MAX_APP_CALL_ACCOUNT_REFERENCES
and (
(
ref_type == "assetHolding"
and ref_dict["asset"] in (getattr(t.txn, "foreign_assets", []) or [])
)
or (
ref_type == "appLocal"
and (
ref_dict["app"] in (getattr(t.txn, "foreign_apps", []) or [])
or t.txn.index == ref_dict["app"]
)
)
)
),
-1,
)
if txn_idx >= 0:
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
accounts = list(getattr(app_txn, "accounts", []) or [])
accounts.append(account)
app_txn.accounts = accounts
return
# Handle box references
if ref_type == "box":
box_ref = (reference["app"], base64.b64decode(reference["name"])) # type: ignore[index]
# Try to find transaction that already has the app available
txn_idx = next(
(
i
for i, t in enumerate(txns)
if is_appl_below_limit(t)
and isinstance(t.txn, transaction.ApplicationCallTxn)
and (box_ref[0] in (getattr(t.txn, "foreign_apps", []) or []) or t.txn.index == box_ref[0])
),
-1,
)
if txn_idx >= 0:
app_txn = cast(transaction.ApplicationCallTxn, txns[txn_idx].txn)
boxes = list(getattr(app_txn, "boxes", []) or [])
boxes.append(BoxReference.translate_box_reference(box_ref, app_txn.foreign_apps or [], app_txn.index)) # type: ignore[arg-type]
app_txn.boxes = boxes
return
# Find available transaction for the resource
txn_idx = _find_available_transaction_index(txns, ref_type, reference)
if txn_idx == -1: