algokit-utils 5.0.0a3__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.
- algokit_abi/__init__.py +9 -0
- algokit_abi/_arc32_to_arc56.py +242 -0
- algokit_abi/_arc56_serde.py +161 -0
- algokit_abi/abi.py +667 -0
- algokit_abi/arc32.py +210 -0
- algokit_abi/arc56.py +821 -0
- algokit_abi/py.typed +0 -0
- algokit_algo25/__init__.py +38 -0
- algokit_algo25/_encoding.py +46 -0
- algokit_algo25/_wordlist.py +2065 -0
- algokit_algo25/exceptions.py +29 -0
- algokit_algo25/mnemonic.py +128 -0
- algokit_algo25/py.typed +0 -0
- algokit_algod_client/__init__.py +10 -0
- algokit_algod_client/client.py +1585 -0
- algokit_algod_client/config.py +36 -0
- algokit_algod_client/exceptions.py +59 -0
- algokit_algod_client/models/__init__.py +229 -0
- algokit_algod_client/models/_account.py +150 -0
- algokit_algod_client/models/_account_application_response.py +25 -0
- algokit_algod_client/models/_account_asset_response.py +25 -0
- algokit_algod_client/models/_account_participation.py +53 -0
- algokit_algod_client/models/_account_state_delta.py +30 -0
- algokit_algod_client/models/_allocations_for_genesis_file.py +23 -0
- algokit_algod_client/models/_allocations_for_genesis_file_state_model.py +42 -0
- algokit_algod_client/models/_application.py +23 -0
- algokit_algod_client/models/_application_initial_states.py +37 -0
- algokit_algod_client/models/_application_kvstorage.py +29 -0
- algokit_algod_client/models/_application_local_state.py +33 -0
- algokit_algod_client/models/_application_params.py +63 -0
- algokit_algod_client/models/_application_state_operation.py +41 -0
- algokit_algod_client/models/_application_state_schema.py +22 -0
- algokit_algod_client/models/_asset.py +23 -0
- algokit_algod_client/models/_asset_holding.py +29 -0
- algokit_algod_client/models/_asset_params.py +102 -0
- algokit_algod_client/models/_avm_key_value.py +28 -0
- algokit_algod_client/models/_avm_value.py +32 -0
- algokit_algod_client/models/_block.py +363 -0
- algokit_algod_client/models/_block_hash_response.py +14 -0
- algokit_algod_client/models/_block_txids_response.py +14 -0
- algokit_algod_client/models/_box.py +36 -0
- algokit_algod_client/models/_box_descriptor.py +24 -0
- algokit_algod_client/models/_boxes_response.py +21 -0
- algokit_algod_client/models/_build_version_contains_the_current_algod_build_version_information.py +34 -0
- algokit_algod_client/models/_compile_response.py +24 -0
- algokit_algod_client/models/_disassemble_response.py +14 -0
- algokit_algod_client/models/_error_response.py +22 -0
- algokit_algod_client/models/_eval_delta.py +32 -0
- algokit_algod_client/models/_eval_delta_key_value.py +28 -0
- algokit_algod_client/models/_genesis_file_in_json.py +53 -0
- algokit_algod_client/models/_get_block_time_stamp_offset_response.py +14 -0
- algokit_algod_client/models/_get_sync_round_response.py +14 -0
- algokit_algod_client/models/_ledger_state_delta.py +389 -0
- algokit_algod_client/models/_light_block_header_proof.py +32 -0
- algokit_algod_client/models/_node_status_response.py +118 -0
- algokit_algod_client/models/_pending_transaction_response.py +91 -0
- algokit_algod_client/models/_pending_transactions_response.py +29 -0
- algokit_algod_client/models/_post_transactions_response.py +14 -0
- algokit_algod_client/models/_scratch_change.py +23 -0
- algokit_algod_client/models/_serde_helpers.py +241 -0
- algokit_algod_client/models/_simulate_initial_states.py +25 -0
- algokit_algod_client/models/_simulate_request.py +54 -0
- algokit_algod_client/models/_simulate_request_transaction_group.py +25 -0
- algokit_algod_client/models/_simulate_response.py +44 -0
- algokit_algod_client/models/_simulate_trace_config.py +30 -0
- algokit_algod_client/models/_simulate_transaction_group_result.py +46 -0
- algokit_algod_client/models/_simulate_transaction_result.py +41 -0
- algokit_algod_client/models/_simulate_unnamed_resources_accessed.py +64 -0
- algokit_algod_client/models/_simulation_eval_overrides.py +40 -0
- algokit_algod_client/models/_simulation_opcode_trace_unit.py +55 -0
- algokit_algod_client/models/_simulation_transaction_exec_trace.py +82 -0
- algokit_algod_client/models/_source_map.py +30 -0
- algokit_algod_client/models/_state_delta.py +6 -0
- algokit_algod_client/models/_state_proof.py +28 -0
- algokit_algod_client/models/_state_proof_message.py +44 -0
- algokit_algod_client/models/_supply_response.py +26 -0
- algokit_algod_client/models/_teal_key_value.py +28 -0
- algokit_algod_client/models/_teal_key_value_store.py +6 -0
- algokit_algod_client/models/_teal_value.py +32 -0
- algokit_algod_client/models/_transaction_group_ledger_state_deltas_for_round_response.py +21 -0
- algokit_algod_client/models/_transaction_parameters_response.py +45 -0
- algokit_algod_client/models/_transaction_proof.py +44 -0
- algokit_algod_client/models/_version_contains_the_current_algod_version.py +38 -0
- algokit_algod_client/models/suggested_params.py +42 -0
- algokit_algod_client/py.typed +1 -0
- algokit_algod_client/types.py +7 -0
- algokit_algosdk/__init__.py +38 -0
- algokit_algosdk/account.py +32 -0
- algokit_algosdk/app_access.py +228 -0
- algokit_algosdk/box_reference.py +100 -0
- algokit_algosdk/constants.py +147 -0
- algokit_algosdk/encoding.py +89 -0
- algokit_algosdk/error.py +180 -0
- algokit_algosdk/logic.py +61 -0
- algokit_algosdk/logicsig.py +218 -0
- algokit_algosdk/mnemonic.py +216 -0
- algokit_algosdk/multisig.py +161 -0
- algokit_algosdk/py.typed +0 -0
- algokit_algosdk/transaction.py +596 -0
- algokit_algosdk/wordlist.py +2054 -0
- algokit_common/__init__.py +50 -0
- algokit_common/address.py +34 -0
- algokit_common/constants.py +47 -0
- algokit_common/hashing.py +25 -0
- algokit_common/py.typed +0 -0
- algokit_common/serde/__init__.py +40 -0
- algokit_common/serde/_core.py +610 -0
- algokit_common/serde/_primitives.py +135 -0
- algokit_common/source_map.py +158 -0
- algokit_indexer_client/__init__.py +10 -0
- algokit_indexer_client/client.py +1456 -0
- algokit_indexer_client/config.py +36 -0
- algokit_indexer_client/exceptions.py +59 -0
- algokit_indexer_client/models/__init__.py +148 -0
- algokit_indexer_client/models/_account.py +161 -0
- algokit_indexer_client/models/_account_participation.py +53 -0
- algokit_indexer_client/models/_account_response.py +19 -0
- algokit_indexer_client/models/_account_state_delta.py +29 -0
- algokit_indexer_client/models/_accounts_response.py +29 -0
- algokit_indexer_client/models/_application.py +35 -0
- algokit_indexer_client/models/_application_local_state.py +45 -0
- algokit_indexer_client/models/_application_local_states_response.py +29 -0
- algokit_indexer_client/models/_application_log_data.py +28 -0
- algokit_indexer_client/models/_application_logs_response.py +33 -0
- algokit_indexer_client/models/_application_params.py +62 -0
- algokit_indexer_client/models/_application_response.py +20 -0
- algokit_indexer_client/models/_application_state_schema.py +22 -0
- algokit_indexer_client/models/_applications_response.py +29 -0
- algokit_indexer_client/models/_asset.py +35 -0
- algokit_indexer_client/models/_asset_balances_response.py +29 -0
- algokit_indexer_client/models/_asset_holding.py +41 -0
- algokit_indexer_client/models/_asset_holdings_response.py +29 -0
- algokit_indexer_client/models/_asset_params.py +102 -0
- algokit_indexer_client/models/_asset_response.py +19 -0
- algokit_indexer_client/models/_assets_response.py +29 -0
- algokit_indexer_client/models/_block.py +150 -0
- algokit_indexer_client/models/_block_headers_response.py +29 -0
- algokit_indexer_client/models/_block_rewards.py +38 -0
- algokit_indexer_client/models/_block_upgrade_state.py +34 -0
- algokit_indexer_client/models/_block_upgrade_vote.py +26 -0
- algokit_indexer_client/models/_box.py +36 -0
- algokit_indexer_client/models/_box_descriptor.py +24 -0
- algokit_indexer_client/models/_box_reference.py +28 -0
- algokit_indexer_client/models/_boxes_response.py +29 -0
- algokit_indexer_client/models/_error_response.py +18 -0
- algokit_indexer_client/models/_eval_delta.py +32 -0
- algokit_indexer_client/models/_eval_delta_key_value.py +28 -0
- algokit_indexer_client/models/_hash_factory.py +14 -0
- algokit_indexer_client/models/_hb_proof_fields.py +57 -0
- algokit_indexer_client/models/_health_check.py +42 -0
- algokit_indexer_client/models/_holding_ref.py +23 -0
- algokit_indexer_client/models/_indexer_state_proof_message.py +40 -0
- algokit_indexer_client/models/_locals_ref.py +23 -0
- algokit_indexer_client/models/_merkle_array_proof.py +29 -0
- algokit_indexer_client/models/_mini_asset_holding.py +38 -0
- algokit_indexer_client/models/_on_completion.py +25 -0
- algokit_indexer_client/models/_participation_updates.py +22 -0
- algokit_indexer_client/models/_resource_ref.py +42 -0
- algokit_indexer_client/models/_serde_helpers.py +241 -0
- algokit_indexer_client/models/_state_delta.py +6 -0
- algokit_indexer_client/models/_state_proof_fields.py +57 -0
- algokit_indexer_client/models/_state_proof_participant.py +20 -0
- algokit_indexer_client/models/_state_proof_reveal.py +25 -0
- algokit_indexer_client/models/_state_proof_sig_slot.py +20 -0
- algokit_indexer_client/models/_state_proof_signature.py +37 -0
- algokit_indexer_client/models/_state_proof_tracking.py +32 -0
- algokit_indexer_client/models/_state_proof_verifier.py +24 -0
- algokit_indexer_client/models/_state_schema.py +25 -0
- algokit_indexer_client/models/_teal_key_value.py +28 -0
- algokit_indexer_client/models/_teal_key_value_store.py +6 -0
- algokit_indexer_client/models/_teal_value.py +32 -0
- algokit_indexer_client/models/_transaction.py +213 -0
- algokit_indexer_client/models/_transaction_application.py +105 -0
- algokit_indexer_client/models/_transaction_asset_config.py +31 -0
- algokit_indexer_client/models/_transaction_asset_freeze.py +29 -0
- algokit_indexer_client/models/_transaction_asset_transfer.py +41 -0
- algokit_indexer_client/models/_transaction_heartbeat.py +52 -0
- algokit_indexer_client/models/_transaction_keyreg.py +59 -0
- algokit_indexer_client/models/_transaction_payment.py +33 -0
- algokit_indexer_client/models/_transaction_response.py +19 -0
- algokit_indexer_client/models/_transaction_signature.py +35 -0
- algokit_indexer_client/models/_transaction_signature_logicsig.py +59 -0
- algokit_indexer_client/models/_transaction_signature_multisig.py +36 -0
- algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py +28 -0
- algokit_indexer_client/models/_transaction_state_proof.py +32 -0
- algokit_indexer_client/models/_transactions_response.py +29 -0
- algokit_indexer_client/py.typed +1 -0
- algokit_indexer_client/types.py +7 -0
- algokit_kmd_client/__init__.py +10 -0
- algokit_kmd_client/client.py +1240 -0
- algokit_kmd_client/config.py +36 -0
- algokit_kmd_client/exceptions.py +59 -0
- algokit_kmd_client/models/__init__.py +112 -0
- algokit_kmd_client/models/_classical_signatures.py +4 -0
- algokit_kmd_client/models/_create_wallet_request.py +30 -0
- algokit_kmd_client/models/_create_wallet_response.py +19 -0
- algokit_kmd_client/models/_delete_key_request.py +27 -0
- algokit_kmd_client/models/_delete_multisig_request.py +27 -0
- algokit_kmd_client/models/_digest_represents_a32_byte_value_holding_the256_bit_hash_digest.py +4 -0
- algokit_kmd_client/models/_ed25519_public_key.py +4 -0
- algokit_kmd_client/models/_export_key_request.py +27 -0
- algokit_kmd_client/models/_export_key_response.py +24 -0
- algokit_kmd_client/models/_export_master_key_request.py +22 -0
- algokit_kmd_client/models/_export_master_key_response.py +18 -0
- algokit_kmd_client/models/_export_multisig_request.py +23 -0
- algokit_kmd_client/models/_export_multisig_response.py +26 -0
- algokit_kmd_client/models/_generate_key_request.py +18 -0
- algokit_kmd_client/models/_generate_key_response.py +19 -0
- algokit_kmd_client/models/_import_key_request.py +28 -0
- algokit_kmd_client/models/_import_key_response.py +19 -0
- algokit_kmd_client/models/_import_multisig_request.py +30 -0
- algokit_kmd_client/models/_import_multisig_response.py +19 -0
- algokit_kmd_client/models/_init_wallet_handle_token_request.py +22 -0
- algokit_kmd_client/models/_init_wallet_handle_token_response.py +18 -0
- algokit_kmd_client/models/_list_keys_request.py +18 -0
- algokit_kmd_client/models/_list_keys_response.py +18 -0
- algokit_kmd_client/models/_list_multisig_request.py +18 -0
- algokit_kmd_client/models/_list_multisig_response.py +18 -0
- algokit_kmd_client/models/_list_wallets_request.py +11 -0
- algokit_kmd_client/models/_list_wallets_response.py +25 -0
- algokit_kmd_client/models/_master_derivation_key.py +4 -0
- algokit_kmd_client/models/_multisig_sig.py +33 -0
- algokit_kmd_client/models/_multisig_subsig.py +23 -0
- algokit_kmd_client/models/_public_key.py +4 -0
- algokit_kmd_client/models/_release_wallet_handle_token_request.py +18 -0
- algokit_kmd_client/models/_rename_wallet_request.py +26 -0
- algokit_kmd_client/models/_rename_wallet_response.py +19 -0
- algokit_kmd_client/models/_renew_wallet_handle_token_request.py +18 -0
- algokit_kmd_client/models/_renew_wallet_handle_token_response.py +19 -0
- algokit_kmd_client/models/_serde_helpers.py +241 -0
- algokit_kmd_client/models/_sign_multisig_response.py +24 -0
- algokit_kmd_client/models/_sign_multisig_txn_request.py +45 -0
- algokit_kmd_client/models/_sign_program_multisig_request.py +50 -0
- algokit_kmd_client/models/_sign_program_multisig_response.py +24 -0
- algokit_kmd_client/models/_sign_program_request.py +37 -0
- algokit_kmd_client/models/_sign_program_response.py +24 -0
- algokit_kmd_client/models/_sign_transaction_response.py +24 -0
- algokit_kmd_client/models/_sign_txn_request.py +36 -0
- algokit_kmd_client/models/_signature.py +4 -0
- algokit_kmd_client/models/_tx_type.py +4 -0
- algokit_kmd_client/models/_versions_request.py +11 -0
- algokit_kmd_client/models/_versions_response.py +19 -0
- algokit_kmd_client/models/_wallet.py +38 -0
- algokit_kmd_client/models/_wallet_handle.py +24 -0
- algokit_kmd_client/models/_wallet_info_request.py +18 -0
- algokit_kmd_client/models/_wallet_info_response.py +19 -0
- algokit_kmd_client/py.typed +1 -0
- algokit_kmd_client/types.py +7 -0
- algokit_transact/__init__.py +190 -0
- algokit_transact/codec/__init__.py +0 -0
- algokit_transact/codec/msgpack.py +11 -0
- algokit_transact/codec/serde.py +7 -0
- algokit_transact/codec/signed.py +57 -0
- algokit_transact/codec/transaction.py +65 -0
- algokit_transact/exceptions.py +17 -0
- algokit_transact/logicsig.py +220 -0
- algokit_transact/models/__init__.py +0 -0
- algokit_transact/models/app_call.py +447 -0
- algokit_transact/models/asset_config.py +19 -0
- algokit_transact/models/asset_freeze.py +11 -0
- algokit_transact/models/asset_transfer.py +13 -0
- algokit_transact/models/common.py +17 -0
- algokit_transact/models/heartbeat.py +21 -0
- algokit_transact/models/key_registration.py +14 -0
- algokit_transact/models/payment.py +14 -0
- algokit_transact/models/signed_transaction.py +21 -0
- algokit_transact/models/state_proof.py +150 -0
- algokit_transact/models/transaction.py +88 -0
- algokit_transact/multisig.py +93 -0
- algokit_transact/ops/__init__.py +0 -0
- algokit_transact/ops/fees.py +47 -0
- algokit_transact/ops/group.py +28 -0
- algokit_transact/ops/ids.py +14 -0
- algokit_transact/ops/validate.py +503 -0
- algokit_transact/py.typed +0 -0
- algokit_transact/signer.py +195 -0
- algokit_transact/signing/__init__.py +0 -0
- algokit_transact/signing/logic_signature.py +19 -0
- algokit_transact/signing/multisig.py +84 -0
- algokit_transact/signing/types.py +39 -0
- algokit_transact/signing/validation.py +63 -0
- algokit_utils/__init__.py +23 -0
- algokit_utils/_debugging.py +304 -0
- algokit_utils/accounts/__init__.py +2 -0
- algokit_utils/accounts/account_manager.py +1051 -0
- algokit_utils/accounts/kmd_account_manager.py +206 -0
- algokit_utils/algo25.py +46 -0
- algokit_utils/algorand.py +383 -0
- algokit_utils/applications/__init__.py +7 -0
- algokit_utils/applications/abi.py +280 -0
- algokit_utils/applications/app_client.py +2193 -0
- algokit_utils/applications/app_deployer.py +788 -0
- algokit_utils/applications/app_factory.py +1140 -0
- algokit_utils/applications/app_manager.py +575 -0
- algokit_utils/applications/app_spec/__init__.py +6 -0
- algokit_utils/applications/enums.py +40 -0
- algokit_utils/assets/__init__.py +1 -0
- algokit_utils/assets/asset_manager.py +344 -0
- algokit_utils/clients/__init__.py +41 -0
- algokit_utils/clients/client_manager.py +756 -0
- algokit_utils/clients/dispenser_api_client.py +212 -0
- algokit_utils/common.py +40 -0
- algokit_utils/config.py +159 -0
- algokit_utils/errors/__init__.py +1 -0
- algokit_utils/errors/logic_error.py +160 -0
- algokit_utils/models/__init__.py +7 -0
- algokit_utils/models/account.py +12 -0
- algokit_utils/models/amount.py +198 -0
- algokit_utils/models/application.py +90 -0
- algokit_utils/models/network.py +29 -0
- algokit_utils/models/simulate.py +7 -0
- algokit_utils/models/state.py +53 -0
- algokit_utils/models/transaction.py +49 -0
- algokit_utils/protocols/__init__.py +3 -0
- algokit_utils/protocols/account.py +11 -0
- algokit_utils/protocols/signer.py +17 -0
- algokit_utils/protocols/typed_clients.py +110 -0
- algokit_utils/py.typed +0 -0
- algokit_utils/transact.py +195 -0
- algokit_utils/transactions/__init__.py +3 -0
- algokit_utils/transactions/builders/__init__.py +67 -0
- algokit_utils/transactions/builders/app.py +248 -0
- algokit_utils/transactions/builders/asset.py +256 -0
- algokit_utils/transactions/builders/common.py +263 -0
- algokit_utils/transactions/builders/keyreg.py +103 -0
- algokit_utils/transactions/builders/method_call.py +380 -0
- algokit_utils/transactions/builders/payment.py +43 -0
- algokit_utils/transactions/composer_resources.py +409 -0
- algokit_utils/transactions/fee_coverage.py +79 -0
- algokit_utils/transactions/helpers.py +9 -0
- algokit_utils/transactions/transaction_composer.py +1574 -0
- algokit_utils/transactions/transaction_creator.py +699 -0
- algokit_utils/transactions/transaction_sender.py +1240 -0
- algokit_utils/transactions/types.py +262 -0
- algokit_utils-5.0.0a3.dist-info/METADATA +105 -0
- algokit_utils-5.0.0a3.dist-info/RECORD +337 -0
- algokit_utils-5.0.0a3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from typing import Any, TypeAlias, TypedDict, cast
|
|
7
|
+
|
|
8
|
+
from algokit_abi import arc56
|
|
9
|
+
from algokit_algod_client import AlgodClient
|
|
10
|
+
from algokit_algod_client import models as algod_models
|
|
11
|
+
from algokit_algod_client.exceptions import UnexpectedStatusError
|
|
12
|
+
from algokit_algod_client.models import SimulateTransactionResult
|
|
13
|
+
from algokit_common.constants import MAX_TRANSACTION_GROUP_SIZE
|
|
14
|
+
from algokit_transact import decode_signed_transaction, encode_signed_transactions, make_empty_transaction_signer
|
|
15
|
+
from algokit_transact.models.signed_transaction import SignedTransaction
|
|
16
|
+
from algokit_transact.models.transaction import Transaction, TransactionType
|
|
17
|
+
from algokit_transact.ops.fees import calculate_fee
|
|
18
|
+
from algokit_transact.ops.group import group_transactions
|
|
19
|
+
from algokit_transact.ops.ids import get_transaction_id
|
|
20
|
+
from algokit_transact.signer import AddressWithTransactionSigner, TransactionSigner
|
|
21
|
+
from algokit_utils.applications.abi import ABIReturn
|
|
22
|
+
from algokit_utils.applications.app_manager import AppManager
|
|
23
|
+
from algokit_utils.clients.client_manager import ClientManager
|
|
24
|
+
from algokit_utils.config import config
|
|
25
|
+
from algokit_utils.models.amount import AlgoAmount
|
|
26
|
+
from algokit_utils.models.transaction import Arc2TransactionNote, SendParams
|
|
27
|
+
from algokit_utils.transactions.builders import (
|
|
28
|
+
build_app_call_method_call_transaction,
|
|
29
|
+
build_app_call_transaction,
|
|
30
|
+
build_app_create_method_call_transaction,
|
|
31
|
+
build_app_create_transaction,
|
|
32
|
+
build_app_delete_method_call_transaction,
|
|
33
|
+
build_app_delete_transaction,
|
|
34
|
+
build_app_update_method_call_transaction,
|
|
35
|
+
build_app_update_transaction,
|
|
36
|
+
build_asset_config_transaction,
|
|
37
|
+
build_asset_create_transaction,
|
|
38
|
+
build_asset_destroy_transaction,
|
|
39
|
+
build_asset_freeze_transaction,
|
|
40
|
+
build_asset_opt_in_transaction,
|
|
41
|
+
build_asset_opt_out_transaction,
|
|
42
|
+
build_asset_transfer_transaction,
|
|
43
|
+
build_offline_key_registration_transaction,
|
|
44
|
+
build_online_key_registration_transaction,
|
|
45
|
+
build_payment_transaction,
|
|
46
|
+
)
|
|
47
|
+
from algokit_utils.transactions.builders.common import calculate_inner_fee_delta
|
|
48
|
+
from algokit_utils.transactions.composer_resources import populate_group_resources, populate_transaction_resources
|
|
49
|
+
from algokit_utils.transactions.fee_coverage import FeeDelta, FeePriority
|
|
50
|
+
from algokit_utils.transactions.helpers import calculate_extra_program_pages
|
|
51
|
+
from algokit_utils.transactions.types import (
|
|
52
|
+
AppCallMethodCallParams,
|
|
53
|
+
AppCallParams,
|
|
54
|
+
AppCreateMethodCallParams,
|
|
55
|
+
AppCreateParams,
|
|
56
|
+
AppCreateSchema,
|
|
57
|
+
AppDeleteMethodCallParams,
|
|
58
|
+
AppDeleteParams,
|
|
59
|
+
AppUpdateMethodCallParams,
|
|
60
|
+
AppUpdateParams,
|
|
61
|
+
AssetConfigParams,
|
|
62
|
+
AssetCreateParams,
|
|
63
|
+
AssetDestroyParams,
|
|
64
|
+
AssetFreezeParams,
|
|
65
|
+
AssetOptInParams,
|
|
66
|
+
AssetOptOutParams,
|
|
67
|
+
AssetTransferParams,
|
|
68
|
+
OfflineKeyRegistrationParams,
|
|
69
|
+
OnlineKeyRegistrationParams,
|
|
70
|
+
PaymentParams,
|
|
71
|
+
TxnParams,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ABIMethod: TypeAlias = arc56.Method
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"MAX_TRANSACTION_GROUP_SIZE",
|
|
78
|
+
"AppCallMethodCallParams",
|
|
79
|
+
"AppCallParams",
|
|
80
|
+
"AppCreateMethodCallParams",
|
|
81
|
+
"AppCreateParams",
|
|
82
|
+
"AppCreateSchema",
|
|
83
|
+
"AppDeleteMethodCallParams",
|
|
84
|
+
"AppDeleteParams",
|
|
85
|
+
"AppMethodCallTransactionArgument",
|
|
86
|
+
"AppUpdateMethodCallParams",
|
|
87
|
+
"AppUpdateParams",
|
|
88
|
+
"AssetConfigParams",
|
|
89
|
+
"AssetCreateParams",
|
|
90
|
+
"AssetDestroyParams",
|
|
91
|
+
"AssetFreezeParams",
|
|
92
|
+
"AssetOptInParams",
|
|
93
|
+
"AssetOptOutParams",
|
|
94
|
+
"AssetTransferParams",
|
|
95
|
+
"BuiltTransactions",
|
|
96
|
+
"ErrorTransformer",
|
|
97
|
+
"ErrorTransformerError",
|
|
98
|
+
"InvalidErrorTransformerValueError",
|
|
99
|
+
"OfflineKeyRegistrationParams",
|
|
100
|
+
"OnlineKeyRegistrationParams",
|
|
101
|
+
"PaymentParams",
|
|
102
|
+
"SendParams",
|
|
103
|
+
"SendTransactionComposerResults",
|
|
104
|
+
"TransactionComposer",
|
|
105
|
+
"TransactionComposerConfig",
|
|
106
|
+
"TransactionComposerError",
|
|
107
|
+
"TransactionComposerParams",
|
|
108
|
+
"TransactionWithSigner",
|
|
109
|
+
"TxnParams",
|
|
110
|
+
"calculate_extra_program_pages",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
AppMethodCallTransactionArgument = Any
|
|
114
|
+
TxnParamTypes = (
|
|
115
|
+
PaymentParams
|
|
116
|
+
| AssetCreateParams
|
|
117
|
+
| AssetConfigParams
|
|
118
|
+
| AssetFreezeParams
|
|
119
|
+
| AssetDestroyParams
|
|
120
|
+
| AssetTransferParams
|
|
121
|
+
| AssetOptInParams
|
|
122
|
+
| AssetOptOutParams
|
|
123
|
+
| AppCreateParams
|
|
124
|
+
| AppUpdateParams
|
|
125
|
+
| AppDeleteParams
|
|
126
|
+
| AppCallParams
|
|
127
|
+
| OnlineKeyRegistrationParams
|
|
128
|
+
| OfflineKeyRegistrationParams
|
|
129
|
+
)
|
|
130
|
+
MethodCallTxnParamTypes = (
|
|
131
|
+
AppCreateMethodCallParams | AppUpdateMethodCallParams | AppDeleteMethodCallParams | AppCallMethodCallParams
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ErrorTransformerError(RuntimeError):
|
|
136
|
+
"""Raised when an error transformer throws."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
ErrorTransformer = Callable[[Exception], Exception]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class InvalidErrorTransformerValueError(RuntimeError):
|
|
143
|
+
"""Raised when an error transformer returns a non-error value."""
|
|
144
|
+
|
|
145
|
+
def __init__(self, original_error: Exception, value: object) -> None:
|
|
146
|
+
super().__init__(
|
|
147
|
+
f"An error transformer returned a non-error value: {value}. "
|
|
148
|
+
f"The original error before any transformation: {original_error}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TransactionComposerError(RuntimeError):
|
|
153
|
+
"""Error raised when transaction composer fails to send transactions.
|
|
154
|
+
|
|
155
|
+
Contains detailed debugging information including simulation traces and sent transactions.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
message: str,
|
|
161
|
+
*,
|
|
162
|
+
cause: Exception | None = None,
|
|
163
|
+
traces: list[SimulateTransactionResult] | None = None,
|
|
164
|
+
sent_transactions: list[Transaction] | None = None,
|
|
165
|
+
simulate_response: algod_models.SimulateResponse | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
super().__init__(message)
|
|
168
|
+
self.__cause__ = cause
|
|
169
|
+
self.traces = traces
|
|
170
|
+
self.sent_transactions = sent_transactions
|
|
171
|
+
self.simulate_response = simulate_response
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(slots=True)
|
|
175
|
+
class TransactionComposerConfig:
|
|
176
|
+
cover_app_call_inner_transaction_fees: bool = False
|
|
177
|
+
populate_app_call_resources: bool = True
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(slots=True)
|
|
181
|
+
class TransactionComposerParams:
|
|
182
|
+
algod: AlgodClient
|
|
183
|
+
get_signer: Callable[[str], TransactionSigner]
|
|
184
|
+
get_suggested_params: Callable[[], algod_models.SuggestedParams] | None = None
|
|
185
|
+
default_validity_window: int | None = None
|
|
186
|
+
app_manager: AppManager | None = None
|
|
187
|
+
error_transformers: list[ErrorTransformer] | None = None
|
|
188
|
+
composer_config: TransactionComposerConfig | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class _BuilderKwargs(TypedDict):
|
|
192
|
+
suggested_params: algod_models.SuggestedParams
|
|
193
|
+
default_validity_window: int
|
|
194
|
+
default_validity_window_is_explicit: bool
|
|
195
|
+
is_localnet: bool
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass(slots=True, frozen=True)
|
|
199
|
+
class TransactionWithSigner:
|
|
200
|
+
txn: Transaction
|
|
201
|
+
signer: TransactionSigner
|
|
202
|
+
method: ABIMethod | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(slots=True, frozen=True)
|
|
206
|
+
class BuiltTransactions:
|
|
207
|
+
transactions: list[Transaction]
|
|
208
|
+
method_calls: dict[int, ABIMethod]
|
|
209
|
+
signers: dict[int, TransactionSigner]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(slots=True, frozen=True)
|
|
213
|
+
class SendTransactionComposerResults:
|
|
214
|
+
tx_ids: list[str]
|
|
215
|
+
transactions: list[Transaction]
|
|
216
|
+
confirmations: list[algod_models.PendingTransactionResponse]
|
|
217
|
+
returns: list[ABIReturn]
|
|
218
|
+
group_id: str | None = None
|
|
219
|
+
simulate_response: algod_models.SimulateResponse | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(slots=True)
|
|
223
|
+
class _QueuedTransaction:
|
|
224
|
+
txn: Transaction | TxnParams
|
|
225
|
+
signer: TransactionSigner | AddressWithTransactionSigner | None
|
|
226
|
+
max_fee: AlgoAmount | None = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(slots=True)
|
|
230
|
+
class _BuiltTxnSpec:
|
|
231
|
+
txn: Transaction
|
|
232
|
+
signer: TransactionSigner | None
|
|
233
|
+
logical_max_fee: AlgoAmount | None
|
|
234
|
+
method: ABIMethod | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(slots=True)
|
|
238
|
+
class _TransactionAnalysis:
|
|
239
|
+
required_fee_delta: FeeDelta | None
|
|
240
|
+
unnamed_resources_accessed: algod_models.SimulateUnnamedResourcesAccessed | None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(slots=True)
|
|
244
|
+
class _GroupAnalysis:
|
|
245
|
+
transactions: list[_TransactionAnalysis]
|
|
246
|
+
unnamed_resources_accessed: algod_models.SimulateUnnamedResourcesAccessed | None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TransactionComposer:
|
|
250
|
+
"""Light-weight transaction composer built on top of algokit_transact."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, params: TransactionComposerParams) -> None:
|
|
253
|
+
self._algod = params.algod
|
|
254
|
+
self._get_signer = params.get_signer
|
|
255
|
+
self._get_suggested_params = params.get_suggested_params or self._algod.suggested_params
|
|
256
|
+
self._config = params.composer_config or TransactionComposerConfig()
|
|
257
|
+
self._error_transformers = params.error_transformers or []
|
|
258
|
+
self._default_validity_window = params.default_validity_window or 10
|
|
259
|
+
self._default_validity_window_is_explicit = params.default_validity_window is not None
|
|
260
|
+
self._app_manager = params.app_manager or AppManager(params.algod)
|
|
261
|
+
|
|
262
|
+
self._queued: list[_QueuedTransaction] = []
|
|
263
|
+
self._transactions_with_signers: list[TransactionWithSigner] | None = None
|
|
264
|
+
self._signed_transactions: list[SignedTransaction] | None = None
|
|
265
|
+
self._raw_built_transactions: list[Transaction] | None = None
|
|
266
|
+
|
|
267
|
+
def clone(self, composer_config: TransactionComposerConfig | None = None) -> "TransactionComposer":
|
|
268
|
+
"""Create a shallow copy of this composer, optionally overriding config flags."""
|
|
269
|
+
config_override = composer_config or self._config
|
|
270
|
+
cloned = TransactionComposer(
|
|
271
|
+
TransactionComposerParams(
|
|
272
|
+
algod=self._algod,
|
|
273
|
+
get_signer=self._get_signer,
|
|
274
|
+
get_suggested_params=self._get_suggested_params,
|
|
275
|
+
default_validity_window=self._default_validity_window
|
|
276
|
+
if self._default_validity_window_is_explicit
|
|
277
|
+
else None,
|
|
278
|
+
app_manager=self._app_manager,
|
|
279
|
+
error_transformers=list(self._error_transformers),
|
|
280
|
+
composer_config=TransactionComposerConfig(
|
|
281
|
+
cover_app_call_inner_transaction_fees=config_override.cover_app_call_inner_transaction_fees,
|
|
282
|
+
populate_app_call_resources=config_override.populate_app_call_resources,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
cloned._queued = [self._clone_entry(entry) for entry in self._queued]
|
|
287
|
+
cloned._raw_built_transactions = list(self._raw_built_transactions) if self._raw_built_transactions else None
|
|
288
|
+
return cloned
|
|
289
|
+
|
|
290
|
+
def register_error_transformer(self, transformer: ErrorTransformer) -> "TransactionComposer":
|
|
291
|
+
self._error_transformers.append(transformer)
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
def add_transaction(self, txn: Transaction, signer: TransactionSigner | None = None) -> "TransactionComposer":
|
|
295
|
+
self._ensure_not_built()
|
|
296
|
+
self._queued.append(_QueuedTransaction(txn=self._sanitize_transaction(txn), signer=signer))
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
def add_payment(self, params: PaymentParams) -> "TransactionComposer":
|
|
300
|
+
self._ensure_not_built()
|
|
301
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def add_asset_create(self, params: AssetCreateParams) -> "TransactionComposer":
|
|
305
|
+
self._ensure_not_built()
|
|
306
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def add_asset_config(self, params: AssetConfigParams) -> "TransactionComposer":
|
|
310
|
+
self._ensure_not_built()
|
|
311
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
312
|
+
return self
|
|
313
|
+
|
|
314
|
+
def add_asset_freeze(self, params: AssetFreezeParams) -> "TransactionComposer":
|
|
315
|
+
self._ensure_not_built()
|
|
316
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def add_asset_destroy(self, params: AssetDestroyParams) -> "TransactionComposer":
|
|
320
|
+
self._ensure_not_built()
|
|
321
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
322
|
+
return self
|
|
323
|
+
|
|
324
|
+
def add_asset_transfer(self, params: AssetTransferParams) -> "TransactionComposer":
|
|
325
|
+
self._ensure_not_built()
|
|
326
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def add_asset_opt_in(self, params: AssetOptInParams) -> "TransactionComposer":
|
|
330
|
+
self._ensure_not_built()
|
|
331
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
332
|
+
return self
|
|
333
|
+
|
|
334
|
+
def add_asset_opt_out(self, params: AssetOptOutParams) -> "TransactionComposer":
|
|
335
|
+
self._ensure_not_built()
|
|
336
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
337
|
+
return self
|
|
338
|
+
|
|
339
|
+
def add_app_create(self, params: AppCreateParams) -> "TransactionComposer":
|
|
340
|
+
self._ensure_not_built()
|
|
341
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
def add_app_update(self, params: AppUpdateParams) -> "TransactionComposer":
|
|
345
|
+
self._ensure_not_built()
|
|
346
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def add_app_delete(self, params: AppDeleteParams) -> "TransactionComposer":
|
|
350
|
+
self._ensure_not_built()
|
|
351
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
352
|
+
return self
|
|
353
|
+
|
|
354
|
+
def add_app_call(self, params: AppCallParams) -> "TransactionComposer":
|
|
355
|
+
self._ensure_not_built()
|
|
356
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
357
|
+
return self
|
|
358
|
+
|
|
359
|
+
def add_app_create_method_call(self, params: AppCreateMethodCallParams) -> "TransactionComposer":
|
|
360
|
+
self._ensure_not_built()
|
|
361
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
362
|
+
return self
|
|
363
|
+
|
|
364
|
+
def add_app_update_method_call(self, params: AppUpdateMethodCallParams) -> "TransactionComposer":
|
|
365
|
+
self._ensure_not_built()
|
|
366
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
367
|
+
return self
|
|
368
|
+
|
|
369
|
+
def add_app_delete_method_call(self, params: AppDeleteMethodCallParams) -> "TransactionComposer":
|
|
370
|
+
self._ensure_not_built()
|
|
371
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
372
|
+
return self
|
|
373
|
+
|
|
374
|
+
def add_app_call_method_call(self, params: AppCallMethodCallParams) -> "TransactionComposer":
|
|
375
|
+
self._ensure_not_built()
|
|
376
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def add_online_key_registration(self, params: OnlineKeyRegistrationParams) -> "TransactionComposer":
|
|
380
|
+
self._ensure_not_built()
|
|
381
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
def add_offline_key_registration(self, params: OfflineKeyRegistrationParams) -> "TransactionComposer":
|
|
385
|
+
self._ensure_not_built()
|
|
386
|
+
self._queued.append(_QueuedTransaction(txn=params, signer=params.signer))
|
|
387
|
+
return self
|
|
388
|
+
|
|
389
|
+
def count(self) -> int:
|
|
390
|
+
return len(self._queued)
|
|
391
|
+
|
|
392
|
+
def rebuild(self) -> BuiltTransactions:
|
|
393
|
+
self._transactions_with_signers = None
|
|
394
|
+
self._signed_transactions = None
|
|
395
|
+
self._raw_built_transactions = None
|
|
396
|
+
return self.build()
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def arc2_note(note: Arc2TransactionNote) -> bytes:
|
|
400
|
+
pattern = r"^[a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}$"
|
|
401
|
+
if not re.match(pattern, note["dapp_name"]):
|
|
402
|
+
raise ValueError(
|
|
403
|
+
"dapp_name must be 5-32 chars, start with alphanumeric, and contain only alphanumeric, _, /, @, ., or -"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
data = note["data"]
|
|
407
|
+
if note["format"] == "j" and isinstance(data, (dict | list)):
|
|
408
|
+
data = json.dumps(data)
|
|
409
|
+
|
|
410
|
+
arc2_payload = f"{note['dapp_name']}:{note['format']}{data}"
|
|
411
|
+
return arc2_payload.encode("utf-8")
|
|
412
|
+
|
|
413
|
+
def add_transaction_composer(self, composer: "TransactionComposer") -> "TransactionComposer":
|
|
414
|
+
self._ensure_not_built()
|
|
415
|
+
current_size = len(self._queued)
|
|
416
|
+
composer_size = len(composer._queued) # noqa: SLF001
|
|
417
|
+
new_size = current_size + composer_size
|
|
418
|
+
if new_size > MAX_TRANSACTION_GROUP_SIZE:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
"Adding transactions from composer would exceed the maximum group size. "
|
|
421
|
+
f"Current: {current_size}, Adding: {composer_size}, "
|
|
422
|
+
f"Maximum: {MAX_TRANSACTION_GROUP_SIZE}"
|
|
423
|
+
)
|
|
424
|
+
for entry in composer._queued: # noqa: SLF001
|
|
425
|
+
self._queued.append(self._clone_entry(entry))
|
|
426
|
+
return self
|
|
427
|
+
|
|
428
|
+
def build(self) -> BuiltTransactions:
|
|
429
|
+
"""Build transactions with grouping, resource population, and fee adjustments applied."""
|
|
430
|
+
self._ensure_built()
|
|
431
|
+
assert self._transactions_with_signers is not None
|
|
432
|
+
transactions = [entry.txn for entry in self._transactions_with_signers]
|
|
433
|
+
signers = {index: entry.signer for index, entry in enumerate(self._transactions_with_signers)}
|
|
434
|
+
method_calls = {
|
|
435
|
+
index: entry.method for index, entry in enumerate(self._transactions_with_signers) if entry.method
|
|
436
|
+
}
|
|
437
|
+
return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
|
|
438
|
+
|
|
439
|
+
def build_transactions(self) -> BuiltTransactions:
|
|
440
|
+
"""Build queued transactions without resource population or grouping.
|
|
441
|
+
|
|
442
|
+
Returns raw transactions, method call metadata, and any explicit signers. This does not
|
|
443
|
+
populate unnamed resources or adjust fees, and it leaves grouping unchanged.
|
|
444
|
+
"""
|
|
445
|
+
if not self._queued:
|
|
446
|
+
raise ValueError("Cannot build an empty transaction group")
|
|
447
|
+
|
|
448
|
+
suggested_params = self._get_suggested_params()
|
|
449
|
+
genesis_id = getattr(suggested_params, "genesis_id", None)
|
|
450
|
+
if genesis_id is None:
|
|
451
|
+
genesis_id = getattr(suggested_params, "gen", "")
|
|
452
|
+
is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
|
|
453
|
+
|
|
454
|
+
built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
|
|
455
|
+
transactions = [entry.txn for entry in built_entries]
|
|
456
|
+
self._raw_built_transactions = list(transactions)
|
|
457
|
+
signers = {index: entry.signer for index, entry in enumerate(built_entries) if entry.signer is not None}
|
|
458
|
+
return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
|
|
459
|
+
|
|
460
|
+
def gather_signatures(self) -> list[SignedTransaction]:
|
|
461
|
+
self._ensure_built()
|
|
462
|
+
if self._signed_transactions is None:
|
|
463
|
+
self._signed_transactions = self._sign_transactions(self._transactions_with_signers or [])
|
|
464
|
+
return self._signed_transactions
|
|
465
|
+
|
|
466
|
+
def send(self, params: SendParams | None = None) -> SendTransactionComposerResults:
|
|
467
|
+
"""Compose the transaction group and send it to the network."""
|
|
468
|
+
params = params or SendParams()
|
|
469
|
+
|
|
470
|
+
# Update config from params if provided
|
|
471
|
+
cover_flag = params.get("cover_app_call_inner_transaction_fees")
|
|
472
|
+
populate_flag = params.get("populate_app_call_resources")
|
|
473
|
+
effective_cover = bool(cover_flag)
|
|
474
|
+
effective_populate = bool(populate_flag) if populate_flag is not None else True
|
|
475
|
+
if (
|
|
476
|
+
effective_cover != self._config.cover_app_call_inner_transaction_fees
|
|
477
|
+
or effective_populate != self._config.populate_app_call_resources
|
|
478
|
+
):
|
|
479
|
+
self._config = TransactionComposerConfig(
|
|
480
|
+
cover_app_call_inner_transaction_fees=effective_cover,
|
|
481
|
+
populate_app_call_resources=effective_populate,
|
|
482
|
+
)
|
|
483
|
+
# Reset built state to force rebuild with new config
|
|
484
|
+
self._transactions_with_signers = None
|
|
485
|
+
self._signed_transactions = None
|
|
486
|
+
self._raw_built_transactions = None
|
|
487
|
+
|
|
488
|
+
# Build and sign transactions - let validation errors bubble up as-is
|
|
489
|
+
signed_transactions = self.gather_signatures()
|
|
490
|
+
|
|
491
|
+
if config.debug and config.trace_all and config.project_root:
|
|
492
|
+
try:
|
|
493
|
+
self.simulate(result_on_failure=True)
|
|
494
|
+
except Exception:
|
|
495
|
+
config.logger.debug(
|
|
496
|
+
"Failed to simulate and persist trace for debugging",
|
|
497
|
+
exc_info=True,
|
|
498
|
+
extra={"suppress_log": params.get("suppress_log")},
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Send transactions and handle network errors
|
|
502
|
+
try:
|
|
503
|
+
blobs = encode_signed_transactions(signed_transactions)
|
|
504
|
+
self._algod.send_raw_transaction(blobs)
|
|
505
|
+
|
|
506
|
+
tx_ids = [get_transaction_id(entry.txn) for entry in self._transactions_with_signers or []]
|
|
507
|
+
group_id = self._group_id()
|
|
508
|
+
if not params.get("suppress_log") and tx_ids:
|
|
509
|
+
if len(tx_ids) > 1:
|
|
510
|
+
config.logger.info(
|
|
511
|
+
"Sent group of %s transactions (%s)",
|
|
512
|
+
len(tx_ids),
|
|
513
|
+
group_id or "no-group",
|
|
514
|
+
extra={"suppress_log": params.get("suppress_log")},
|
|
515
|
+
)
|
|
516
|
+
config.logger.debug(
|
|
517
|
+
"Transaction IDs (%s): %s",
|
|
518
|
+
group_id or "no-group",
|
|
519
|
+
tx_ids,
|
|
520
|
+
extra={"suppress_log": params.get("suppress_log")},
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
txn = (self._transactions_with_signers or [])[0].txn
|
|
524
|
+
config.logger.info(
|
|
525
|
+
"Sent transaction ID %s %s from %s",
|
|
526
|
+
tx_ids[0],
|
|
527
|
+
txn.transaction_type,
|
|
528
|
+
txn.sender,
|
|
529
|
+
extra={"suppress_log": params.get("suppress_log")},
|
|
530
|
+
)
|
|
531
|
+
confirmations = self._wait_for_confirmations(tx_ids, params)
|
|
532
|
+
abi_returns = self._parse_abi_return_values(confirmations)
|
|
533
|
+
return SendTransactionComposerResults(
|
|
534
|
+
tx_ids=tx_ids,
|
|
535
|
+
transactions=[entry.txn for entry in self._transactions_with_signers or []],
|
|
536
|
+
confirmations=confirmations,
|
|
537
|
+
returns=abi_returns,
|
|
538
|
+
group_id=group_id,
|
|
539
|
+
)
|
|
540
|
+
except Exception as err:
|
|
541
|
+
sent_transactions = self._resolve_error_transactions()
|
|
542
|
+
simulate_response: algod_models.SimulateResponse | None = None
|
|
543
|
+
traces: list[SimulateTransactionResult] = []
|
|
544
|
+
|
|
545
|
+
if config.debug and sent_transactions:
|
|
546
|
+
simulate_response, traces = self._simulate_error_context(
|
|
547
|
+
sent_transactions,
|
|
548
|
+
suppress_log=params.get("suppress_log"),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if config.debug and config.project_root and not config.trace_all and simulate_response is None:
|
|
552
|
+
from algokit_utils._debugging import simulate_and_persist_response
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
simulate_and_persist_response(
|
|
556
|
+
self,
|
|
557
|
+
config.project_root,
|
|
558
|
+
self._algod,
|
|
559
|
+
buffer_size_mb=config.trace_buffer_size_mb,
|
|
560
|
+
)
|
|
561
|
+
except Exception:
|
|
562
|
+
config.logger.debug(
|
|
563
|
+
"Failed to simulate and persist trace for debugging",
|
|
564
|
+
exc_info=True,
|
|
565
|
+
extra={"suppress_log": params.get("suppress_log")},
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
interpreted = self._interpret_error(err)
|
|
569
|
+
composer_error = self._create_composer_error(interpreted, sent_transactions, simulate_response, traces)
|
|
570
|
+
raise self._transform_error(composer_error) from err
|
|
571
|
+
|
|
572
|
+
def simulate(
|
|
573
|
+
self,
|
|
574
|
+
*,
|
|
575
|
+
skip_signatures: bool = False,
|
|
576
|
+
result_on_failure: bool = False,
|
|
577
|
+
**raw_options: Any,
|
|
578
|
+
) -> SendTransactionComposerResults:
|
|
579
|
+
"""Compose the transaction group and simulate execution without submitting to the network.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
skip_signatures: Whether to skip signatures for all built transactions and use an empty signer instead.
|
|
583
|
+
This will set `allow_empty_signatures` and `fix_signers` when sending the request to algod.
|
|
584
|
+
result_on_failure: Whether to return the result on simulation failure instead of throwing an error.
|
|
585
|
+
Defaults to False (throws on failure).
|
|
586
|
+
**raw_options: Additional options to pass to the simulate request.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
SendTransactionComposerResults containing simulation results.
|
|
590
|
+
"""
|
|
591
|
+
try:
|
|
592
|
+
persist_trace = bool(raw_options.pop("_persist_trace", True))
|
|
593
|
+
txns_with_signers: list[TransactionWithSigner]
|
|
594
|
+
if "throw_on_failure" in raw_options:
|
|
595
|
+
raw_options.pop("throw_on_failure")
|
|
596
|
+
effective_throw_on_failure = not result_on_failure
|
|
597
|
+
if skip_signatures:
|
|
598
|
+
raw_options.setdefault("allow_empty_signatures", True)
|
|
599
|
+
raw_options.setdefault("fix_signers", True)
|
|
600
|
+
if "allow_more_logs" in raw_options:
|
|
601
|
+
raw_options["allow_more_logging"] = raw_options.pop("allow_more_logs")
|
|
602
|
+
if "simulation_round" in raw_options:
|
|
603
|
+
raw_options["round_"] = raw_options.pop("simulation_round")
|
|
604
|
+
|
|
605
|
+
txns_with_signers = self._build_transactions_for_simulation()
|
|
606
|
+
|
|
607
|
+
if config.debug:
|
|
608
|
+
raw_options.setdefault("allow_more_logging", True)
|
|
609
|
+
raw_options.setdefault(
|
|
610
|
+
"exec_trace_config",
|
|
611
|
+
algod_models.SimulateTraceConfig(
|
|
612
|
+
enable=True,
|
|
613
|
+
scratch_change=True,
|
|
614
|
+
stack_change=True,
|
|
615
|
+
state_change=True,
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
empty_signer: TransactionSigner = make_empty_transaction_signer()
|
|
620
|
+
signing_entries = [
|
|
621
|
+
TransactionWithSigner(
|
|
622
|
+
txn=entry.txn,
|
|
623
|
+
signer=empty_signer if skip_signatures else entry.signer,
|
|
624
|
+
)
|
|
625
|
+
for entry in txns_with_signers
|
|
626
|
+
]
|
|
627
|
+
signed_transactions = self._sign_transactions(signing_entries)
|
|
628
|
+
|
|
629
|
+
request = algod_models.SimulateRequest(
|
|
630
|
+
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
|
|
631
|
+
**raw_options,
|
|
632
|
+
)
|
|
633
|
+
response = self._algod.simulate_transactions(request)
|
|
634
|
+
|
|
635
|
+
if response.txn_groups and response.txn_groups[0].failure_message and effective_throw_on_failure:
|
|
636
|
+
raise RuntimeError(response.txn_groups[0].failure_message)
|
|
637
|
+
|
|
638
|
+
tx_ids = [get_transaction_id(entry.txn) for entry in txns_with_signers]
|
|
639
|
+
group = response.txn_groups[0] if response.txn_groups else None
|
|
640
|
+
confirmations = [result.txn_result for result in (group.txn_results if group else [])]
|
|
641
|
+
method_calls = {index: entry.method for index, entry in enumerate(txns_with_signers) if entry.method}
|
|
642
|
+
abi_returns = self._parse_abi_return_values(confirmations, method_calls)
|
|
643
|
+
result = SendTransactionComposerResults(
|
|
644
|
+
tx_ids=tx_ids,
|
|
645
|
+
transactions=[entry.txn for entry in txns_with_signers],
|
|
646
|
+
confirmations=confirmations,
|
|
647
|
+
returns=abi_returns,
|
|
648
|
+
group_id=(
|
|
649
|
+
base64.b64encode(txns_with_signers[0].txn.group).decode()
|
|
650
|
+
if txns_with_signers and txns_with_signers[0].txn.group
|
|
651
|
+
else None
|
|
652
|
+
),
|
|
653
|
+
simulate_response=response,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if config.debug and config.project_root and config.trace_all and persist_trace:
|
|
657
|
+
from algokit_utils._debugging import simulate_and_persist_response
|
|
658
|
+
|
|
659
|
+
try:
|
|
660
|
+
simulate_and_persist_response(
|
|
661
|
+
self,
|
|
662
|
+
config.project_root,
|
|
663
|
+
self._algod,
|
|
664
|
+
buffer_size_mb=config.trace_buffer_size_mb,
|
|
665
|
+
result=result,
|
|
666
|
+
)
|
|
667
|
+
except Exception:
|
|
668
|
+
config.logger.debug("Failed to persist simulation trace", exc_info=True)
|
|
669
|
+
|
|
670
|
+
return result
|
|
671
|
+
except Exception as err:
|
|
672
|
+
interpreted = self._interpret_error(err)
|
|
673
|
+
raise self._transform_error(interpreted) from err
|
|
674
|
+
|
|
675
|
+
def _ensure_not_built(self) -> None:
|
|
676
|
+
if self._transactions_with_signers is not None:
|
|
677
|
+
raise RuntimeError("Transactions have already been built")
|
|
678
|
+
if len(self._queued) >= MAX_TRANSACTION_GROUP_SIZE:
|
|
679
|
+
raise ValueError("Transaction group size exceeds maximum limit")
|
|
680
|
+
|
|
681
|
+
def _build_txn_specs(
|
|
682
|
+
self,
|
|
683
|
+
suggested_params: algod_models.SuggestedParams,
|
|
684
|
+
*,
|
|
685
|
+
is_localnet: bool,
|
|
686
|
+
) -> tuple[list[_BuiltTxnSpec], dict[int, ABIMethod]]:
|
|
687
|
+
if not self._queued:
|
|
688
|
+
raise ValueError("Cannot build an empty transaction group")
|
|
689
|
+
|
|
690
|
+
built_entries: list[_BuiltTxnSpec] = []
|
|
691
|
+
method_calls: dict[int, ABIMethod] = {}
|
|
692
|
+
|
|
693
|
+
for entry in self._queued:
|
|
694
|
+
sender = entry.txn.sender if hasattr(entry.txn, "sender") else None
|
|
695
|
+
override_signer = self._resolve_param_signer(entry.signer, sender)
|
|
696
|
+
if isinstance(entry.txn, Transaction):
|
|
697
|
+
txn = self._sanitize_transaction(entry.txn)
|
|
698
|
+
built_entries.append(
|
|
699
|
+
_BuiltTxnSpec(txn=txn, signer=override_signer, logical_max_fee=entry.max_fee, method=None)
|
|
700
|
+
)
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
specs = self._build_txn_from_params(entry.txn, suggested_params, is_localnet=is_localnet)
|
|
704
|
+
for spec in specs:
|
|
705
|
+
resolved_signer = spec.signer or override_signer
|
|
706
|
+
if resolved_signer is None:
|
|
707
|
+
raise ValueError("Signer is required for transaction in composer queue")
|
|
708
|
+
index = len(built_entries)
|
|
709
|
+
built_entries.append(
|
|
710
|
+
_BuiltTxnSpec(
|
|
711
|
+
txn=self._sanitize_transaction(spec.txn),
|
|
712
|
+
signer=resolved_signer,
|
|
713
|
+
logical_max_fee=spec.logical_max_fee,
|
|
714
|
+
method=spec.method,
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
if spec.method:
|
|
718
|
+
method_calls[index] = spec.method
|
|
719
|
+
|
|
720
|
+
return built_entries, method_calls
|
|
721
|
+
|
|
722
|
+
def _ensure_built(self) -> None:
|
|
723
|
+
if self._transactions_with_signers is not None:
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
suggested_params = self._get_suggested_params()
|
|
727
|
+
genesis_id = getattr(suggested_params, "genesis_id", None)
|
|
728
|
+
if genesis_id is None:
|
|
729
|
+
genesis_id = getattr(suggested_params, "gen", "")
|
|
730
|
+
is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
|
|
731
|
+
|
|
732
|
+
built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
|
|
733
|
+
transactions = [entry.txn for entry in built_entries]
|
|
734
|
+
self._raw_built_transactions = list(transactions)
|
|
735
|
+
logical_max_fees = [entry.logical_max_fee for entry in built_entries]
|
|
736
|
+
|
|
737
|
+
needs_analysis = (
|
|
738
|
+
self._config.cover_app_call_inner_transaction_fees or self._config.populate_app_call_resources
|
|
739
|
+
) and any(txn.transaction_type == TransactionType.AppCall for txn in transactions)
|
|
740
|
+
if needs_analysis:
|
|
741
|
+
group_analysis = self._analyze_group_requirements(
|
|
742
|
+
transactions,
|
|
743
|
+
logical_max_fees,
|
|
744
|
+
suggested_params,
|
|
745
|
+
self._config,
|
|
746
|
+
)
|
|
747
|
+
self._populate_transaction_and_group_resources(transactions, group_analysis, logical_max_fees)
|
|
748
|
+
|
|
749
|
+
grouped = group_transactions(transactions)
|
|
750
|
+
self._transactions_with_signers = [
|
|
751
|
+
TransactionWithSigner(
|
|
752
|
+
txn=grouped[index],
|
|
753
|
+
signer=cast(TransactionSigner, entry.signer),
|
|
754
|
+
method=method_calls.get(index),
|
|
755
|
+
)
|
|
756
|
+
for index, entry in enumerate(built_entries)
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
def _build_transactions_for_simulation(self) -> list[TransactionWithSigner]:
|
|
760
|
+
if self._transactions_with_signers is None:
|
|
761
|
+
suggested_params = self._get_suggested_params()
|
|
762
|
+
genesis_id = getattr(suggested_params, "genesis_id", None)
|
|
763
|
+
if genesis_id is None:
|
|
764
|
+
genesis_id = getattr(suggested_params, "gen", "")
|
|
765
|
+
is_localnet = ClientManager.genesis_id_is_localnet(genesis_id or "")
|
|
766
|
+
|
|
767
|
+
built_entries, method_calls = self._build_txn_specs(suggested_params, is_localnet=is_localnet)
|
|
768
|
+
transactions = [entry.txn for entry in built_entries]
|
|
769
|
+
if len(transactions) > 1:
|
|
770
|
+
transactions = group_transactions(transactions)
|
|
771
|
+
return [
|
|
772
|
+
TransactionWithSigner(
|
|
773
|
+
txn=transactions[index],
|
|
774
|
+
signer=cast(TransactionSigner, entry.signer),
|
|
775
|
+
method=method_calls.get(index),
|
|
776
|
+
)
|
|
777
|
+
for index, entry in enumerate(built_entries)
|
|
778
|
+
]
|
|
779
|
+
|
|
780
|
+
return self._transactions_with_signers
|
|
781
|
+
|
|
782
|
+
def _analyze_group_requirements( # noqa: C901, PLR0912
|
|
783
|
+
self,
|
|
784
|
+
transactions: list[Transaction],
|
|
785
|
+
logical_max_fees: Sequence[AlgoAmount | None],
|
|
786
|
+
suggested_params: algod_models.SuggestedParams,
|
|
787
|
+
config: TransactionComposerConfig,
|
|
788
|
+
) -> _GroupAnalysis:
|
|
789
|
+
app_call_indexes_without_max_fees: list[int] = []
|
|
790
|
+
transactions_to_simulate: list[Transaction] = []
|
|
791
|
+
for index, txn in enumerate(transactions):
|
|
792
|
+
txn_to_simulate = replace(txn, group=None)
|
|
793
|
+
if config.cover_app_call_inner_transaction_fees and txn.transaction_type == TransactionType.AppCall:
|
|
794
|
+
logical_max_fee = logical_max_fees[index]
|
|
795
|
+
if logical_max_fee is None:
|
|
796
|
+
app_call_indexes_without_max_fees.append(index)
|
|
797
|
+
else:
|
|
798
|
+
txn_to_simulate = replace(txn_to_simulate, fee=logical_max_fee.micro_algo)
|
|
799
|
+
transactions_to_simulate.append(txn_to_simulate)
|
|
800
|
+
|
|
801
|
+
if config.cover_app_call_inner_transaction_fees and app_call_indexes_without_max_fees:
|
|
802
|
+
indexes = ", ".join(str(index) for index in app_call_indexes_without_max_fees)
|
|
803
|
+
raise ValueError(
|
|
804
|
+
"Please provide a `max_fee` for each app call transaction when "
|
|
805
|
+
"cover_app_call_inner_transaction_fees is enabled. "
|
|
806
|
+
f"Required for transaction {indexes}"
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
if len(transactions_to_simulate) > 1:
|
|
810
|
+
transactions_to_simulate = group_transactions(transactions_to_simulate)
|
|
811
|
+
|
|
812
|
+
empty_signer: TransactionSigner = make_empty_transaction_signer()
|
|
813
|
+
signed_transactions = self._sign_transactions(
|
|
814
|
+
[TransactionWithSigner(txn=txn, signer=empty_signer) for txn in transactions_to_simulate]
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
simulate_request = algod_models.SimulateRequest(
|
|
818
|
+
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
|
|
819
|
+
allow_unnamed_resources=True,
|
|
820
|
+
allow_empty_signatures=True,
|
|
821
|
+
fix_signers=True,
|
|
822
|
+
allow_more_logging=True,
|
|
823
|
+
exec_trace_config=algod_models.SimulateTraceConfig(
|
|
824
|
+
enable=True,
|
|
825
|
+
scratch_change=True,
|
|
826
|
+
stack_change=True,
|
|
827
|
+
state_change=True,
|
|
828
|
+
),
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
response = self._algod.simulate_transactions(simulate_request)
|
|
832
|
+
group_response = response.txn_groups[0]
|
|
833
|
+
|
|
834
|
+
if group_response.failure_message:
|
|
835
|
+
if config.cover_app_call_inner_transaction_fees and "fee too small" in group_response.failure_message:
|
|
836
|
+
raise ValueError(
|
|
837
|
+
"Fees were too small to resolve execution info via simulate. "
|
|
838
|
+
"You may need to increase an app call transaction maxFee."
|
|
839
|
+
)
|
|
840
|
+
raise ValueError(
|
|
841
|
+
"Error resolving execution info via simulate in transaction "
|
|
842
|
+
f"{group_response.failed_at or []}: {group_response.failure_message}"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
txn_analysis_results: list[_TransactionAnalysis] = []
|
|
846
|
+
for index, simulate_txn_result in enumerate(group_response.txn_results):
|
|
847
|
+
txn = transactions[index]
|
|
848
|
+
required_fee_delta: FeeDelta | None = None
|
|
849
|
+
if config.cover_app_call_inner_transaction_fees:
|
|
850
|
+
min_txn_fee = calculate_fee(txn, fee_per_byte=suggested_params.fee, min_fee=suggested_params.min_fee)
|
|
851
|
+
txn_fee = txn.fee or 0
|
|
852
|
+
txn_fee_delta = FeeDelta.from_int(min_txn_fee - txn_fee)
|
|
853
|
+
if txn.transaction_type == TransactionType.AppCall:
|
|
854
|
+
inner_delta = calculate_inner_fee_delta(
|
|
855
|
+
simulate_txn_result.txn_result.inner_txns, suggested_params.min_fee
|
|
856
|
+
)
|
|
857
|
+
required_fee_delta = FeeDelta.add(inner_delta, txn_fee_delta)
|
|
858
|
+
else:
|
|
859
|
+
required_fee_delta = txn_fee_delta
|
|
860
|
+
|
|
861
|
+
unnamed_resources = (
|
|
862
|
+
simulate_txn_result.unnamed_resources_accessed if config.populate_app_call_resources else None
|
|
863
|
+
)
|
|
864
|
+
txn_analysis_results.append(
|
|
865
|
+
_TransactionAnalysis(
|
|
866
|
+
required_fee_delta=required_fee_delta,
|
|
867
|
+
unnamed_resources_accessed=unnamed_resources,
|
|
868
|
+
)
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
group_resources = group_response.unnamed_resources_accessed if config.populate_app_call_resources else None
|
|
872
|
+
if group_resources:
|
|
873
|
+
group_resources.accounts = sorted(group_resources.accounts or [])
|
|
874
|
+
group_resources.assets = sorted(group_resources.assets or [])
|
|
875
|
+
group_resources.apps = sorted(group_resources.apps or [])
|
|
876
|
+
group_resources.boxes = sorted(
|
|
877
|
+
group_resources.boxes or [],
|
|
878
|
+
key=lambda box: (box.app_id, box.name),
|
|
879
|
+
)
|
|
880
|
+
group_resources.app_locals = sorted(
|
|
881
|
+
group_resources.app_locals or [],
|
|
882
|
+
key=lambda entry: (entry.app_id, entry.address),
|
|
883
|
+
)
|
|
884
|
+
group_resources.asset_holdings = sorted(
|
|
885
|
+
group_resources.asset_holdings or [],
|
|
886
|
+
key=lambda entry: (entry.asset_id, entry.address),
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
return _GroupAnalysis(transactions=txn_analysis_results, unnamed_resources_accessed=group_resources)
|
|
890
|
+
|
|
891
|
+
def _populate_transaction_and_group_resources( # noqa: C901, PLR0912, PLR0915
|
|
892
|
+
self,
|
|
893
|
+
transactions: list[Transaction],
|
|
894
|
+
group_analysis: _GroupAnalysis,
|
|
895
|
+
logical_max_fees: Sequence[AlgoAmount | None],
|
|
896
|
+
) -> None:
|
|
897
|
+
if not group_analysis:
|
|
898
|
+
return
|
|
899
|
+
|
|
900
|
+
surplus_group_fees = 0
|
|
901
|
+
transaction_analysis_list: list[dict[str, Any]] = []
|
|
902
|
+
|
|
903
|
+
for group_index, txn_analysis in enumerate(group_analysis.transactions):
|
|
904
|
+
fee_delta = txn_analysis.required_fee_delta
|
|
905
|
+
if fee_delta and FeeDelta.is_surplus(fee_delta):
|
|
906
|
+
surplus_group_fees += FeeDelta.amount(fee_delta)
|
|
907
|
+
|
|
908
|
+
txn = transactions[group_index]
|
|
909
|
+
max_fee_source = logical_max_fees[group_index]
|
|
910
|
+
max_fee_amount: int | None
|
|
911
|
+
if max_fee_source is not None:
|
|
912
|
+
max_fee_amount = max_fee_source.micro_algo
|
|
913
|
+
elif not self._config.cover_app_call_inner_transaction_fees:
|
|
914
|
+
txn_fee = txn.fee or 0
|
|
915
|
+
max_fee_amount = txn_fee if txn_fee > 0 else None
|
|
916
|
+
else:
|
|
917
|
+
max_fee_amount = None
|
|
918
|
+
is_immutable_fee = max_fee_amount is not None and max_fee_amount == (txn.fee or 0)
|
|
919
|
+
|
|
920
|
+
priority = FeePriority.Covered
|
|
921
|
+
if fee_delta and FeeDelta.is_deficit(fee_delta):
|
|
922
|
+
deficit_amount = FeeDelta.amount(fee_delta)
|
|
923
|
+
if is_immutable_fee or txn.transaction_type != TransactionType.AppCall:
|
|
924
|
+
priority = FeePriority.ImmutableDeficit(deficit_amount)
|
|
925
|
+
else:
|
|
926
|
+
priority = FeePriority.ModifiableDeficit(deficit_amount)
|
|
927
|
+
|
|
928
|
+
transaction_analysis_list.append(
|
|
929
|
+
{
|
|
930
|
+
"group_index": group_index,
|
|
931
|
+
"required_fee_delta": fee_delta,
|
|
932
|
+
"priority": priority,
|
|
933
|
+
"unnamed_resources_accessed": txn_analysis.unnamed_resources_accessed,
|
|
934
|
+
"logical_max_fee": max_fee_amount,
|
|
935
|
+
}
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
transaction_analysis_list.sort(key=lambda item: item["priority"], reverse=True)
|
|
939
|
+
indexes_with_access_references: list[int] = []
|
|
940
|
+
|
|
941
|
+
for item in transaction_analysis_list:
|
|
942
|
+
group_index = item["group_index"]
|
|
943
|
+
logical_max_fee = item["logical_max_fee"]
|
|
944
|
+
required_fee_delta: FeeDelta | None = item["required_fee_delta"]
|
|
945
|
+
unnamed_resources_accessed = item["unnamed_resources_accessed"]
|
|
946
|
+
|
|
947
|
+
if required_fee_delta and FeeDelta.is_deficit(required_fee_delta):
|
|
948
|
+
deficit_amount = FeeDelta.amount(required_fee_delta)
|
|
949
|
+
additional_fee_delta: FeeDelta | None
|
|
950
|
+
|
|
951
|
+
if surplus_group_fees == 0:
|
|
952
|
+
additional_fee_delta = required_fee_delta
|
|
953
|
+
elif surplus_group_fees >= deficit_amount:
|
|
954
|
+
surplus_group_fees -= deficit_amount
|
|
955
|
+
additional_fee_delta = None
|
|
956
|
+
else:
|
|
957
|
+
additional_fee_delta = FeeDelta.from_int(deficit_amount - surplus_group_fees)
|
|
958
|
+
surplus_group_fees = 0
|
|
959
|
+
|
|
960
|
+
if additional_fee_delta and FeeDelta.is_deficit(additional_fee_delta):
|
|
961
|
+
additional_deficit_amount = FeeDelta.amount(additional_fee_delta)
|
|
962
|
+
txn = transactions[group_index]
|
|
963
|
+
|
|
964
|
+
if txn.transaction_type != TransactionType.AppCall:
|
|
965
|
+
raise ValueError(
|
|
966
|
+
"An additional fee of "
|
|
967
|
+
f"{additional_deficit_amount} µALGO is required for non app call transaction {group_index}",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
current_fee = txn.fee or 0
|
|
971
|
+
transaction_fee = current_fee + additional_deficit_amount
|
|
972
|
+
if logical_max_fee is not None and transaction_fee > logical_max_fee:
|
|
973
|
+
raise ValueError(
|
|
974
|
+
"Calculated transaction fee "
|
|
975
|
+
f"{transaction_fee} µALGO is greater than max of {logical_max_fee} "
|
|
976
|
+
f"for transaction {group_index}"
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
transactions[group_index] = replace(txn, fee=transaction_fee)
|
|
980
|
+
|
|
981
|
+
if unnamed_resources_accessed and transactions[group_index].transaction_type == TransactionType.AppCall:
|
|
982
|
+
has_access_references = bool(transactions[group_index].application_call.access_references)
|
|
983
|
+
if not has_access_references:
|
|
984
|
+
transactions[group_index] = populate_transaction_resources(
|
|
985
|
+
transactions[group_index], unnamed_resources_accessed, group_index
|
|
986
|
+
)
|
|
987
|
+
else:
|
|
988
|
+
indexes_with_access_references.append(group_index)
|
|
989
|
+
|
|
990
|
+
if indexes_with_access_references:
|
|
991
|
+
config.logger.warning(
|
|
992
|
+
"Resource population will be skipped for transaction indexes %s as they use access references.",
|
|
993
|
+
indexes_with_access_references,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
if group_analysis.unnamed_resources_accessed:
|
|
997
|
+
populate_group_resources(transactions, group_analysis.unnamed_resources_accessed)
|
|
998
|
+
|
|
999
|
+
def _build_txn_from_params( # noqa: C901, PLR0911, PLR0912, PLR0915
|
|
1000
|
+
self,
|
|
1001
|
+
params: TxnParams,
|
|
1002
|
+
suggested_params: algod_models.SuggestedParams,
|
|
1003
|
+
*,
|
|
1004
|
+
is_localnet: bool,
|
|
1005
|
+
) -> list[_BuiltTxnSpec]:
|
|
1006
|
+
builder_kwargs: _BuilderKwargs = {
|
|
1007
|
+
"suggested_params": suggested_params,
|
|
1008
|
+
"default_validity_window": self._default_validity_window,
|
|
1009
|
+
"default_validity_window_is_explicit": self._default_validity_window_is_explicit,
|
|
1010
|
+
"is_localnet": is_localnet,
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if isinstance(params, PaymentParams):
|
|
1014
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1015
|
+
built = build_payment_transaction(params, **builder_kwargs)
|
|
1016
|
+
return [
|
|
1017
|
+
_BuiltTxnSpec(
|
|
1018
|
+
txn=built.txn,
|
|
1019
|
+
signer=signer,
|
|
1020
|
+
logical_max_fee=built.logical_max_fee,
|
|
1021
|
+
)
|
|
1022
|
+
]
|
|
1023
|
+
elif isinstance(params, AssetCreateParams):
|
|
1024
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1025
|
+
built = build_asset_create_transaction(params, **builder_kwargs)
|
|
1026
|
+
return [
|
|
1027
|
+
_BuiltTxnSpec(
|
|
1028
|
+
txn=built.txn,
|
|
1029
|
+
signer=signer,
|
|
1030
|
+
logical_max_fee=built.logical_max_fee,
|
|
1031
|
+
)
|
|
1032
|
+
]
|
|
1033
|
+
elif isinstance(params, AssetConfigParams):
|
|
1034
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1035
|
+
built = build_asset_config_transaction(params, **builder_kwargs)
|
|
1036
|
+
return [
|
|
1037
|
+
_BuiltTxnSpec(
|
|
1038
|
+
txn=built.txn,
|
|
1039
|
+
signer=signer,
|
|
1040
|
+
logical_max_fee=built.logical_max_fee,
|
|
1041
|
+
)
|
|
1042
|
+
]
|
|
1043
|
+
elif isinstance(params, AssetFreezeParams):
|
|
1044
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1045
|
+
built = build_asset_freeze_transaction(params, **builder_kwargs)
|
|
1046
|
+
return [
|
|
1047
|
+
_BuiltTxnSpec(
|
|
1048
|
+
txn=built.txn,
|
|
1049
|
+
signer=signer,
|
|
1050
|
+
logical_max_fee=built.logical_max_fee,
|
|
1051
|
+
)
|
|
1052
|
+
]
|
|
1053
|
+
elif isinstance(params, AssetDestroyParams):
|
|
1054
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1055
|
+
built = build_asset_destroy_transaction(params, **builder_kwargs)
|
|
1056
|
+
return [
|
|
1057
|
+
_BuiltTxnSpec(
|
|
1058
|
+
txn=built.txn,
|
|
1059
|
+
signer=signer,
|
|
1060
|
+
logical_max_fee=built.logical_max_fee,
|
|
1061
|
+
)
|
|
1062
|
+
]
|
|
1063
|
+
elif isinstance(params, AssetTransferParams):
|
|
1064
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1065
|
+
built = build_asset_transfer_transaction(params, **builder_kwargs)
|
|
1066
|
+
return [
|
|
1067
|
+
_BuiltTxnSpec(
|
|
1068
|
+
txn=built.txn,
|
|
1069
|
+
signer=signer,
|
|
1070
|
+
logical_max_fee=built.logical_max_fee,
|
|
1071
|
+
)
|
|
1072
|
+
]
|
|
1073
|
+
elif isinstance(params, AssetOptInParams):
|
|
1074
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1075
|
+
built = build_asset_opt_in_transaction(params, **builder_kwargs)
|
|
1076
|
+
return [
|
|
1077
|
+
_BuiltTxnSpec(
|
|
1078
|
+
txn=built.txn,
|
|
1079
|
+
signer=signer,
|
|
1080
|
+
logical_max_fee=built.logical_max_fee,
|
|
1081
|
+
)
|
|
1082
|
+
]
|
|
1083
|
+
elif isinstance(params, AssetOptOutParams):
|
|
1084
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1085
|
+
built = build_asset_opt_out_transaction(params, **builder_kwargs)
|
|
1086
|
+
return [
|
|
1087
|
+
_BuiltTxnSpec(
|
|
1088
|
+
txn=built.txn,
|
|
1089
|
+
signer=signer,
|
|
1090
|
+
logical_max_fee=built.logical_max_fee,
|
|
1091
|
+
)
|
|
1092
|
+
]
|
|
1093
|
+
elif isinstance(params, AppCreateParams):
|
|
1094
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1095
|
+
built = build_app_create_transaction(params, app_manager=self._app_manager, **builder_kwargs)
|
|
1096
|
+
return [
|
|
1097
|
+
_BuiltTxnSpec(
|
|
1098
|
+
txn=built.txn,
|
|
1099
|
+
signer=signer,
|
|
1100
|
+
logical_max_fee=built.logical_max_fee,
|
|
1101
|
+
)
|
|
1102
|
+
]
|
|
1103
|
+
elif isinstance(params, AppUpdateParams):
|
|
1104
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1105
|
+
built = build_app_update_transaction(params, app_manager=self._app_manager, **builder_kwargs)
|
|
1106
|
+
return [
|
|
1107
|
+
_BuiltTxnSpec(
|
|
1108
|
+
txn=built.txn,
|
|
1109
|
+
signer=signer,
|
|
1110
|
+
logical_max_fee=built.logical_max_fee,
|
|
1111
|
+
)
|
|
1112
|
+
]
|
|
1113
|
+
elif isinstance(params, AppDeleteParams):
|
|
1114
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1115
|
+
built = build_app_delete_transaction(params, app_manager=self._app_manager, **builder_kwargs)
|
|
1116
|
+
return [
|
|
1117
|
+
_BuiltTxnSpec(
|
|
1118
|
+
txn=built.txn,
|
|
1119
|
+
signer=signer,
|
|
1120
|
+
logical_max_fee=built.logical_max_fee,
|
|
1121
|
+
)
|
|
1122
|
+
]
|
|
1123
|
+
elif isinstance(params, AppCallParams):
|
|
1124
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1125
|
+
built = build_app_call_transaction(params, app_manager=self._app_manager, **builder_kwargs)
|
|
1126
|
+
return [
|
|
1127
|
+
_BuiltTxnSpec(
|
|
1128
|
+
txn=built.txn,
|
|
1129
|
+
signer=signer,
|
|
1130
|
+
logical_max_fee=built.logical_max_fee,
|
|
1131
|
+
)
|
|
1132
|
+
]
|
|
1133
|
+
elif isinstance(params, OnlineKeyRegistrationParams):
|
|
1134
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1135
|
+
built = build_online_key_registration_transaction(params, **builder_kwargs)
|
|
1136
|
+
return [
|
|
1137
|
+
_BuiltTxnSpec(
|
|
1138
|
+
txn=built.txn,
|
|
1139
|
+
signer=signer,
|
|
1140
|
+
logical_max_fee=built.logical_max_fee,
|
|
1141
|
+
)
|
|
1142
|
+
]
|
|
1143
|
+
elif isinstance(params, OfflineKeyRegistrationParams):
|
|
1144
|
+
signer = self._resolve_param_signer(params.signer, params.sender)
|
|
1145
|
+
built = build_offline_key_registration_transaction(params, **builder_kwargs)
|
|
1146
|
+
return [
|
|
1147
|
+
_BuiltTxnSpec(
|
|
1148
|
+
txn=built.txn,
|
|
1149
|
+
signer=signer,
|
|
1150
|
+
logical_max_fee=built.logical_max_fee,
|
|
1151
|
+
)
|
|
1152
|
+
]
|
|
1153
|
+
elif isinstance(params, MethodCallTxnParamTypes):
|
|
1154
|
+
extra_specs, flattened_params = self._extract_method_call_transactions(
|
|
1155
|
+
params, suggested_params, is_localnet=is_localnet
|
|
1156
|
+
)
|
|
1157
|
+
if isinstance(params, AppCreateMethodCallParams):
|
|
1158
|
+
create_params = cast(AppCreateMethodCallParams, flattened_params)
|
|
1159
|
+
built = build_app_create_method_call_transaction(
|
|
1160
|
+
create_params,
|
|
1161
|
+
suggested_params=suggested_params,
|
|
1162
|
+
method_args=create_params.args,
|
|
1163
|
+
app_manager=self._app_manager,
|
|
1164
|
+
default_validity_window=self._default_validity_window,
|
|
1165
|
+
default_validity_window_is_explicit=self._default_validity_window_is_explicit,
|
|
1166
|
+
is_localnet=is_localnet,
|
|
1167
|
+
)
|
|
1168
|
+
return [
|
|
1169
|
+
*extra_specs,
|
|
1170
|
+
_BuiltTxnSpec(
|
|
1171
|
+
txn=built.txn,
|
|
1172
|
+
signer=self._resolve_param_signer(create_params.signer, create_params.sender),
|
|
1173
|
+
logical_max_fee=built.logical_max_fee,
|
|
1174
|
+
method=create_params.method,
|
|
1175
|
+
),
|
|
1176
|
+
]
|
|
1177
|
+
if isinstance(params, AppUpdateMethodCallParams):
|
|
1178
|
+
update_params = cast(AppUpdateMethodCallParams, flattened_params)
|
|
1179
|
+
built = build_app_update_method_call_transaction(
|
|
1180
|
+
update_params,
|
|
1181
|
+
suggested_params=suggested_params,
|
|
1182
|
+
method_args=update_params.args,
|
|
1183
|
+
app_manager=self._app_manager,
|
|
1184
|
+
default_validity_window=self._default_validity_window,
|
|
1185
|
+
default_validity_window_is_explicit=self._default_validity_window_is_explicit,
|
|
1186
|
+
is_localnet=is_localnet,
|
|
1187
|
+
)
|
|
1188
|
+
return [
|
|
1189
|
+
*extra_specs,
|
|
1190
|
+
_BuiltTxnSpec(
|
|
1191
|
+
txn=built.txn,
|
|
1192
|
+
signer=self._resolve_param_signer(update_params.signer, update_params.sender),
|
|
1193
|
+
logical_max_fee=built.logical_max_fee,
|
|
1194
|
+
method=update_params.method,
|
|
1195
|
+
),
|
|
1196
|
+
]
|
|
1197
|
+
if isinstance(params, AppDeleteMethodCallParams):
|
|
1198
|
+
delete_params = cast(AppDeleteMethodCallParams, flattened_params)
|
|
1199
|
+
built = build_app_delete_method_call_transaction(
|
|
1200
|
+
delete_params,
|
|
1201
|
+
suggested_params=suggested_params,
|
|
1202
|
+
method_args=delete_params.args,
|
|
1203
|
+
app_manager=self._app_manager,
|
|
1204
|
+
default_validity_window=self._default_validity_window,
|
|
1205
|
+
default_validity_window_is_explicit=self._default_validity_window_is_explicit,
|
|
1206
|
+
is_localnet=is_localnet,
|
|
1207
|
+
)
|
|
1208
|
+
return [
|
|
1209
|
+
*extra_specs,
|
|
1210
|
+
_BuiltTxnSpec(
|
|
1211
|
+
txn=built.txn,
|
|
1212
|
+
signer=self._resolve_param_signer(delete_params.signer, delete_params.sender),
|
|
1213
|
+
logical_max_fee=built.logical_max_fee,
|
|
1214
|
+
method=delete_params.method,
|
|
1215
|
+
),
|
|
1216
|
+
]
|
|
1217
|
+
|
|
1218
|
+
call_params = cast(AppCallMethodCallParams, flattened_params)
|
|
1219
|
+
built = build_app_call_method_call_transaction(
|
|
1220
|
+
call_params,
|
|
1221
|
+
suggested_params=suggested_params,
|
|
1222
|
+
method_args=call_params.args,
|
|
1223
|
+
app_manager=self._app_manager,
|
|
1224
|
+
default_validity_window=self._default_validity_window,
|
|
1225
|
+
default_validity_window_is_explicit=self._default_validity_window_is_explicit,
|
|
1226
|
+
is_localnet=is_localnet,
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
return [
|
|
1230
|
+
*extra_specs,
|
|
1231
|
+
_BuiltTxnSpec(
|
|
1232
|
+
txn=built.txn,
|
|
1233
|
+
signer=self._resolve_param_signer(call_params.signer, call_params.sender),
|
|
1234
|
+
logical_max_fee=built.logical_max_fee,
|
|
1235
|
+
method=call_params.method,
|
|
1236
|
+
),
|
|
1237
|
+
]
|
|
1238
|
+
|
|
1239
|
+
raise ValueError(f"Unsupported transaction params type: {type(params)}")
|
|
1240
|
+
|
|
1241
|
+
def _clone_entry(self, entry: _QueuedTransaction) -> _QueuedTransaction:
|
|
1242
|
+
if isinstance(entry.txn, Transaction):
|
|
1243
|
+
return _QueuedTransaction(txn=self._sanitize_transaction(entry.txn), signer=entry.signer)
|
|
1244
|
+
# TxnParams are immutable (frozen dataclasses) so we can share them
|
|
1245
|
+
return _QueuedTransaction(txn=entry.txn, signer=entry.signer)
|
|
1246
|
+
|
|
1247
|
+
def _process_method_call_arg(
|
|
1248
|
+
self,
|
|
1249
|
+
arg: object | None,
|
|
1250
|
+
current_signer: TransactionSigner | None,
|
|
1251
|
+
suggested_params: algod_models.SuggestedParams,
|
|
1252
|
+
*,
|
|
1253
|
+
is_localnet: bool,
|
|
1254
|
+
) -> tuple[list[_BuiltTxnSpec], object | None]:
|
|
1255
|
+
if arg is None:
|
|
1256
|
+
return [], None
|
|
1257
|
+
|
|
1258
|
+
if isinstance(arg, TransactionWithSigner):
|
|
1259
|
+
return [
|
|
1260
|
+
_BuiltTxnSpec(
|
|
1261
|
+
txn=self._sanitize_transaction(arg.txn),
|
|
1262
|
+
signer=arg.signer,
|
|
1263
|
+
logical_max_fee=None,
|
|
1264
|
+
)
|
|
1265
|
+
], None
|
|
1266
|
+
|
|
1267
|
+
if isinstance(arg, MethodCallTxnParamTypes):
|
|
1268
|
+
nested_params = arg
|
|
1269
|
+
if arg.signer is None and current_signer is not None:
|
|
1270
|
+
nested_params = replace(arg, signer=current_signer)
|
|
1271
|
+
return (
|
|
1272
|
+
self._build_txn_from_params(
|
|
1273
|
+
nested_params,
|
|
1274
|
+
suggested_params,
|
|
1275
|
+
is_localnet=is_localnet,
|
|
1276
|
+
),
|
|
1277
|
+
None,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
if isinstance(arg, Transaction):
|
|
1281
|
+
return [
|
|
1282
|
+
_BuiltTxnSpec(
|
|
1283
|
+
txn=self._sanitize_transaction(arg),
|
|
1284
|
+
signer=current_signer,
|
|
1285
|
+
logical_max_fee=None,
|
|
1286
|
+
)
|
|
1287
|
+
], None
|
|
1288
|
+
|
|
1289
|
+
if isinstance(arg, TxnParamTypes):
|
|
1290
|
+
return (
|
|
1291
|
+
self._build_txn_from_params(arg, suggested_params, is_localnet=is_localnet),
|
|
1292
|
+
None,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
return [], arg
|
|
1296
|
+
|
|
1297
|
+
def _extract_method_call_transactions(
|
|
1298
|
+
self,
|
|
1299
|
+
params: MethodCallTxnParamTypes,
|
|
1300
|
+
suggested_params: algod_models.SuggestedParams,
|
|
1301
|
+
*,
|
|
1302
|
+
is_localnet: bool,
|
|
1303
|
+
) -> tuple[list[_BuiltTxnSpec], MethodCallTxnParamTypes]:
|
|
1304
|
+
"""Flatten transaction arguments inside ABI method calls into queued specs."""
|
|
1305
|
+
if not params.args:
|
|
1306
|
+
return [], params
|
|
1307
|
+
|
|
1308
|
+
def _to_signer(value: TransactionSigner | AddressWithTransactionSigner | None) -> TransactionSigner | None:
|
|
1309
|
+
if isinstance(value, AddressWithTransactionSigner):
|
|
1310
|
+
return value.signer
|
|
1311
|
+
return value
|
|
1312
|
+
|
|
1313
|
+
current_signer = _to_signer(params.signer)
|
|
1314
|
+
extra_specs: list[_BuiltTxnSpec] = []
|
|
1315
|
+
processed_args: list[Any] = []
|
|
1316
|
+
|
|
1317
|
+
for arg in params.args:
|
|
1318
|
+
specs, processed = self._process_method_call_arg(
|
|
1319
|
+
arg,
|
|
1320
|
+
current_signer,
|
|
1321
|
+
suggested_params,
|
|
1322
|
+
is_localnet=is_localnet,
|
|
1323
|
+
)
|
|
1324
|
+
extra_specs.extend(specs)
|
|
1325
|
+
processed_args.append(processed)
|
|
1326
|
+
|
|
1327
|
+
return extra_specs, replace(params, args=processed_args)
|
|
1328
|
+
|
|
1329
|
+
def _sanitize_transaction(self, txn: Transaction) -> Transaction:
|
|
1330
|
+
return replace(txn, group=None)
|
|
1331
|
+
|
|
1332
|
+
def _resolve_param_signer(
|
|
1333
|
+
self,
|
|
1334
|
+
signer: TransactionSigner | AddressWithTransactionSigner | None,
|
|
1335
|
+
sender: str | None = None,
|
|
1336
|
+
) -> TransactionSigner:
|
|
1337
|
+
if isinstance(signer, AddressWithTransactionSigner):
|
|
1338
|
+
return signer.signer
|
|
1339
|
+
if signer is None:
|
|
1340
|
+
if sender is None:
|
|
1341
|
+
raise ValueError("Sender is required to resolve signer")
|
|
1342
|
+
resolved = self._get_signer(sender)
|
|
1343
|
+
if resolved is None:
|
|
1344
|
+
raise ValueError(f"No signer found for address {sender}")
|
|
1345
|
+
return resolved
|
|
1346
|
+
return signer
|
|
1347
|
+
|
|
1348
|
+
def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner]) -> list[SignedTransaction]:
|
|
1349
|
+
if not txns_with_signers:
|
|
1350
|
+
raise ValueError("No transactions available to sign")
|
|
1351
|
+
|
|
1352
|
+
transactions = [entry.txn for entry in txns_with_signers]
|
|
1353
|
+
signer_groups: dict[int, tuple[TransactionSigner, list[int]]] = {}
|
|
1354
|
+
for index, entry in enumerate(txns_with_signers):
|
|
1355
|
+
key = id(entry.signer)
|
|
1356
|
+
if key not in signer_groups:
|
|
1357
|
+
signer_groups[key] = (entry.signer, [])
|
|
1358
|
+
signer_groups[key][1].append(index)
|
|
1359
|
+
|
|
1360
|
+
signed_blobs: dict[int, list[bytes]] = {}
|
|
1361
|
+
for key, (signer, indexes) in signer_groups.items():
|
|
1362
|
+
blobs = signer(transactions, indexes)
|
|
1363
|
+
signed_blobs[key] = list(blobs)
|
|
1364
|
+
if len(blobs) != len(indexes):
|
|
1365
|
+
raise ValueError("Signer returned unexpected number of transactions")
|
|
1366
|
+
|
|
1367
|
+
ordered: list[SignedTransaction | None] = [None] * len(transactions)
|
|
1368
|
+
for key, (_, indexes) in signer_groups.items():
|
|
1369
|
+
blobs = signed_blobs[key]
|
|
1370
|
+
for blob_index, txn_index in enumerate(indexes):
|
|
1371
|
+
ordered[txn_index] = decode_signed_transaction(blobs[blob_index])
|
|
1372
|
+
|
|
1373
|
+
if any(item is None for item in ordered):
|
|
1374
|
+
raise ValueError("One or more transactions were not signed")
|
|
1375
|
+
|
|
1376
|
+
return [item for item in ordered if item is not None]
|
|
1377
|
+
|
|
1378
|
+
def _group_id(self) -> str | None:
|
|
1379
|
+
txns = self._transactions_with_signers or []
|
|
1380
|
+
if not txns:
|
|
1381
|
+
return None
|
|
1382
|
+
group = txns[0].txn.group
|
|
1383
|
+
if group is None:
|
|
1384
|
+
return None
|
|
1385
|
+
return base64.b64encode(group).decode()
|
|
1386
|
+
|
|
1387
|
+
def _wait_for_confirmations(
|
|
1388
|
+
self, tx_ids: Sequence[str], params: SendParams
|
|
1389
|
+
) -> list[algod_models.PendingTransactionResponse]:
|
|
1390
|
+
confirmations: list[algod_models.PendingTransactionResponse] = []
|
|
1391
|
+
max_rounds = params.get("max_rounds_to_wait")
|
|
1392
|
+
|
|
1393
|
+
if max_rounds is None:
|
|
1394
|
+
suggested = self._get_suggested_params()
|
|
1395
|
+
first = int(getattr(suggested, "first_valid", getattr(suggested, "first", 0)))
|
|
1396
|
+
last = max(entry.txn.last_valid for entry in self._transactions_with_signers or [])
|
|
1397
|
+
max_rounds = int(max(last - first + 1, 0))
|
|
1398
|
+
for tx_id in tx_ids:
|
|
1399
|
+
confirmations.append(_wait_for_confirmation(self._algod, tx_id, max_rounds))
|
|
1400
|
+
return confirmations
|
|
1401
|
+
|
|
1402
|
+
def _transform_error(self, err: Exception) -> Exception:
|
|
1403
|
+
original_error = err
|
|
1404
|
+
transformed = err
|
|
1405
|
+
for transformer in self._error_transformers:
|
|
1406
|
+
try:
|
|
1407
|
+
transformed = transformer(transformed)
|
|
1408
|
+
except Exception as transformer_error:
|
|
1409
|
+
raise ErrorTransformerError("Error transformer raised an exception") from transformer_error
|
|
1410
|
+
if not isinstance(transformed, Exception):
|
|
1411
|
+
raise InvalidErrorTransformerValueError(original_error, transformed)
|
|
1412
|
+
return transformed
|
|
1413
|
+
|
|
1414
|
+
def _parse_abi_return_values(
|
|
1415
|
+
self,
|
|
1416
|
+
confirmations: Sequence[algod_models.PendingTransactionResponse],
|
|
1417
|
+
method_calls: dict[int, ABIMethod] | None = None,
|
|
1418
|
+
) -> list[ABIReturn]:
|
|
1419
|
+
abi_returns: list[ABIReturn] = []
|
|
1420
|
+
method_calls = method_calls or {
|
|
1421
|
+
index: entry.method for index, entry in enumerate(self._transactions_with_signers or []) if entry.method
|
|
1422
|
+
}
|
|
1423
|
+
for index, confirmation in enumerate(confirmations):
|
|
1424
|
+
method = method_calls.get(index)
|
|
1425
|
+
if not method:
|
|
1426
|
+
continue
|
|
1427
|
+
abi_return = self._app_manager.get_abi_return(confirmation, method)
|
|
1428
|
+
if abi_return is not None:
|
|
1429
|
+
abi_returns.append(abi_return)
|
|
1430
|
+
return abi_returns
|
|
1431
|
+
|
|
1432
|
+
def _resolve_error_transactions(self) -> list[Transaction] | None:
|
|
1433
|
+
if self._transactions_with_signers is not None:
|
|
1434
|
+
return [entry.txn for entry in self._transactions_with_signers]
|
|
1435
|
+
if self._raw_built_transactions:
|
|
1436
|
+
transactions = list(self._raw_built_transactions)
|
|
1437
|
+
return group_transactions(transactions) if len(transactions) > 1 else transactions
|
|
1438
|
+
return None
|
|
1439
|
+
|
|
1440
|
+
def _simulate_error_context(
|
|
1441
|
+
self,
|
|
1442
|
+
sent_transactions: Sequence[Transaction],
|
|
1443
|
+
*,
|
|
1444
|
+
suppress_log: bool | None,
|
|
1445
|
+
) -> tuple[algod_models.SimulateResponse | None, list[SimulateTransactionResult]]:
|
|
1446
|
+
"""Simulate transactions to get error context including traces.
|
|
1447
|
+
|
|
1448
|
+
Returns:
|
|
1449
|
+
A tuple of (simulate_response, traces).
|
|
1450
|
+
"""
|
|
1451
|
+
try:
|
|
1452
|
+
empty_signer: TransactionSigner = make_empty_transaction_signer()
|
|
1453
|
+
signed_transactions = self._sign_transactions(
|
|
1454
|
+
[TransactionWithSigner(txn=txn, signer=empty_signer) for txn in sent_transactions]
|
|
1455
|
+
)
|
|
1456
|
+
request = algod_models.SimulateRequest(
|
|
1457
|
+
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
|
|
1458
|
+
allow_empty_signatures=True,
|
|
1459
|
+
fix_signers=True,
|
|
1460
|
+
allow_more_logging=True,
|
|
1461
|
+
exec_trace_config=algod_models.SimulateTraceConfig(
|
|
1462
|
+
enable=True,
|
|
1463
|
+
scratch_change=True,
|
|
1464
|
+
stack_change=True,
|
|
1465
|
+
state_change=True,
|
|
1466
|
+
),
|
|
1467
|
+
)
|
|
1468
|
+
response = self._algod.simulate_transactions(request)
|
|
1469
|
+
|
|
1470
|
+
# Extract traces from the response - use SimulateTransactionResult directly
|
|
1471
|
+
# aligned with TypeScript which uses algod client types directly
|
|
1472
|
+
traces: list[SimulateTransactionResult] = []
|
|
1473
|
+
if response.txn_groups and response.txn_groups[0].failed_at:
|
|
1474
|
+
traces = list(response.txn_groups[0].txn_results)
|
|
1475
|
+
|
|
1476
|
+
return response, traces
|
|
1477
|
+
except Exception:
|
|
1478
|
+
config.logger.debug(
|
|
1479
|
+
"Failed to simulate transaction group after send error",
|
|
1480
|
+
exc_info=True,
|
|
1481
|
+
extra={"suppress_log": suppress_log},
|
|
1482
|
+
)
|
|
1483
|
+
return None, []
|
|
1484
|
+
|
|
1485
|
+
def _create_composer_error(
|
|
1486
|
+
self,
|
|
1487
|
+
err: Exception,
|
|
1488
|
+
sent_transactions: Sequence[Transaction] | None,
|
|
1489
|
+
simulate_response: algod_models.SimulateResponse | None,
|
|
1490
|
+
traces: list[SimulateTransactionResult],
|
|
1491
|
+
) -> TransactionComposerError:
|
|
1492
|
+
"""Create a TransactionComposerError with full context."""
|
|
1493
|
+
return TransactionComposerError(
|
|
1494
|
+
str(err),
|
|
1495
|
+
cause=err,
|
|
1496
|
+
traces=traces if traces else None,
|
|
1497
|
+
sent_transactions=list(sent_transactions) if sent_transactions else None,
|
|
1498
|
+
simulate_response=simulate_response,
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
def set_max_fees(self, max_fees: dict[int, AlgoAmount]) -> "TransactionComposer":
|
|
1502
|
+
"""Override max_fee for queued transactions by index before building."""
|
|
1503
|
+
if self._transactions_with_signers is not None:
|
|
1504
|
+
raise RuntimeError("Transactions have already been built")
|
|
1505
|
+
|
|
1506
|
+
for index in max_fees:
|
|
1507
|
+
if index < 0 or index >= len(self._queued):
|
|
1508
|
+
raise ValueError(
|
|
1509
|
+
f"Index {index} is out of range. The composer only contains {len(self._queued)} transactions"
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
for index, max_fee in max_fees.items():
|
|
1513
|
+
entry = self._queued[index]
|
|
1514
|
+
if isinstance(entry.txn, Transaction):
|
|
1515
|
+
self._queued[index] = replace(entry, max_fee=max_fee)
|
|
1516
|
+
elif hasattr(entry.txn, "max_fee"):
|
|
1517
|
+
self._queued[index] = replace(entry, txn=replace(entry.txn, max_fee=max_fee))
|
|
1518
|
+
else:
|
|
1519
|
+
raise ValueError(f"Transaction at index {index} does not support max_fee overrides")
|
|
1520
|
+
|
|
1521
|
+
return self
|
|
1522
|
+
|
|
1523
|
+
def _interpret_error(self, err: Exception) -> Exception:
|
|
1524
|
+
if isinstance(err, UnexpectedStatusError):
|
|
1525
|
+
payload_message = self._extract_algod_error_message(err.payload)
|
|
1526
|
+
if payload_message:
|
|
1527
|
+
return RuntimeError(payload_message)
|
|
1528
|
+
return err
|
|
1529
|
+
|
|
1530
|
+
@staticmethod
|
|
1531
|
+
def _extract_algod_error_message(payload: object) -> str | None: # noqa: PLR0911
|
|
1532
|
+
if payload is None:
|
|
1533
|
+
return None
|
|
1534
|
+
if isinstance(payload, bytes):
|
|
1535
|
+
text = payload.decode("utf-8", errors="ignore")
|
|
1536
|
+
else:
|
|
1537
|
+
text = str(payload)
|
|
1538
|
+
text = text.strip()
|
|
1539
|
+
if not text:
|
|
1540
|
+
return None
|
|
1541
|
+
try:
|
|
1542
|
+
decoded = json.loads(text)
|
|
1543
|
+
except Exception:
|
|
1544
|
+
return text
|
|
1545
|
+
if isinstance(decoded, dict):
|
|
1546
|
+
for key in ("message", "msg", "error", "detail", "description"):
|
|
1547
|
+
value = decoded.get(key)
|
|
1548
|
+
if isinstance(value, str) and value.strip():
|
|
1549
|
+
return value
|
|
1550
|
+
return text
|
|
1551
|
+
if isinstance(decoded, list) and decoded:
|
|
1552
|
+
first = decoded[0]
|
|
1553
|
+
if isinstance(first, str) and first.strip():
|
|
1554
|
+
return first
|
|
1555
|
+
return text
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _wait_for_confirmation(
|
|
1559
|
+
algod: AlgodClient,
|
|
1560
|
+
tx_id: str,
|
|
1561
|
+
max_rounds: int,
|
|
1562
|
+
) -> algod_models.PendingTransactionResponse:
|
|
1563
|
+
remaining = max_rounds
|
|
1564
|
+
status = algod.get_status()
|
|
1565
|
+
current_round = getattr(status, "last_round", 0)
|
|
1566
|
+
while remaining > 0:
|
|
1567
|
+
pending = algod.pending_transaction_information(tx_id)
|
|
1568
|
+
confirmed_round = getattr(pending, "confirmed_round", None)
|
|
1569
|
+
if confirmed_round is not None and confirmed_round > 0:
|
|
1570
|
+
return pending
|
|
1571
|
+
current_round += 1
|
|
1572
|
+
algod.status_after_block(current_round)
|
|
1573
|
+
remaining -= 1
|
|
1574
|
+
raise TimeoutError(f"Transaction {tx_id} not confirmed after {max_rounds} rounds")
|