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,2193 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from dataclasses import asdict, dataclass, fields, replace
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar
|
|
8
|
+
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from algokit_abi import abi, arc32, arc56
|
|
12
|
+
from algokit_common import ProgramSourceMap, get_application_address
|
|
13
|
+
from algokit_transact.models.common import OnApplicationComplete
|
|
14
|
+
from algokit_transact.models.transaction import Transaction
|
|
15
|
+
from algokit_transact.signer import AddressWithTransactionSigner
|
|
16
|
+
from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps
|
|
17
|
+
from algokit_utils.applications.abi import (
|
|
18
|
+
ABIReturn,
|
|
19
|
+
ABIStruct,
|
|
20
|
+
ABIType,
|
|
21
|
+
ABIValue,
|
|
22
|
+
Arc56ReturnValueType,
|
|
23
|
+
BoxABIValue,
|
|
24
|
+
get_abi_decoded_value,
|
|
25
|
+
get_abi_encoded_value,
|
|
26
|
+
)
|
|
27
|
+
from algokit_utils.config import config
|
|
28
|
+
from algokit_utils.errors.logic_error import LogicError, parse_logic_error
|
|
29
|
+
from algokit_utils.models.amount import AlgoAmount
|
|
30
|
+
from algokit_utils.models.application import (
|
|
31
|
+
AppSourceMaps,
|
|
32
|
+
AppState,
|
|
33
|
+
CompiledTeal,
|
|
34
|
+
)
|
|
35
|
+
from algokit_utils.models.state import BoxName, BoxValue
|
|
36
|
+
from algokit_utils.models.transaction import SendParams
|
|
37
|
+
from algokit_utils.transactions.transaction_composer import (
|
|
38
|
+
AppCallMethodCallParams,
|
|
39
|
+
AppCallParams,
|
|
40
|
+
AppCreateSchema,
|
|
41
|
+
AppDeleteMethodCallParams,
|
|
42
|
+
AppMethodCallTransactionArgument,
|
|
43
|
+
AppUpdateMethodCallParams,
|
|
44
|
+
AppUpdateParams,
|
|
45
|
+
BuiltTransactions,
|
|
46
|
+
PaymentParams,
|
|
47
|
+
SendTransactionComposerResults,
|
|
48
|
+
)
|
|
49
|
+
from algokit_utils.transactions.transaction_sender import (
|
|
50
|
+
SendAppTransactionResult,
|
|
51
|
+
SendAppUpdateTransactionResult,
|
|
52
|
+
SendSingleTransactionResult,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from algokit_utils.algorand import AlgorandClient
|
|
57
|
+
from algokit_utils.applications.app_deployer import ApplicationLookup
|
|
58
|
+
from algokit_utils.applications.app_manager import AppManager
|
|
59
|
+
from algokit_utils.models.state import BoxIdentifier, BoxReference, TealTemplateParams
|
|
60
|
+
from algokit_utils.protocols.signer import TransactionSigner
|
|
61
|
+
else:
|
|
62
|
+
AlgorandClient = ApplicationLookup = AppManager = TransactionSigner = Any # type: ignore[assignment]
|
|
63
|
+
BoxIdentifier = BoxReference = TealTemplateParams = Any # type: ignore[assignment]
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"AppClient",
|
|
67
|
+
"AppClientBareCallCreateParams",
|
|
68
|
+
"AppClientBareCallParams",
|
|
69
|
+
"AppClientCompilationParams",
|
|
70
|
+
"AppClientCompilationResult",
|
|
71
|
+
"AppClientCreateSchema",
|
|
72
|
+
"AppClientMethodCallCreateParams",
|
|
73
|
+
"AppClientMethodCallParams",
|
|
74
|
+
"AppClientParams",
|
|
75
|
+
"AppSourceMaps",
|
|
76
|
+
"BaseAppClientMethodCallParams",
|
|
77
|
+
"CommonAppCallCreateParams",
|
|
78
|
+
"CommonAppCallParams",
|
|
79
|
+
"CreateOnComplete",
|
|
80
|
+
"FundAppAccountParams",
|
|
81
|
+
"get_constant_block_offset",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# TEAL opcodes for constant blocks
|
|
85
|
+
BYTE_CBLOCK = 38 # bytecblock opcode
|
|
86
|
+
INT_CBLOCK = 32 # intcblock opcode
|
|
87
|
+
MAX_SIMULATE_OPCODE_BUDGET = (
|
|
88
|
+
20_000 * 16
|
|
89
|
+
) # https://github.com/algorand/go-algorand/blob/807b29a91c371d225e12b9287c5d56e9b33c4e4c/ledger/simulation/trace.go#L104
|
|
90
|
+
|
|
91
|
+
T = TypeVar("T") # For generic return type in _handle_call_errors
|
|
92
|
+
|
|
93
|
+
# Sentinel to detect missing arguments in clone() method of AppClient
|
|
94
|
+
_MISSING = object()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_constant_block_offset(program: bytes) -> int: # noqa: C901
|
|
98
|
+
"""Calculate the offset after constant blocks in TEAL program.
|
|
99
|
+
|
|
100
|
+
Analyzes a compiled TEAL program to find the ending offset position after any bytecblock and intcblock operations.
|
|
101
|
+
|
|
102
|
+
:param program: The compiled TEAL program as bytes
|
|
103
|
+
:return: The maximum offset position after any constant block operations
|
|
104
|
+
"""
|
|
105
|
+
bytes_list = list(program)
|
|
106
|
+
program_size = len(bytes_list)
|
|
107
|
+
|
|
108
|
+
# Remove version byte
|
|
109
|
+
bytes_list.pop(0)
|
|
110
|
+
|
|
111
|
+
# Track offsets
|
|
112
|
+
bytecblock_offset: int | None = None
|
|
113
|
+
intcblock_offset: int | None = None
|
|
114
|
+
|
|
115
|
+
while bytes_list:
|
|
116
|
+
# Get current byte
|
|
117
|
+
byte = bytes_list.pop(0)
|
|
118
|
+
|
|
119
|
+
# Check if byte is a constant block opcode
|
|
120
|
+
if byte in (BYTE_CBLOCK, INT_CBLOCK):
|
|
121
|
+
is_bytecblock = byte == BYTE_CBLOCK
|
|
122
|
+
|
|
123
|
+
# Get number of values in constant block
|
|
124
|
+
if not bytes_list:
|
|
125
|
+
break
|
|
126
|
+
values_remaining = bytes_list.pop(0)
|
|
127
|
+
|
|
128
|
+
# Process each value in the block
|
|
129
|
+
for _ in range(values_remaining):
|
|
130
|
+
if is_bytecblock:
|
|
131
|
+
# For bytecblock, next byte is length of element
|
|
132
|
+
if not bytes_list:
|
|
133
|
+
break
|
|
134
|
+
length = bytes_list.pop(0)
|
|
135
|
+
# Remove the bytes for this element
|
|
136
|
+
bytes_list = bytes_list[length:]
|
|
137
|
+
else:
|
|
138
|
+
# For intcblock, read until we find end of uvarint (MSB not set)
|
|
139
|
+
while bytes_list:
|
|
140
|
+
byte = bytes_list.pop(0)
|
|
141
|
+
if not (byte & 0x80): # Check if MSB is not set
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Update appropriate offset
|
|
145
|
+
if is_bytecblock:
|
|
146
|
+
bytecblock_offset = program_size - len(bytes_list) - 1
|
|
147
|
+
else:
|
|
148
|
+
intcblock_offset = program_size - len(bytes_list) - 1
|
|
149
|
+
|
|
150
|
+
# If next byte isn't a constant block opcode, we're done
|
|
151
|
+
if not bytes_list or bytes_list[0] not in (BYTE_CBLOCK, INT_CBLOCK):
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
# Return maximum offset
|
|
155
|
+
return max(bytecblock_offset or 0, intcblock_offset or 0)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
CreateOnComplete = Literal[
|
|
159
|
+
OnApplicationComplete.NoOp,
|
|
160
|
+
OnApplicationComplete.UpdateApplication,
|
|
161
|
+
OnApplicationComplete.DeleteApplication,
|
|
162
|
+
OnApplicationComplete.OptIn,
|
|
163
|
+
OnApplicationComplete.CloseOut,
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(kw_only=True, frozen=True)
|
|
168
|
+
class AppClientCompilationResult:
|
|
169
|
+
"""Result of compiling an application's TEAL code.
|
|
170
|
+
|
|
171
|
+
Contains the compiled approval and clear state programs along with optional compilation artifacts.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
approval_program: bytes
|
|
175
|
+
"""The compiled approval program bytes"""
|
|
176
|
+
clear_state_program: bytes
|
|
177
|
+
"""The compiled clear state program bytes"""
|
|
178
|
+
compiled_approval: CompiledTeal | None = None
|
|
179
|
+
"""Optional compilation artifacts for approval program"""
|
|
180
|
+
compiled_clear: CompiledTeal | None = None
|
|
181
|
+
"""Optional compilation artifacts for clear state program"""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AppClientCompilationParams(TypedDict, total=False):
|
|
185
|
+
"""Parameters for compiling an application's TEAL code.
|
|
186
|
+
|
|
187
|
+
:ivar deploy_time_params: Optional template parameters to use during compilation
|
|
188
|
+
:ivar updatable: Optional flag indicating if app should be updatable
|
|
189
|
+
:ivar deletable: Optional flag indicating if app should be deletable
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
deploy_time_params: TealTemplateParams | None
|
|
193
|
+
updatable: bool | None
|
|
194
|
+
deletable: bool | None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
ArgsT = TypeVar("ArgsT")
|
|
198
|
+
MethodT = TypeVar("MethodT")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(kw_only=True, frozen=True)
|
|
202
|
+
class CommonAppCallParams:
|
|
203
|
+
"""Common configuration for app call transaction parameters"""
|
|
204
|
+
|
|
205
|
+
account_references: list[str] | None = None
|
|
206
|
+
"""List of account addresses to reference"""
|
|
207
|
+
app_references: list[int] | None = None
|
|
208
|
+
"""List of app IDs to reference"""
|
|
209
|
+
asset_references: list[int] | None = None
|
|
210
|
+
"""List of asset IDs to reference"""
|
|
211
|
+
box_references: list[BoxReference | BoxIdentifier] | None = None
|
|
212
|
+
"""List of box references to include"""
|
|
213
|
+
extra_fee: AlgoAmount | None = None
|
|
214
|
+
"""Additional fee to add to transaction"""
|
|
215
|
+
lease: bytes | None = None
|
|
216
|
+
"""Transaction lease value"""
|
|
217
|
+
max_fee: AlgoAmount | None = None
|
|
218
|
+
"""Maximum fee allowed for transaction"""
|
|
219
|
+
note: bytes | None = None
|
|
220
|
+
"""Custom note for the transaction"""
|
|
221
|
+
rekey_to: str | None = None
|
|
222
|
+
"""Address to rekey account to"""
|
|
223
|
+
sender: str | None = None
|
|
224
|
+
"""Sender address override"""
|
|
225
|
+
signer: TransactionSigner | None = None
|
|
226
|
+
"""Custom transaction signer"""
|
|
227
|
+
static_fee: AlgoAmount | None = None
|
|
228
|
+
"""Fixed fee for transaction"""
|
|
229
|
+
validity_window: int | None = None
|
|
230
|
+
"""Number of rounds valid"""
|
|
231
|
+
first_valid_round: int | None = None
|
|
232
|
+
"""First valid round number"""
|
|
233
|
+
last_valid_round: int | None = None
|
|
234
|
+
"""Last valid round number"""
|
|
235
|
+
on_complete: OnApplicationComplete | None = None
|
|
236
|
+
"""Optional on complete action"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass(frozen=True)
|
|
240
|
+
class AppClientCreateSchema:
|
|
241
|
+
"""Schema for application creation."""
|
|
242
|
+
|
|
243
|
+
extra_program_pages: int | None = None
|
|
244
|
+
"""Optional number of extra program pages"""
|
|
245
|
+
schema: AppCreateSchema | None = None
|
|
246
|
+
"""Optional application creation schema"""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(kw_only=True, frozen=True)
|
|
250
|
+
class CommonAppCallCreateParams(AppClientCreateSchema, CommonAppCallParams):
|
|
251
|
+
"""Common configuration for app create call transaction parameters."""
|
|
252
|
+
|
|
253
|
+
on_complete: CreateOnComplete | None = None
|
|
254
|
+
"""Optional on complete action"""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(kw_only=True, frozen=True)
|
|
258
|
+
class FundAppAccountParams(CommonAppCallParams):
|
|
259
|
+
"""Parameters for funding an application's account."""
|
|
260
|
+
|
|
261
|
+
amount: AlgoAmount
|
|
262
|
+
"""Amount to fund"""
|
|
263
|
+
close_remainder_to: str | None = None
|
|
264
|
+
"""Optional address to close remainder to"""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(kw_only=True, frozen=True)
|
|
268
|
+
class AppClientBareCallParams(CommonAppCallParams):
|
|
269
|
+
"""Parameters for bare application calls."""
|
|
270
|
+
|
|
271
|
+
args: list[bytes] | None = None
|
|
272
|
+
"""Optional arguments"""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(frozen=True)
|
|
276
|
+
class AppClientBareCallCreateParams(CommonAppCallCreateParams):
|
|
277
|
+
"""Parameters for creating application with bare call."""
|
|
278
|
+
|
|
279
|
+
on_complete: CreateOnComplete | None = None
|
|
280
|
+
"""Optional on complete action"""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass(kw_only=True, frozen=True)
|
|
284
|
+
class BaseAppClientMethodCallParams(Generic[ArgsT, MethodT], CommonAppCallParams):
|
|
285
|
+
"""Base parameters for application method calls."""
|
|
286
|
+
|
|
287
|
+
method: MethodT
|
|
288
|
+
"""Method to call"""
|
|
289
|
+
args: ArgsT | None = None
|
|
290
|
+
"""Arguments to pass to the application method call"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(kw_only=True, frozen=True)
|
|
294
|
+
class AppClientMethodCallParams(
|
|
295
|
+
BaseAppClientMethodCallParams[
|
|
296
|
+
Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None],
|
|
297
|
+
str,
|
|
298
|
+
]
|
|
299
|
+
):
|
|
300
|
+
"""Parameters for application method calls."""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True)
|
|
304
|
+
class AppClientMethodCallCreateParams(AppClientCreateSchema, AppClientMethodCallParams):
|
|
305
|
+
"""Parameters for creating application with method call"""
|
|
306
|
+
|
|
307
|
+
on_complete: CreateOnComplete | None = None
|
|
308
|
+
"""Optional on complete action"""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class _AppClientStateMethods:
|
|
312
|
+
def __init__(
|
|
313
|
+
self,
|
|
314
|
+
*,
|
|
315
|
+
get_all: Callable[[], dict[str, Any]],
|
|
316
|
+
get_value: Callable[[str, dict[str, AppState] | None], ABIValue | None],
|
|
317
|
+
get_map_value: Callable[[str, bytes | Any, dict[str, AppState] | None], Any],
|
|
318
|
+
get_map: Callable[[str], dict[str, ABIValue]],
|
|
319
|
+
) -> None:
|
|
320
|
+
self._get_all = get_all
|
|
321
|
+
self._get_value = get_value
|
|
322
|
+
self._get_map_value = get_map_value
|
|
323
|
+
self._get_map = get_map
|
|
324
|
+
|
|
325
|
+
def get_all(self) -> dict[str, Any]:
|
|
326
|
+
return self._get_all()
|
|
327
|
+
|
|
328
|
+
def get_value(self, name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None:
|
|
329
|
+
return self._get_value(name, app_state)
|
|
330
|
+
|
|
331
|
+
def get_map_value(self, map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401
|
|
332
|
+
return self._get_map_value(map_name, key, app_state)
|
|
333
|
+
|
|
334
|
+
def get_map(self, map_name: str) -> dict[str, ABIValue]:
|
|
335
|
+
return self._get_map(map_name)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class _AppClientBoxMethods:
|
|
339
|
+
def __init__(
|
|
340
|
+
self,
|
|
341
|
+
*,
|
|
342
|
+
get_all: Callable[[], dict[str, Any]],
|
|
343
|
+
get_value: Callable[[str], ABIValue | None],
|
|
344
|
+
get_map_value: Callable[[str, bytes | Any], Any],
|
|
345
|
+
get_map: Callable[[str], dict[str, ABIValue]],
|
|
346
|
+
) -> None:
|
|
347
|
+
self._get_all = get_all
|
|
348
|
+
self._get_value = get_value
|
|
349
|
+
self._get_map_value = get_map_value
|
|
350
|
+
self._get_map = get_map
|
|
351
|
+
|
|
352
|
+
def get_all(self) -> dict[str, Any]:
|
|
353
|
+
return self._get_all()
|
|
354
|
+
|
|
355
|
+
def get_value(self, name: str) -> ABIValue | None:
|
|
356
|
+
return self._get_value(name)
|
|
357
|
+
|
|
358
|
+
def get_map_value(self, map_name: str, key: bytes | Any) -> Any: # noqa: ANN401
|
|
359
|
+
return self._get_map_value(map_name, key)
|
|
360
|
+
|
|
361
|
+
def get_map(self, map_name: str) -> dict[str, ABIValue]:
|
|
362
|
+
return self._get_map(map_name)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class _StateAccessor:
|
|
366
|
+
def __init__(self, client: "AppClient") -> None:
|
|
367
|
+
self._client = client
|
|
368
|
+
self._algorand = client._algorand
|
|
369
|
+
self._app_id = client._app_id
|
|
370
|
+
self._app_spec = client._app_spec
|
|
371
|
+
|
|
372
|
+
def local_state(self, address: str) -> _AppClientStateMethods:
|
|
373
|
+
"""Methods to access local state for the current app for a given address"""
|
|
374
|
+
return self._get_state_methods(
|
|
375
|
+
state_getter=lambda: self._algorand.app.get_local_state(self._app_id, address),
|
|
376
|
+
key_getter=lambda: self._app_spec.state.keys.local_state,
|
|
377
|
+
map_getter=lambda: self._app_spec.state.maps.local_state,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def global_state(self) -> _AppClientStateMethods:
|
|
382
|
+
"""Methods to access global state for the current app"""
|
|
383
|
+
return self._get_state_methods(
|
|
384
|
+
state_getter=lambda: self._algorand.app.get_global_state(self._app_id),
|
|
385
|
+
key_getter=lambda: self._app_spec.state.keys.global_state,
|
|
386
|
+
map_getter=lambda: self._app_spec.state.maps.global_state,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def box(self) -> _AppClientBoxMethods:
|
|
391
|
+
"""Methods to access box storage for the current app"""
|
|
392
|
+
return self._get_box_methods()
|
|
393
|
+
|
|
394
|
+
def _get_box_methods(self) -> _AppClientBoxMethods:
|
|
395
|
+
def get_all() -> dict[str, Any]:
|
|
396
|
+
"""Returns all single-key box values in a dict keyed by the key name."""
|
|
397
|
+
return {key: get_value(key) for key in self._app_spec.state.keys.box}
|
|
398
|
+
|
|
399
|
+
def get_value(name: str) -> ABIValue | None:
|
|
400
|
+
"""Returns a single box value for the current app with the value a decoded ABI value.
|
|
401
|
+
|
|
402
|
+
:param name: The name of the box value to retrieve
|
|
403
|
+
:return: The decoded ABI value from the box storage, or None if not found
|
|
404
|
+
"""
|
|
405
|
+
metadata = self._app_spec.state.keys.box[name]
|
|
406
|
+
value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(metadata.key))
|
|
407
|
+
return get_abi_decoded_value(value, metadata.value_type)
|
|
408
|
+
|
|
409
|
+
def get_map_value(map_name: str, key: bytes | Any) -> Any: # noqa: ANN401
|
|
410
|
+
"""Get a value from a box map.
|
|
411
|
+
|
|
412
|
+
Retrieves a value from a box map storage using the provided map name and key.
|
|
413
|
+
|
|
414
|
+
:param map_name: The name of the map to read from
|
|
415
|
+
:param key: The key within the map (without any map prefix) as either bytes or a value
|
|
416
|
+
that will be converted to bytes by encoding it using the specified ABI key type
|
|
417
|
+
:return: The decoded value from the box map storage
|
|
418
|
+
"""
|
|
419
|
+
metadata = self._app_spec.state.maps.box[map_name]
|
|
420
|
+
prefix = base64.b64decode(metadata.prefix or "")
|
|
421
|
+
encoded_key = get_abi_encoded_value(key, metadata.key_type)
|
|
422
|
+
full_key = base64.b64encode(prefix + encoded_key).decode("utf-8")
|
|
423
|
+
value = self._algorand.app.get_box_value(self._app_id, base64.b64decode(full_key))
|
|
424
|
+
return get_abi_decoded_value(value, metadata.value_type)
|
|
425
|
+
|
|
426
|
+
def get_map(map_name: str) -> dict[str, ABIValue]:
|
|
427
|
+
"""Get all key-value pairs from a box map.
|
|
428
|
+
|
|
429
|
+
Retrieves all key-value pairs stored in a box map for the current app.
|
|
430
|
+
|
|
431
|
+
:param map_name: The name of the map to read from
|
|
432
|
+
:return: A dictionary mapping string keys to their corresponding ABI-decoded values
|
|
433
|
+
:raises ValueError: If there is an error decoding any key or value in the map
|
|
434
|
+
"""
|
|
435
|
+
metadata = self._app_spec.state.maps.box[map_name]
|
|
436
|
+
prefix = base64.b64decode(metadata.prefix or "")
|
|
437
|
+
box_names = self._algorand.app.get_box_names(self._app_id)
|
|
438
|
+
|
|
439
|
+
result = {}
|
|
440
|
+
for box in box_names:
|
|
441
|
+
if not box.name_raw.startswith(prefix):
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
key = get_abi_decoded_value(box.name_raw[len(prefix) :], metadata.key_type)
|
|
446
|
+
value = get_abi_decoded_value(
|
|
447
|
+
self._algorand.app.get_box_value(self._app_id, box.name_raw),
|
|
448
|
+
metadata.value_type,
|
|
449
|
+
)
|
|
450
|
+
result[str(key)] = value
|
|
451
|
+
except Exception as e:
|
|
452
|
+
raise ValueError(f"Failed to decode value for key {box.name_raw.decode('utf-8')}") from e
|
|
453
|
+
|
|
454
|
+
return result
|
|
455
|
+
|
|
456
|
+
return _AppClientBoxMethods(
|
|
457
|
+
get_all=get_all,
|
|
458
|
+
get_value=get_value,
|
|
459
|
+
get_map_value=get_map_value,
|
|
460
|
+
get_map=get_map,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def _get_state_methods( # noqa: C901
|
|
464
|
+
self,
|
|
465
|
+
state_getter: Callable[[], dict[str, AppState]],
|
|
466
|
+
key_getter: Callable[[], dict[str, arc56.StorageKey]],
|
|
467
|
+
map_getter: Callable[[], dict[str, arc56.StorageMap]],
|
|
468
|
+
) -> _AppClientStateMethods:
|
|
469
|
+
def get_all() -> dict[str, Any]:
|
|
470
|
+
state = state_getter()
|
|
471
|
+
keys = key_getter()
|
|
472
|
+
return {key: get_value(key, state) for key in keys}
|
|
473
|
+
|
|
474
|
+
def get_value(name: str, app_state: dict[str, AppState] | None = None) -> ABIValue | None:
|
|
475
|
+
state = app_state or state_getter()
|
|
476
|
+
key_info = key_getter()[name]
|
|
477
|
+
value = next((s for s in state.values() if s.key_base64 == key_info.key), None)
|
|
478
|
+
|
|
479
|
+
if value and value.value_raw:
|
|
480
|
+
return get_abi_decoded_value(value.value_raw, key_info.value_type)
|
|
481
|
+
|
|
482
|
+
return value.value if value else None
|
|
483
|
+
|
|
484
|
+
def get_map_value(map_name: str, key: bytes | Any, app_state: dict[str, AppState] | None = None) -> Any: # noqa: ANN401
|
|
485
|
+
state = app_state or state_getter()
|
|
486
|
+
metadata = map_getter()[map_name]
|
|
487
|
+
|
|
488
|
+
prefix = base64.b64decode(metadata.prefix or "")
|
|
489
|
+
encoded_key = get_abi_encoded_value(key, metadata.key_type)
|
|
490
|
+
full_key = base64.b64encode(prefix + encoded_key).decode("utf-8")
|
|
491
|
+
value = next((s for s in state.values() if s.key_base64 == full_key), None)
|
|
492
|
+
if value and value.value_raw:
|
|
493
|
+
return get_abi_decoded_value(value.value_raw, metadata.value_type)
|
|
494
|
+
return value.value if value else None
|
|
495
|
+
|
|
496
|
+
def get_map(map_name: str) -> dict[str, ABIValue]:
|
|
497
|
+
state = state_getter()
|
|
498
|
+
metadata = map_getter()[map_name]
|
|
499
|
+
|
|
500
|
+
prefix = base64.b64decode(metadata.prefix or "").decode("utf-8")
|
|
501
|
+
|
|
502
|
+
prefixed_state = {k: v for k, v in state.items() if k.startswith(prefix)}
|
|
503
|
+
|
|
504
|
+
decoded_map = {}
|
|
505
|
+
|
|
506
|
+
for key_encoded, value in prefixed_state.items():
|
|
507
|
+
key_bytes = key_encoded[len(prefix) :]
|
|
508
|
+
try:
|
|
509
|
+
decoded_key = get_abi_decoded_value(key_bytes, metadata.key_type)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
raise ValueError(f"Failed to decode key {key_encoded}") from e
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
if value and value.value_raw:
|
|
515
|
+
decoded_value = get_abi_decoded_value(value.value_raw, metadata.value_type)
|
|
516
|
+
else:
|
|
517
|
+
decoded_value = get_abi_decoded_value(value.value, metadata.value_type)
|
|
518
|
+
except Exception as e:
|
|
519
|
+
raise ValueError(f"Failed to decode value {value}") from e
|
|
520
|
+
|
|
521
|
+
decoded_map[str(decoded_key)] = decoded_value
|
|
522
|
+
|
|
523
|
+
return decoded_map
|
|
524
|
+
|
|
525
|
+
return _AppClientStateMethods(
|
|
526
|
+
get_all=get_all,
|
|
527
|
+
get_value=get_value,
|
|
528
|
+
get_map_value=get_map_value,
|
|
529
|
+
get_map=get_map,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
def get_local_state(self, address: str) -> dict[str, AppState]:
|
|
533
|
+
return self._algorand.app.get_local_state(self._app_id, address)
|
|
534
|
+
|
|
535
|
+
def get_global_state(self) -> dict[str, AppState]:
|
|
536
|
+
return self._algorand.app.get_global_state(self._app_id)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class _BareParamsBuilder:
|
|
540
|
+
def __init__(self, client: "AppClient") -> None:
|
|
541
|
+
self._client = client
|
|
542
|
+
self._algorand = client._algorand
|
|
543
|
+
self._app_id = client._app_id
|
|
544
|
+
self._app_spec = client._app_spec
|
|
545
|
+
|
|
546
|
+
def _get_bare_params(
|
|
547
|
+
self, params: dict[str, Any] | None, on_complete: OnApplicationComplete | None = None
|
|
548
|
+
) -> dict[str, Any]:
|
|
549
|
+
params = params or {}
|
|
550
|
+
sender = self._client._get_sender(params.get("sender"))
|
|
551
|
+
return {
|
|
552
|
+
**params,
|
|
553
|
+
"app_id": self._app_id,
|
|
554
|
+
"sender": sender,
|
|
555
|
+
"signer": self._client._get_signer(params.get("sender"), params.get("signer")),
|
|
556
|
+
"on_complete": on_complete or OnApplicationComplete.NoOp,
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
def update(
|
|
560
|
+
self,
|
|
561
|
+
params: AppClientBareCallParams | None = None,
|
|
562
|
+
) -> AppUpdateParams:
|
|
563
|
+
"""Create parameters for updating an application.
|
|
564
|
+
|
|
565
|
+
:param params: Optional compilation and send parameters, defaults to None
|
|
566
|
+
:return: Parameters for updating the application
|
|
567
|
+
"""
|
|
568
|
+
call_params: AppUpdateParams = AppUpdateParams(
|
|
569
|
+
**self._get_bare_params(params.__dict__ if params else {}, OnApplicationComplete.UpdateApplication)
|
|
570
|
+
)
|
|
571
|
+
return call_params
|
|
572
|
+
|
|
573
|
+
def opt_in(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
|
|
574
|
+
"""Create parameters for opting into an application.
|
|
575
|
+
|
|
576
|
+
:param params: Optional send parameters, defaults to None
|
|
577
|
+
:return: Parameters for opting into the application
|
|
578
|
+
"""
|
|
579
|
+
call_params: AppCallParams = AppCallParams(
|
|
580
|
+
**self._get_bare_params(params.__dict__ if params else {}, OnApplicationComplete.OptIn)
|
|
581
|
+
)
|
|
582
|
+
return call_params
|
|
583
|
+
|
|
584
|
+
def delete(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
|
|
585
|
+
"""Create parameters for deleting an application.
|
|
586
|
+
|
|
587
|
+
:param params: Optional send parameters, defaults to None
|
|
588
|
+
:return: Parameters for deleting the application
|
|
589
|
+
"""
|
|
590
|
+
call_params: AppCallParams = AppCallParams(
|
|
591
|
+
**self._get_bare_params(params.__dict__ if params else {}, OnApplicationComplete.DeleteApplication)
|
|
592
|
+
)
|
|
593
|
+
return call_params
|
|
594
|
+
|
|
595
|
+
def clear_state(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
|
|
596
|
+
"""Create parameters for clearing application state.
|
|
597
|
+
|
|
598
|
+
:param params: Optional send parameters, defaults to None
|
|
599
|
+
:return: Parameters for clearing application state
|
|
600
|
+
"""
|
|
601
|
+
call_params: AppCallParams = AppCallParams(
|
|
602
|
+
**self._get_bare_params(params.__dict__ if params else {}, OnApplicationComplete.ClearState)
|
|
603
|
+
)
|
|
604
|
+
return call_params
|
|
605
|
+
|
|
606
|
+
def close_out(self, params: AppClientBareCallParams | None = None) -> AppCallParams:
|
|
607
|
+
"""Create parameters for closing out of an application.
|
|
608
|
+
|
|
609
|
+
:param params: Optional send parameters, defaults to None
|
|
610
|
+
:return: Parameters for closing out of the application
|
|
611
|
+
"""
|
|
612
|
+
call_params: AppCallParams = AppCallParams(
|
|
613
|
+
**self._get_bare_params(params.__dict__ if params else {}, OnApplicationComplete.CloseOut)
|
|
614
|
+
)
|
|
615
|
+
return call_params
|
|
616
|
+
|
|
617
|
+
def call(
|
|
618
|
+
self,
|
|
619
|
+
params: AppClientBareCallParams | None = None,
|
|
620
|
+
on_complete: OnApplicationComplete | None = OnApplicationComplete.NoOp,
|
|
621
|
+
) -> AppCallParams:
|
|
622
|
+
"""Create parameters for calling an application.
|
|
623
|
+
|
|
624
|
+
:param params: Optional call parameters with on complete action, defaults to None
|
|
625
|
+
:param on_complete: The OnApplicationComplete action, defaults to OnApplicationComplete.NoOp
|
|
626
|
+
:return: Parameters for calling the application
|
|
627
|
+
"""
|
|
628
|
+
call_params: AppCallParams = AppCallParams(
|
|
629
|
+
**self._get_bare_params(params.__dict__ if params else {}, on_complete or OnApplicationComplete.NoOp)
|
|
630
|
+
)
|
|
631
|
+
return call_params
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class _MethodParamsBuilder:
|
|
635
|
+
def __init__(self, client: "AppClient") -> None:
|
|
636
|
+
self._client = client
|
|
637
|
+
self._algorand = client._algorand
|
|
638
|
+
self._app_id = client._app_id
|
|
639
|
+
self._app_spec = client._app_spec
|
|
640
|
+
self._bare_params_accessor = _BareParamsBuilder(client)
|
|
641
|
+
|
|
642
|
+
@property
|
|
643
|
+
def bare(self) -> _BareParamsBuilder:
|
|
644
|
+
return self._bare_params_accessor
|
|
645
|
+
|
|
646
|
+
def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams:
|
|
647
|
+
"""Create parameters for funding an application account.
|
|
648
|
+
|
|
649
|
+
:param params: Parameters for funding the application account
|
|
650
|
+
:return: Parameters for sending a payment transaction to fund the application account
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
def random_note() -> bytes:
|
|
654
|
+
return base64.b64encode(os.urandom(16))
|
|
655
|
+
|
|
656
|
+
return PaymentParams(
|
|
657
|
+
sender=self._client._get_sender(params.sender),
|
|
658
|
+
signer=self._client._get_signer(params.sender, params.signer),
|
|
659
|
+
receiver=self._client.app_address,
|
|
660
|
+
amount=params.amount,
|
|
661
|
+
rekey_to=params.rekey_to,
|
|
662
|
+
note=params.note or random_note(),
|
|
663
|
+
lease=params.lease,
|
|
664
|
+
static_fee=params.static_fee,
|
|
665
|
+
extra_fee=params.extra_fee,
|
|
666
|
+
max_fee=params.max_fee,
|
|
667
|
+
validity_window=params.validity_window,
|
|
668
|
+
first_valid_round=params.first_valid_round,
|
|
669
|
+
last_valid_round=params.last_valid_round,
|
|
670
|
+
close_remainder_to=params.close_remainder_to,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
|
|
674
|
+
"""Create parameters for opting into an application.
|
|
675
|
+
|
|
676
|
+
:param params: Parameters for the opt-in call
|
|
677
|
+
:return: Parameters for opting into the application
|
|
678
|
+
"""
|
|
679
|
+
input_params = self._get_abi_params(
|
|
680
|
+
params.__dict__, on_complete=params.on_complete or OnApplicationComplete.OptIn
|
|
681
|
+
)
|
|
682
|
+
return AppCallMethodCallParams(**input_params)
|
|
683
|
+
|
|
684
|
+
def call(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
|
|
685
|
+
"""Create parameters for calling an application method.
|
|
686
|
+
|
|
687
|
+
:param params: Parameters for the method call
|
|
688
|
+
:return: Parameters for calling the application method
|
|
689
|
+
"""
|
|
690
|
+
input_params = self._get_abi_params(
|
|
691
|
+
params.__dict__, on_complete=params.on_complete or OnApplicationComplete.NoOp
|
|
692
|
+
)
|
|
693
|
+
return AppCallMethodCallParams(**input_params)
|
|
694
|
+
|
|
695
|
+
def delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCallParams:
|
|
696
|
+
"""Create parameters for deleting an application.
|
|
697
|
+
|
|
698
|
+
:param params: Parameters for the delete call
|
|
699
|
+
:return: Parameters for deleting the application
|
|
700
|
+
"""
|
|
701
|
+
input_params = self._get_abi_params(
|
|
702
|
+
params.__dict__, on_complete=params.on_complete or OnApplicationComplete.DeleteApplication
|
|
703
|
+
)
|
|
704
|
+
return AppDeleteMethodCallParams(**input_params)
|
|
705
|
+
|
|
706
|
+
def update(
|
|
707
|
+
self, params: AppClientMethodCallParams, compilation_params: AppClientCompilationParams | None = None
|
|
708
|
+
) -> AppUpdateMethodCallParams:
|
|
709
|
+
"""Create parameters for updating an application.
|
|
710
|
+
|
|
711
|
+
:param params: Parameters for the update call, optionally including compilation parameters
|
|
712
|
+
:param compilation_params: Parameters for the compilation, defaults to None
|
|
713
|
+
:return: Parameters for updating the application
|
|
714
|
+
"""
|
|
715
|
+
compile_params = asdict(
|
|
716
|
+
self._client.compile(
|
|
717
|
+
app_spec=self._client.app_spec,
|
|
718
|
+
app_manager=self._algorand.app,
|
|
719
|
+
compilation_params=compilation_params,
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
input_params = {
|
|
724
|
+
**self._get_abi_params(
|
|
725
|
+
params.__dict__, on_complete=params.on_complete or OnApplicationComplete.UpdateApplication
|
|
726
|
+
),
|
|
727
|
+
**compile_params,
|
|
728
|
+
}
|
|
729
|
+
# Filter input_params to include only fields valid for AppUpdateMethodCallParams
|
|
730
|
+
app_update_method_call_fields = {field.name for field in fields(AppUpdateMethodCallParams)}
|
|
731
|
+
filtered_input_params = {k: v for k, v in input_params.items() if k in app_update_method_call_fields}
|
|
732
|
+
return AppUpdateMethodCallParams(**filtered_input_params)
|
|
733
|
+
|
|
734
|
+
def close_out(self, params: AppClientMethodCallParams) -> AppCallMethodCallParams:
|
|
735
|
+
"""Create parameters for closing out of an application.
|
|
736
|
+
|
|
737
|
+
:param params: Parameters for the close-out call
|
|
738
|
+
:return: Parameters for closing out of the application
|
|
739
|
+
"""
|
|
740
|
+
input_params = self._get_abi_params(
|
|
741
|
+
params.__dict__, on_complete=params.on_complete or OnApplicationComplete.CloseOut
|
|
742
|
+
)
|
|
743
|
+
return AppCallMethodCallParams(**input_params)
|
|
744
|
+
|
|
745
|
+
def _get_abi_params(self, params: dict[str, Any], on_complete: OnApplicationComplete) -> dict[str, Any]:
|
|
746
|
+
input_params = copy.deepcopy(params)
|
|
747
|
+
|
|
748
|
+
input_params["app_id"] = self._app_id
|
|
749
|
+
input_params["on_complete"] = on_complete
|
|
750
|
+
input_params["sender"] = self._client._get_sender(params["sender"])
|
|
751
|
+
input_params["signer"] = self._client._get_signer(params["sender"], params["signer"])
|
|
752
|
+
|
|
753
|
+
if params.get("method"):
|
|
754
|
+
input_params["method"] = self._app_spec.get_abi_method(params["method"])
|
|
755
|
+
input_params["args"] = self._client._get_abi_args_with_default_values(
|
|
756
|
+
method_name_or_signature=params["method"],
|
|
757
|
+
args=params.get("args"),
|
|
758
|
+
sender=self._client._get_sender(input_params["sender"]),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
return input_params
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class _AppClientBareCallCreateTransactionMethods:
|
|
765
|
+
def __init__(self, client: "AppClient") -> None:
|
|
766
|
+
self._client = client
|
|
767
|
+
self._algorand = client._algorand
|
|
768
|
+
|
|
769
|
+
def update(self, params: AppClientBareCallParams | None = None) -> Transaction:
|
|
770
|
+
"""Create a transaction to update an application.
|
|
771
|
+
|
|
772
|
+
Creates a transaction that will update an existing application with new approval and clear state programs.
|
|
773
|
+
|
|
774
|
+
:param params: Parameters for the update call including compilation and transaction options, defaults to None
|
|
775
|
+
:return: The constructed application update transaction
|
|
776
|
+
"""
|
|
777
|
+
return self._algorand.create_transaction.app_update(
|
|
778
|
+
self._client.params.bare.update(params or AppClientBareCallParams())
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
def opt_in(self, params: AppClientBareCallParams | None = None) -> Transaction:
|
|
782
|
+
"""Create a transaction to opt into an application.
|
|
783
|
+
|
|
784
|
+
Creates a transaction that will opt the sender account into using this application.
|
|
785
|
+
|
|
786
|
+
:param params: Parameters for the opt-in call including transaction options, defaults to None
|
|
787
|
+
:return: The constructed opt-in transaction
|
|
788
|
+
"""
|
|
789
|
+
return self._algorand.create_transaction.app_call(
|
|
790
|
+
self._client.params.bare.opt_in(params or AppClientBareCallParams())
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
def delete(self, params: AppClientBareCallParams | None = None) -> Transaction:
|
|
794
|
+
"""Create a transaction to delete an application.
|
|
795
|
+
|
|
796
|
+
Creates a transaction that will delete this application from the blockchain.
|
|
797
|
+
|
|
798
|
+
:param params: Parameters for the delete call including transaction options, defaults to None
|
|
799
|
+
:return: The constructed delete transaction
|
|
800
|
+
"""
|
|
801
|
+
return self._algorand.create_transaction.app_call(
|
|
802
|
+
self._client.params.bare.delete(params or AppClientBareCallParams())
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
def clear_state(self, params: AppClientBareCallParams | None = None) -> Transaction:
|
|
806
|
+
"""Create a transaction to clear application state.
|
|
807
|
+
|
|
808
|
+
Creates a transaction that will clear the sender's local state for this application.
|
|
809
|
+
|
|
810
|
+
:param params: Parameters for the clear state call including transaction options, defaults to None
|
|
811
|
+
:return: The constructed clear state transaction
|
|
812
|
+
"""
|
|
813
|
+
return self._algorand.create_transaction.app_call(
|
|
814
|
+
self._client.params.bare.clear_state(params or AppClientBareCallParams())
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
def close_out(self, params: AppClientBareCallParams | None = None) -> Transaction:
|
|
818
|
+
"""Create a transaction to close out of an application.
|
|
819
|
+
|
|
820
|
+
Creates a transaction that will close out the sender's participation in this application.
|
|
821
|
+
|
|
822
|
+
:param params: Parameters for the close out call including transaction options, defaults to None
|
|
823
|
+
:return: The constructed close out transaction
|
|
824
|
+
"""
|
|
825
|
+
return self._algorand.create_transaction.app_call(
|
|
826
|
+
self._client.params.bare.close_out(params or AppClientBareCallParams())
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def call(
|
|
830
|
+
self,
|
|
831
|
+
params: AppClientBareCallParams | None = None,
|
|
832
|
+
on_complete: OnApplicationComplete | None = OnApplicationComplete.NoOp,
|
|
833
|
+
) -> Transaction:
|
|
834
|
+
"""Create a transaction to call an application.
|
|
835
|
+
|
|
836
|
+
Creates a transaction that will call this application with the specified parameters.
|
|
837
|
+
|
|
838
|
+
:param params: Parameters for the application call including on complete action, defaults to None
|
|
839
|
+
:param on_complete: The OnApplicationComplete action, defaults to OnApplicationComplete.NoOp
|
|
840
|
+
:return: The constructed application call transaction
|
|
841
|
+
"""
|
|
842
|
+
return self._algorand.create_transaction.app_call(
|
|
843
|
+
self._client.params.bare.call(
|
|
844
|
+
params or AppClientBareCallParams(), on_complete or OnApplicationComplete.NoOp
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class _TransactionCreator:
|
|
850
|
+
def __init__(self, client: "AppClient") -> None:
|
|
851
|
+
self._client = client
|
|
852
|
+
self._algorand = client._algorand
|
|
853
|
+
self._app_id = client._app_id
|
|
854
|
+
self._app_spec = client._app_spec
|
|
855
|
+
self._bare_create_transaction_methods = _AppClientBareCallCreateTransactionMethods(client)
|
|
856
|
+
|
|
857
|
+
@property
|
|
858
|
+
def bare(self) -> _AppClientBareCallCreateTransactionMethods:
|
|
859
|
+
return self._bare_create_transaction_methods
|
|
860
|
+
|
|
861
|
+
def fund_app_account(self, params: FundAppAccountParams) -> Transaction:
|
|
862
|
+
"""Create a transaction to fund an application account.
|
|
863
|
+
|
|
864
|
+
Creates a payment transaction to fund the application account with the specified parameters.
|
|
865
|
+
|
|
866
|
+
:param params: Parameters for funding the application account including amount and transaction options
|
|
867
|
+
:return: The constructed payment transaction
|
|
868
|
+
"""
|
|
869
|
+
return self._algorand.create_transaction.payment(self._client.params.fund_app_account(params))
|
|
870
|
+
|
|
871
|
+
def opt_in(self, params: AppClientMethodCallParams) -> BuiltTransactions:
|
|
872
|
+
"""Create a transaction to opt into an application.
|
|
873
|
+
|
|
874
|
+
Creates a transaction that will opt the sender into this application with the specified parameters.
|
|
875
|
+
|
|
876
|
+
:param params: Parameters for the opt-in call including method arguments and transaction options
|
|
877
|
+
:return: The constructed opt-in transaction(s)
|
|
878
|
+
"""
|
|
879
|
+
return self._algorand.create_transaction.app_call_method_call(self._client.params.opt_in(params))
|
|
880
|
+
|
|
881
|
+
def update(self, params: AppClientMethodCallParams) -> BuiltTransactions:
|
|
882
|
+
"""Create a transaction to update an application.
|
|
883
|
+
|
|
884
|
+
Creates a transaction that will update this application with new approval and clear state programs.
|
|
885
|
+
|
|
886
|
+
:param params: Parameters for the update call including method arguments and transaction options
|
|
887
|
+
:return: The constructed update transaction(s)
|
|
888
|
+
"""
|
|
889
|
+
return self._algorand.create_transaction.app_update_method_call(self._client.params.update(params))
|
|
890
|
+
|
|
891
|
+
def delete(self, params: AppClientMethodCallParams) -> BuiltTransactions:
|
|
892
|
+
"""Create a transaction to delete an application.
|
|
893
|
+
|
|
894
|
+
Creates a transaction that will delete this application.
|
|
895
|
+
|
|
896
|
+
:param params: Parameters for the delete call including method arguments and transaction options
|
|
897
|
+
:return: The constructed delete transaction(s)
|
|
898
|
+
"""
|
|
899
|
+
return self._algorand.create_transaction.app_delete_method_call(self._client.params.delete(params))
|
|
900
|
+
|
|
901
|
+
def close_out(self, params: AppClientMethodCallParams) -> BuiltTransactions:
|
|
902
|
+
"""Create a transaction to close out of an application.
|
|
903
|
+
|
|
904
|
+
Creates a transaction that will close out the sender's participation in this application.
|
|
905
|
+
|
|
906
|
+
:param params: Parameters for the close out call including method arguments and transaction options
|
|
907
|
+
:return: The constructed close out transaction(s)
|
|
908
|
+
"""
|
|
909
|
+
return self._algorand.create_transaction.app_call_method_call(self._client.params.close_out(params))
|
|
910
|
+
|
|
911
|
+
def call(self, params: AppClientMethodCallParams) -> BuiltTransactions:
|
|
912
|
+
"""Create a transaction to call an application.
|
|
913
|
+
|
|
914
|
+
Creates a transaction that will call this application with the specified parameters.
|
|
915
|
+
|
|
916
|
+
:param params: Parameters for the application call including method arguments and transaction options
|
|
917
|
+
:return: The constructed application call transaction(s)
|
|
918
|
+
"""
|
|
919
|
+
return self._algorand.create_transaction.app_call_method_call(self._client.params.call(params))
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
class _AppClientBareSendAccessor:
|
|
923
|
+
def __init__(self, client: "AppClient") -> None:
|
|
924
|
+
self._client = client
|
|
925
|
+
self._algorand = client._algorand
|
|
926
|
+
self._app_id = client._app_id
|
|
927
|
+
self._app_spec = client._app_spec
|
|
928
|
+
|
|
929
|
+
def update(
|
|
930
|
+
self,
|
|
931
|
+
params: AppClientBareCallParams | None = None,
|
|
932
|
+
send_params: SendParams | None = None,
|
|
933
|
+
compilation_params: AppClientCompilationParams | None = None,
|
|
934
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
935
|
+
"""Send an application update transaction.
|
|
936
|
+
|
|
937
|
+
Sends a transaction to update an existing application with new approval and clear state programs.
|
|
938
|
+
|
|
939
|
+
:param params: The parameters for the update call, including optional compilation parameters,
|
|
940
|
+
deploy time parameters, and transaction configuration
|
|
941
|
+
:param send_params: Send parameters, defaults to None
|
|
942
|
+
:param compilation_params: Parameters for the compilation, defaults to None
|
|
943
|
+
:return: The result of sending the transaction, including compilation artifacts and ABI return
|
|
944
|
+
value if applicable
|
|
945
|
+
"""
|
|
946
|
+
params = params or AppClientBareCallParams()
|
|
947
|
+
compilation = compilation_params or AppClientCompilationParams()
|
|
948
|
+
compiled = self._client.compile_app(
|
|
949
|
+
{
|
|
950
|
+
"deploy_time_params": compilation.get("deploy_time_params"),
|
|
951
|
+
"updatable": compilation.get("updatable"),
|
|
952
|
+
"deletable": compilation.get("deletable"),
|
|
953
|
+
}
|
|
954
|
+
)
|
|
955
|
+
bare_params = self._client.params.bare.update(params)
|
|
956
|
+
bare_params.__setattr__("approval_program", bare_params.approval_program or compiled.compiled_approval)
|
|
957
|
+
bare_params.__setattr__("clear_state_program", bare_params.clear_state_program or compiled.compiled_clear)
|
|
958
|
+
call_result = self._client._handle_call_errors(lambda: self._algorand.send.app_update(bare_params, send_params))
|
|
959
|
+
return SendAppTransactionResult[ABIReturn](
|
|
960
|
+
**{**call_result.__dict__, **(compiled.__dict__ if compiled else {})},
|
|
961
|
+
abi_return=AppManager.get_abi_return(call_result.confirmation, getattr(params, "method", None)),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
def opt_in(
|
|
965
|
+
self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
|
|
966
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
967
|
+
"""Send an application opt-in transaction.
|
|
968
|
+
|
|
969
|
+
Creates and sends a transaction that will opt the sender's account into this application.
|
|
970
|
+
|
|
971
|
+
:param params: Parameters for the opt-in call including transaction options, defaults to None
|
|
972
|
+
:param send_params: Send parameters, defaults to None
|
|
973
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
974
|
+
"""
|
|
975
|
+
return self._client._handle_call_errors(
|
|
976
|
+
lambda: self._algorand.send.app_call(
|
|
977
|
+
self._client.params.bare.opt_in(params or AppClientBareCallParams()), send_params
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
def delete(
|
|
982
|
+
self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
|
|
983
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
984
|
+
"""Send an application delete transaction.
|
|
985
|
+
|
|
986
|
+
Creates and sends a transaction that will delete this application.
|
|
987
|
+
|
|
988
|
+
:param params: Parameters for the delete call including transaction options, defaults to None
|
|
989
|
+
:param send_params: Send parameters, defaults to None
|
|
990
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
991
|
+
"""
|
|
992
|
+
return self._client._handle_call_errors(
|
|
993
|
+
lambda: self._algorand.send.app_call(
|
|
994
|
+
self._client.params.bare.delete(params or AppClientBareCallParams()), send_params
|
|
995
|
+
)
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
def clear_state(
|
|
999
|
+
self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
|
|
1000
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
1001
|
+
"""Send an application clear state transaction.
|
|
1002
|
+
|
|
1003
|
+
Creates and sends a transaction that will clear the sender's local state for this application.
|
|
1004
|
+
|
|
1005
|
+
:param params: Parameters for the clear state call including transaction options, defaults to None
|
|
1006
|
+
:param send_params: Send parameters, defaults to None
|
|
1007
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1008
|
+
"""
|
|
1009
|
+
return self._client._handle_call_errors(
|
|
1010
|
+
lambda: self._algorand.send.app_call(
|
|
1011
|
+
self._client.params.bare.clear_state(params or AppClientBareCallParams()), send_params
|
|
1012
|
+
)
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
def close_out(
|
|
1016
|
+
self, params: AppClientBareCallParams | None = None, send_params: SendParams | None = None
|
|
1017
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
1018
|
+
"""Send an application close out transaction.
|
|
1019
|
+
|
|
1020
|
+
Creates and sends a transaction that will close out the sender's participation in this application.
|
|
1021
|
+
|
|
1022
|
+
:param params: Parameters for the close out call including transaction options, defaults to None
|
|
1023
|
+
:param send_params: Send parameters, defaults to None
|
|
1024
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1025
|
+
"""
|
|
1026
|
+
return self._client._handle_call_errors(
|
|
1027
|
+
lambda: self._algorand.send.app_call(
|
|
1028
|
+
self._client.params.bare.close_out(params or AppClientBareCallParams()), send_params
|
|
1029
|
+
)
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
def call(
|
|
1033
|
+
self,
|
|
1034
|
+
params: AppClientBareCallParams | None = None,
|
|
1035
|
+
on_complete: OnApplicationComplete | None = None,
|
|
1036
|
+
send_params: SendParams | None = None,
|
|
1037
|
+
) -> SendAppTransactionResult[ABIReturn]:
|
|
1038
|
+
"""Send an application call transaction.
|
|
1039
|
+
|
|
1040
|
+
Creates and sends a transaction that will call this application with the specified parameters.
|
|
1041
|
+
|
|
1042
|
+
:param params: Parameters for the application call including transaction options, defaults to None
|
|
1043
|
+
:param on_complete: The OnApplicationComplete action, defaults to None
|
|
1044
|
+
:param send_params: Send parameters, defaults to None
|
|
1045
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1046
|
+
"""
|
|
1047
|
+
return self._client._handle_call_errors(
|
|
1048
|
+
lambda: self._algorand.send.app_call(
|
|
1049
|
+
self._client.params.bare.call(params or AppClientBareCallParams(), on_complete), send_params
|
|
1050
|
+
)
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
class _TransactionSender:
|
|
1055
|
+
def __init__(self, client: "AppClient") -> None:
|
|
1056
|
+
self._client = client
|
|
1057
|
+
self._algorand = client._algorand
|
|
1058
|
+
self._app_id = client._app_id
|
|
1059
|
+
self._app_spec = client._app_spec
|
|
1060
|
+
self._bare_send_accessor = _AppClientBareSendAccessor(client)
|
|
1061
|
+
|
|
1062
|
+
@property
|
|
1063
|
+
def bare(self) -> _AppClientBareSendAccessor:
|
|
1064
|
+
"""Get accessor for bare application calls.
|
|
1065
|
+
|
|
1066
|
+
:return: Accessor for making bare application calls without ABI encoding
|
|
1067
|
+
"""
|
|
1068
|
+
return self._bare_send_accessor
|
|
1069
|
+
|
|
1070
|
+
def fund_app_account(
|
|
1071
|
+
self, params: FundAppAccountParams, send_params: SendParams | None = None
|
|
1072
|
+
) -> SendSingleTransactionResult:
|
|
1073
|
+
"""Send funds to the application account.
|
|
1074
|
+
|
|
1075
|
+
Creates and sends a payment transaction to fund the application account.
|
|
1076
|
+
|
|
1077
|
+
:param params: Parameters for funding the app account including amount and transaction options
|
|
1078
|
+
:param send_params: Send parameters, defaults to None
|
|
1079
|
+
:return: The result of sending the payment transaction
|
|
1080
|
+
"""
|
|
1081
|
+
return self._client._handle_call_errors( # type: ignore[no-any-return]
|
|
1082
|
+
lambda: self._algorand.send.payment(self._client.params.fund_app_account(params), send_params)
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
def opt_in(
|
|
1086
|
+
self, params: AppClientMethodCallParams, send_params: SendParams | None = None
|
|
1087
|
+
) -> SendAppTransactionResult[Arc56ReturnValueType]:
|
|
1088
|
+
"""Send an application opt-in transaction.
|
|
1089
|
+
|
|
1090
|
+
Creates and sends a transaction that will opt the sender into this application.
|
|
1091
|
+
|
|
1092
|
+
:param params: Parameters for the opt-in call including method and transaction options
|
|
1093
|
+
:param send_params: Send parameters, defaults to None
|
|
1094
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1095
|
+
"""
|
|
1096
|
+
return self._client._handle_call_errors(
|
|
1097
|
+
lambda: self._client._process_method_call_return(
|
|
1098
|
+
lambda: self._algorand.send.app_call_method_call(self._client.params.opt_in(params), send_params),
|
|
1099
|
+
self._app_spec.get_abi_method(params.method),
|
|
1100
|
+
)
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
def delete(
|
|
1104
|
+
self, params: AppClientMethodCallParams, send_params: SendParams | None = None
|
|
1105
|
+
) -> SendAppTransactionResult[Arc56ReturnValueType]:
|
|
1106
|
+
"""Send an application delete transaction.
|
|
1107
|
+
|
|
1108
|
+
Creates and sends a transaction that will delete this application.
|
|
1109
|
+
|
|
1110
|
+
:param params: Parameters for the delete call including method and transaction options
|
|
1111
|
+
:param send_params: Send parameters, defaults to None
|
|
1112
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1113
|
+
"""
|
|
1114
|
+
return self._client._handle_call_errors(
|
|
1115
|
+
lambda: self._client._process_method_call_return(
|
|
1116
|
+
lambda: self._algorand.send.app_delete_method_call(self._client.params.delete(params), send_params),
|
|
1117
|
+
self._app_spec.get_abi_method(params.method),
|
|
1118
|
+
)
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
def update(
|
|
1122
|
+
self,
|
|
1123
|
+
params: AppClientMethodCallParams,
|
|
1124
|
+
compilation_params: AppClientCompilationParams | None = None,
|
|
1125
|
+
send_params: SendParams | None = None,
|
|
1126
|
+
) -> SendAppUpdateTransactionResult[Arc56ReturnValueType]:
|
|
1127
|
+
"""Send an application update transaction.
|
|
1128
|
+
|
|
1129
|
+
Creates and sends a transaction that will update this application's program.
|
|
1130
|
+
|
|
1131
|
+
:param params: Parameters for the update call including method, compilation and transaction options
|
|
1132
|
+
:param compilation_params: Parameters for the compilation, defaults to None
|
|
1133
|
+
:param send_params: Send parameters, defaults to None
|
|
1134
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1135
|
+
"""
|
|
1136
|
+
result = self._client._handle_call_errors(
|
|
1137
|
+
lambda: self._client._process_method_call_return(
|
|
1138
|
+
lambda: self._algorand.send.app_update_method_call(
|
|
1139
|
+
self._client.params.update(params, compilation_params), send_params
|
|
1140
|
+
),
|
|
1141
|
+
self._app_spec.get_abi_method(params.method),
|
|
1142
|
+
)
|
|
1143
|
+
)
|
|
1144
|
+
assert isinstance(result, SendAppUpdateTransactionResult)
|
|
1145
|
+
return result
|
|
1146
|
+
|
|
1147
|
+
def close_out(
|
|
1148
|
+
self, params: AppClientMethodCallParams, send_params: SendParams | None = None
|
|
1149
|
+
) -> SendAppTransactionResult[Arc56ReturnValueType]:
|
|
1150
|
+
"""Send an application close out transaction.
|
|
1151
|
+
|
|
1152
|
+
Creates and sends a transaction that will close out the sender's participation in this application.
|
|
1153
|
+
|
|
1154
|
+
:param params: Parameters for the close out call including method and transaction options
|
|
1155
|
+
:param send_params: Send parameters, defaults to None
|
|
1156
|
+
:return: The result of sending the transaction, including ABI return value if applicable
|
|
1157
|
+
"""
|
|
1158
|
+
return self._client._handle_call_errors(
|
|
1159
|
+
lambda: self._client._process_method_call_return(
|
|
1160
|
+
lambda: self._algorand.send.app_call_method_call(self._client.params.close_out(params), send_params),
|
|
1161
|
+
self._app_spec.get_abi_method(params.method),
|
|
1162
|
+
)
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
def call(
|
|
1166
|
+
self, params: AppClientMethodCallParams, send_params: SendParams | None = None
|
|
1167
|
+
) -> SendAppTransactionResult[Arc56ReturnValueType]:
|
|
1168
|
+
"""Send an application call transaction.
|
|
1169
|
+
|
|
1170
|
+
Creates and sends a transaction that will call this application with the specified parameters.
|
|
1171
|
+
For read-only calls, simulates the transaction instead of sending it.
|
|
1172
|
+
|
|
1173
|
+
:param params: Parameters for the application call including method and transaction options
|
|
1174
|
+
:param send_params: Send parameters
|
|
1175
|
+
:return: The result of sending or simulating the transaction, including ABI return value if applicable
|
|
1176
|
+
"""
|
|
1177
|
+
is_read_only_call = (
|
|
1178
|
+
params.on_complete == OnApplicationComplete.NoOp or params.on_complete is None
|
|
1179
|
+
) and self._app_spec.get_abi_method(params.method).readonly
|
|
1180
|
+
|
|
1181
|
+
if is_read_only_call:
|
|
1182
|
+
readonly_params = params
|
|
1183
|
+
readonly_send_params = send_params or SendParams()
|
|
1184
|
+
reported_fee = (
|
|
1185
|
+
params.static_fee.micro_algo if params.static_fee else self._algorand.get_suggested_params().min_fee
|
|
1186
|
+
)
|
|
1187
|
+
reset_reported_fee = False
|
|
1188
|
+
|
|
1189
|
+
# Read-only calls do not require fees to be paid, as they are only simulated on the network.
|
|
1190
|
+
# With maximum opcode budget provided, ensure_budget won't create inner transactions,
|
|
1191
|
+
# so fee coverage is no longer a concern for read-only calls.
|
|
1192
|
+
# If max_fee is provided, use it as static_fee for potential benefits.
|
|
1193
|
+
if readonly_send_params.get("cover_app_call_inner_transaction_fees") and params.max_fee is not None:
|
|
1194
|
+
readonly_params = replace(readonly_params, static_fee=params.max_fee, extra_fee=None)
|
|
1195
|
+
elif readonly_params.static_fee is None:
|
|
1196
|
+
fallback_fee = params.max_fee or AlgoAmount.from_micro_algo(MAX_SIMULATE_OPCODE_BUDGET)
|
|
1197
|
+
readonly_params = replace(readonly_params, static_fee=fallback_fee, extra_fee=None)
|
|
1198
|
+
reset_reported_fee = True
|
|
1199
|
+
|
|
1200
|
+
method_call_to_simulate = self._algorand.new_group().add_app_call_method_call(
|
|
1201
|
+
self._client.params.call(readonly_params)
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
def run_simulate() -> SendTransactionComposerResults:
|
|
1205
|
+
try:
|
|
1206
|
+
return method_call_to_simulate.simulate(
|
|
1207
|
+
allow_unnamed_resources=readonly_send_params.get("populate_app_call_resources") or True,
|
|
1208
|
+
skip_signatures=True,
|
|
1209
|
+
allow_more_logs=True,
|
|
1210
|
+
allow_empty_signatures=True,
|
|
1211
|
+
extra_opcode_budget=MAX_SIMULATE_OPCODE_BUDGET,
|
|
1212
|
+
exec_trace_config=None,
|
|
1213
|
+
simulation_round=None,
|
|
1214
|
+
)
|
|
1215
|
+
except Exception as e:
|
|
1216
|
+
# For read-only calls with max opcode budget, fee issues should be rare
|
|
1217
|
+
# but we can still provide helpful error message if they occur
|
|
1218
|
+
if readonly_send_params.get("cover_app_call_inner_transaction_fees") and "fee too small" in str(e):
|
|
1219
|
+
raise ValueError(
|
|
1220
|
+
"Fees were too small. You may need to increase the transaction `maxFee`."
|
|
1221
|
+
) from e
|
|
1222
|
+
raise
|
|
1223
|
+
|
|
1224
|
+
simulate_response = self._client._handle_call_errors(run_simulate)
|
|
1225
|
+
|
|
1226
|
+
wrapped_transactions = simulate_response.transactions
|
|
1227
|
+
if reset_reported_fee:
|
|
1228
|
+
wrapped_transactions = [replace(txn, fee=reported_fee) for txn in wrapped_transactions]
|
|
1229
|
+
return SendAppTransactionResult[Arc56ReturnValueType](
|
|
1230
|
+
tx_ids=simulate_response.tx_ids,
|
|
1231
|
+
transactions=wrapped_transactions,
|
|
1232
|
+
transaction=wrapped_transactions[-1],
|
|
1233
|
+
confirmation=simulate_response.confirmations[-1],
|
|
1234
|
+
confirmations=simulate_response.confirmations,
|
|
1235
|
+
group_id=simulate_response.group_id or "",
|
|
1236
|
+
returns=simulate_response.returns,
|
|
1237
|
+
abi_return=simulate_response.returns[-1].value,
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
return self._client._handle_call_errors(
|
|
1241
|
+
lambda: self._client._process_method_call_return(
|
|
1242
|
+
lambda: self._algorand.send.app_call_method_call(self._client.params.call(params), send_params),
|
|
1243
|
+
self._app_spec.get_abi_method(params.method),
|
|
1244
|
+
)
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
@dataclass(kw_only=True, frozen=True)
|
|
1249
|
+
class AppClientParams:
|
|
1250
|
+
"""Full parameters for creating an app client"""
|
|
1251
|
+
|
|
1252
|
+
app_spec: arc56.Arc56Contract | arc32.Arc32Contract | str
|
|
1253
|
+
"""The application specification"""
|
|
1254
|
+
algorand: AlgorandClient
|
|
1255
|
+
"""The Algorand client"""
|
|
1256
|
+
app_id: int
|
|
1257
|
+
"""The application ID"""
|
|
1258
|
+
app_name: str | None = None
|
|
1259
|
+
"""The application name"""
|
|
1260
|
+
default_sender: str | None = None
|
|
1261
|
+
"""The default sender address"""
|
|
1262
|
+
default_signer: TransactionSigner | None = None
|
|
1263
|
+
"""The default transaction signer"""
|
|
1264
|
+
approval_source_map: ProgramSourceMap | None = None
|
|
1265
|
+
"""The approval source map"""
|
|
1266
|
+
clear_source_map: ProgramSourceMap | None = None
|
|
1267
|
+
"""The clear source map"""
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
class AppClient:
|
|
1271
|
+
"""A client for interacting with an Algorand smart contract application.
|
|
1272
|
+
|
|
1273
|
+
Provides a high-level interface for interacting with Algorand smart contracts, including
|
|
1274
|
+
methods for calling application methods, managing state, and handling transactions.
|
|
1275
|
+
|
|
1276
|
+
:param params: Parameters for creating the app client
|
|
1277
|
+
|
|
1278
|
+
:example:
|
|
1279
|
+
>>> params = AppClientParams(
|
|
1280
|
+
... app_spec=Arc56Contract.from_json(app_spec_json),
|
|
1281
|
+
... algorand=algorand,
|
|
1282
|
+
... app_id=1234567890,
|
|
1283
|
+
... app_name="My App",
|
|
1284
|
+
... default_sender="SENDERADDRESS",
|
|
1285
|
+
... default_signer=TransactionSigner(
|
|
1286
|
+
... account="SIGNERACCOUNT",
|
|
1287
|
+
... private_key="SIGNERPRIVATEKEY",
|
|
1288
|
+
... ),
|
|
1289
|
+
... approval_source_map=ProgramSourceMap(
|
|
1290
|
+
... source="APPROVALSOURCE",
|
|
1291
|
+
... ),
|
|
1292
|
+
... clear_source_map=ProgramSourceMap(
|
|
1293
|
+
... source="CLEARSOURCE",
|
|
1294
|
+
... ),
|
|
1295
|
+
... )
|
|
1296
|
+
>>> client = AppClient(params)
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
def __init__(self, params: AppClientParams) -> None:
|
|
1300
|
+
self._app_id = params.app_id
|
|
1301
|
+
self._app_spec = self.normalise_app_spec(params.app_spec)
|
|
1302
|
+
self._algorand = params.algorand
|
|
1303
|
+
self._app_address = get_application_address(self._app_id)
|
|
1304
|
+
self._app_name = params.app_name or self._app_spec.name
|
|
1305
|
+
self._default_sender = params.default_sender
|
|
1306
|
+
self._default_signer = params.default_signer
|
|
1307
|
+
self._approval_source_map = params.approval_source_map
|
|
1308
|
+
self._clear_source_map = params.clear_source_map
|
|
1309
|
+
self._last_compiled: dict[str, bytes] = {} # Track compiled programs for error filtering
|
|
1310
|
+
self._state_accessor = _StateAccessor(self)
|
|
1311
|
+
self._params_accessor = _MethodParamsBuilder(self)
|
|
1312
|
+
self._send_accessor = _TransactionSender(self)
|
|
1313
|
+
self._create_transaction_accessor = _TransactionCreator(self)
|
|
1314
|
+
|
|
1315
|
+
# Register the error transformer to handle app-specific logic errors
|
|
1316
|
+
self._algorand.register_error_transformer(self._handle_call_errors_transform)
|
|
1317
|
+
|
|
1318
|
+
@property
|
|
1319
|
+
def algorand(self) -> AlgorandClient:
|
|
1320
|
+
"""Get the Algorand client instance.
|
|
1321
|
+
|
|
1322
|
+
:return: The Algorand client used by this app client
|
|
1323
|
+
"""
|
|
1324
|
+
return self._algorand
|
|
1325
|
+
|
|
1326
|
+
@property
|
|
1327
|
+
def app_id(self) -> int:
|
|
1328
|
+
"""Get the application ID.
|
|
1329
|
+
|
|
1330
|
+
:return: The ID of the Algorand application
|
|
1331
|
+
"""
|
|
1332
|
+
return self._app_id
|
|
1333
|
+
|
|
1334
|
+
@property
|
|
1335
|
+
def app_address(self) -> str:
|
|
1336
|
+
"""Get the application's Algorand address.
|
|
1337
|
+
|
|
1338
|
+
:return: The Algorand address associated with this application
|
|
1339
|
+
"""
|
|
1340
|
+
return self._app_address
|
|
1341
|
+
|
|
1342
|
+
@property
|
|
1343
|
+
def app_name(self) -> str:
|
|
1344
|
+
"""Get the application name.
|
|
1345
|
+
|
|
1346
|
+
:return: The name of the application
|
|
1347
|
+
"""
|
|
1348
|
+
return self._app_name
|
|
1349
|
+
|
|
1350
|
+
@property
|
|
1351
|
+
def app_spec(self) -> arc56.Arc56Contract:
|
|
1352
|
+
"""Get the application specification.
|
|
1353
|
+
|
|
1354
|
+
:return: The ARC-56 contract specification for this application
|
|
1355
|
+
"""
|
|
1356
|
+
return self._app_spec
|
|
1357
|
+
|
|
1358
|
+
@property
|
|
1359
|
+
def state(self) -> _StateAccessor:
|
|
1360
|
+
"""Get the state accessor.
|
|
1361
|
+
|
|
1362
|
+
:return: The state accessor for this application
|
|
1363
|
+
"""
|
|
1364
|
+
return self._state_accessor
|
|
1365
|
+
|
|
1366
|
+
@property
|
|
1367
|
+
def params(self) -> _MethodParamsBuilder:
|
|
1368
|
+
"""Get the method parameters builder.
|
|
1369
|
+
|
|
1370
|
+
:return: The method parameters builder for this application
|
|
1371
|
+
|
|
1372
|
+
:example:
|
|
1373
|
+
>>> # Create a transaction in the future using Algorand Client
|
|
1374
|
+
>>> my_method_call = app_client.params.call(AppClientMethodCallParams(
|
|
1375
|
+
method='my_method',
|
|
1376
|
+
args=[123, 'hello']))
|
|
1377
|
+
>>> # ...
|
|
1378
|
+
>>> await algorand.send.AppMethodCall(my_method_call)
|
|
1379
|
+
>>> # Define a nested transaction as an ABI argument
|
|
1380
|
+
>>> my_method_call = app_client.params.call(AppClientMethodCallParams(
|
|
1381
|
+
method='my_method',
|
|
1382
|
+
args=[123, 'hello']))
|
|
1383
|
+
>>> app_client.send.call(AppClientMethodCallParams(method='my_method2', args=[my_method_call]))
|
|
1384
|
+
"""
|
|
1385
|
+
return self._params_accessor
|
|
1386
|
+
|
|
1387
|
+
@property
|
|
1388
|
+
def send(self) -> _TransactionSender:
|
|
1389
|
+
"""Get the transaction sender.
|
|
1390
|
+
|
|
1391
|
+
:return: The transaction sender for this application
|
|
1392
|
+
"""
|
|
1393
|
+
return self._send_accessor
|
|
1394
|
+
|
|
1395
|
+
@property
|
|
1396
|
+
def create_transaction(self) -> _TransactionCreator:
|
|
1397
|
+
"""Get the transaction creator.
|
|
1398
|
+
|
|
1399
|
+
:return: The transaction creator for this application
|
|
1400
|
+
"""
|
|
1401
|
+
return self._create_transaction_accessor
|
|
1402
|
+
|
|
1403
|
+
@staticmethod
|
|
1404
|
+
def normalise_app_spec(app_spec: arc56.Arc56Contract | arc32.Arc32Contract | str) -> arc56.Arc56Contract:
|
|
1405
|
+
"""Normalize an application specification to ARC-56 format.
|
|
1406
|
+
|
|
1407
|
+
:param app_spec: The application specification to normalize. Can be raw arc32 or arc56 json,
|
|
1408
|
+
or an Arc32Contract or Arc56Contract instance
|
|
1409
|
+
:return: The normalized ARC-56 contract specification
|
|
1410
|
+
:raises ValueError: If the app spec format is invalid
|
|
1411
|
+
|
|
1412
|
+
:example:
|
|
1413
|
+
>>> spec = AppClient.normalise_app_spec(app_spec_json)
|
|
1414
|
+
"""
|
|
1415
|
+
if isinstance(app_spec, str):
|
|
1416
|
+
spec_dict = json.loads(app_spec)
|
|
1417
|
+
spec = arc32.Arc32Contract.from_json(app_spec) if "hints" in spec_dict else spec_dict
|
|
1418
|
+
else:
|
|
1419
|
+
spec = app_spec
|
|
1420
|
+
|
|
1421
|
+
match spec:
|
|
1422
|
+
case arc56.Arc56Contract():
|
|
1423
|
+
return spec
|
|
1424
|
+
case arc32.Arc32Contract():
|
|
1425
|
+
from algokit_abi import arc32_to_arc56
|
|
1426
|
+
|
|
1427
|
+
return arc32_to_arc56(spec.to_json())
|
|
1428
|
+
case dict():
|
|
1429
|
+
return arc56.Arc56Contract.from_dict(spec)
|
|
1430
|
+
case _:
|
|
1431
|
+
raise ValueError("Invalid app spec format")
|
|
1432
|
+
|
|
1433
|
+
@staticmethod
|
|
1434
|
+
def from_network(
|
|
1435
|
+
app_spec: arc56.Arc56Contract | arc32.Arc32Contract | str,
|
|
1436
|
+
algorand: AlgorandClient,
|
|
1437
|
+
app_name: str | None = None,
|
|
1438
|
+
default_sender: str | None = None,
|
|
1439
|
+
default_signer: TransactionSigner | None = None,
|
|
1440
|
+
approval_source_map: ProgramSourceMap | None = None,
|
|
1441
|
+
clear_source_map: ProgramSourceMap | None = None,
|
|
1442
|
+
) -> "AppClient":
|
|
1443
|
+
"""Create an AppClient instance from network information.
|
|
1444
|
+
|
|
1445
|
+
:param app_spec: The application specification
|
|
1446
|
+
:param algorand: The Algorand client instance
|
|
1447
|
+
:param app_name: Optional application name
|
|
1448
|
+
:param default_sender: Optional default sender address
|
|
1449
|
+
:param default_signer: Optional default transaction signer
|
|
1450
|
+
:param approval_source_map: Optional approval program source map
|
|
1451
|
+
:param clear_source_map: Optional clear program source map
|
|
1452
|
+
:return: A new AppClient instance
|
|
1453
|
+
:raises Exception: If no app ID is found for the network
|
|
1454
|
+
|
|
1455
|
+
:example:
|
|
1456
|
+
>>> client = AppClient.from_network(
|
|
1457
|
+
... app_spec=Arc56Contract.from_json(app_spec_json),
|
|
1458
|
+
... algorand=algorand,
|
|
1459
|
+
... app_name="My App",
|
|
1460
|
+
... default_sender="SENDERADDRESS",
|
|
1461
|
+
... default_signer=TransactionSigner(
|
|
1462
|
+
... account="SIGNERACCOUNT",
|
|
1463
|
+
... private_key="SIGNERPRIVATEKEY",
|
|
1464
|
+
... ),
|
|
1465
|
+
... approval_source_map=ProgramSourceMap(
|
|
1466
|
+
... source="APPROVALSOURCE",
|
|
1467
|
+
... ),
|
|
1468
|
+
... clear_source_map=ProgramSourceMap(
|
|
1469
|
+
... source="CLEARSOURCE",
|
|
1470
|
+
... ),
|
|
1471
|
+
... )
|
|
1472
|
+
"""
|
|
1473
|
+
network = algorand.client.network()
|
|
1474
|
+
app_spec = AppClient.normalise_app_spec(app_spec)
|
|
1475
|
+
network_names = [network.genesis_hash]
|
|
1476
|
+
|
|
1477
|
+
if network.is_localnet:
|
|
1478
|
+
network_names.append("localnet")
|
|
1479
|
+
if network.is_mainnet:
|
|
1480
|
+
network_names.append("mainnet")
|
|
1481
|
+
if network.is_testnet:
|
|
1482
|
+
network_names.append("testnet")
|
|
1483
|
+
|
|
1484
|
+
available_app_spec_networks = list(app_spec.networks.keys()) if app_spec.networks else []
|
|
1485
|
+
network_index = next((i for i, n in enumerate(available_app_spec_networks) if n in network_names), None)
|
|
1486
|
+
|
|
1487
|
+
if network_index is None:
|
|
1488
|
+
raise Exception(f"No app ID found for network {json.dumps(network_names)} in the app spec")
|
|
1489
|
+
|
|
1490
|
+
app_id = app_spec.networks[available_app_spec_networks[network_index]].app_id # type: ignore[index]
|
|
1491
|
+
|
|
1492
|
+
return AppClient(
|
|
1493
|
+
AppClientParams(
|
|
1494
|
+
app_id=app_id,
|
|
1495
|
+
app_spec=app_spec,
|
|
1496
|
+
algorand=algorand,
|
|
1497
|
+
app_name=app_name,
|
|
1498
|
+
default_sender=default_sender,
|
|
1499
|
+
default_signer=default_signer,
|
|
1500
|
+
approval_source_map=approval_source_map,
|
|
1501
|
+
clear_source_map=clear_source_map,
|
|
1502
|
+
)
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
@staticmethod
|
|
1506
|
+
def from_creator_and_name(
|
|
1507
|
+
creator_address: str,
|
|
1508
|
+
app_name: str,
|
|
1509
|
+
app_spec: arc56.Arc56Contract | arc32.Arc32Contract | str,
|
|
1510
|
+
algorand: AlgorandClient,
|
|
1511
|
+
default_sender: str | None = None,
|
|
1512
|
+
default_signer: TransactionSigner | None = None,
|
|
1513
|
+
approval_source_map: ProgramSourceMap | None = None,
|
|
1514
|
+
clear_source_map: ProgramSourceMap | None = None,
|
|
1515
|
+
ignore_cache: bool | None = None,
|
|
1516
|
+
app_lookup_cache: ApplicationLookup | None = None,
|
|
1517
|
+
) -> "AppClient":
|
|
1518
|
+
"""Create an AppClient instance from creator address and application name.
|
|
1519
|
+
|
|
1520
|
+
:param creator_address: The address of the application creator
|
|
1521
|
+
:param app_name: The name of the application
|
|
1522
|
+
:param app_spec: The application specification
|
|
1523
|
+
:param algorand: The Algorand client instance
|
|
1524
|
+
:param default_sender: Optional default sender address
|
|
1525
|
+
:param default_signer: Optional default transaction signer
|
|
1526
|
+
:param approval_source_map: Optional approval program source map
|
|
1527
|
+
:param clear_source_map: Optional clear program source map
|
|
1528
|
+
:param ignore_cache: Optional flag to ignore cache
|
|
1529
|
+
:param app_lookup_cache: Optional app lookup cache
|
|
1530
|
+
:return: A new AppClient instance
|
|
1531
|
+
:raises ValueError: If the app is not found for the creator and name
|
|
1532
|
+
|
|
1533
|
+
:example:
|
|
1534
|
+
>>> client = AppClient.from_creator_and_name(
|
|
1535
|
+
... creator_address="CREATORADDRESS",
|
|
1536
|
+
... app_name="APPNAME",
|
|
1537
|
+
... app_spec=Arc56Contract.from_json(app_spec_json),
|
|
1538
|
+
... algorand=algorand,
|
|
1539
|
+
... )
|
|
1540
|
+
"""
|
|
1541
|
+
app_spec_ = AppClient.normalise_app_spec(app_spec)
|
|
1542
|
+
app_lookup = app_lookup_cache or algorand.app_deployer.get_creator_apps_by_name(
|
|
1543
|
+
creator_address=creator_address, ignore_cache=ignore_cache or False
|
|
1544
|
+
)
|
|
1545
|
+
app_metadata = app_lookup.apps.get(app_name or app_spec_.name)
|
|
1546
|
+
if not app_metadata:
|
|
1547
|
+
raise ValueError(f"App not found for creator {creator_address} and name {app_name or app_spec_.name}")
|
|
1548
|
+
|
|
1549
|
+
return AppClient(
|
|
1550
|
+
AppClientParams(
|
|
1551
|
+
app_id=app_metadata.app_id,
|
|
1552
|
+
app_spec=app_spec_,
|
|
1553
|
+
algorand=algorand,
|
|
1554
|
+
app_name=app_name,
|
|
1555
|
+
default_sender=default_sender,
|
|
1556
|
+
default_signer=default_signer,
|
|
1557
|
+
approval_source_map=approval_source_map,
|
|
1558
|
+
clear_source_map=clear_source_map,
|
|
1559
|
+
)
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
@staticmethod
|
|
1563
|
+
def compile(
|
|
1564
|
+
app_spec: arc56.Arc56Contract,
|
|
1565
|
+
app_manager: AppManager,
|
|
1566
|
+
compilation_params: AppClientCompilationParams | None = None,
|
|
1567
|
+
) -> AppClientCompilationResult:
|
|
1568
|
+
"""Compile the application's TEAL code.
|
|
1569
|
+
|
|
1570
|
+
:param app_spec: The application specification
|
|
1571
|
+
:param app_manager: The application manager instance
|
|
1572
|
+
:param compilation_params: Optional compilation parameters
|
|
1573
|
+
:return: The compilation result
|
|
1574
|
+
:raises ValueError: If attempting to compile without source or byte code
|
|
1575
|
+
"""
|
|
1576
|
+
compilation_params = compilation_params or AppClientCompilationParams()
|
|
1577
|
+
deploy_time_params = compilation_params.get("deploy_time_params")
|
|
1578
|
+
updatable = compilation_params.get("updatable")
|
|
1579
|
+
deletable = compilation_params.get("deletable")
|
|
1580
|
+
|
|
1581
|
+
def is_base64(s: str) -> bool:
|
|
1582
|
+
try:
|
|
1583
|
+
return base64.b64encode(base64.b64decode(s)).decode() == s
|
|
1584
|
+
except Exception:
|
|
1585
|
+
return False
|
|
1586
|
+
|
|
1587
|
+
if not app_spec.source:
|
|
1588
|
+
if not app_spec.byte_code or not app_spec.byte_code.approval or not app_spec.byte_code.clear:
|
|
1589
|
+
raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code")
|
|
1590
|
+
|
|
1591
|
+
return AppClientCompilationResult(
|
|
1592
|
+
approval_program=base64.b64decode(app_spec.byte_code.approval),
|
|
1593
|
+
clear_state_program=base64.b64decode(app_spec.byte_code.clear),
|
|
1594
|
+
)
|
|
1595
|
+
|
|
1596
|
+
compiled_approval = app_manager.compile_teal_template(
|
|
1597
|
+
app_spec.source.get_decoded_approval(),
|
|
1598
|
+
template_params=deploy_time_params,
|
|
1599
|
+
deployment_metadata=(
|
|
1600
|
+
{"updatable": updatable, "deletable": deletable}
|
|
1601
|
+
if updatable is not None or deletable is not None
|
|
1602
|
+
else None
|
|
1603
|
+
),
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
compiled_clear = app_manager.compile_teal_template(
|
|
1607
|
+
app_spec.source.get_decoded_clear(),
|
|
1608
|
+
template_params=deploy_time_params,
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
if config.debug and config.project_root:
|
|
1612
|
+
persist_sourcemaps(
|
|
1613
|
+
sources=[
|
|
1614
|
+
PersistSourceMapInput(
|
|
1615
|
+
compiled_teal=compiled_approval, app_name=app_spec.name, file_name="approval.teal"
|
|
1616
|
+
),
|
|
1617
|
+
PersistSourceMapInput(compiled_teal=compiled_clear, app_name=app_spec.name, file_name="clear.teal"),
|
|
1618
|
+
],
|
|
1619
|
+
project_root=config.project_root,
|
|
1620
|
+
client=app_manager._algod,
|
|
1621
|
+
with_sources=True,
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
return AppClientCompilationResult(
|
|
1625
|
+
approval_program=compiled_approval.compiled_base64_to_bytes,
|
|
1626
|
+
compiled_approval=compiled_approval,
|
|
1627
|
+
clear_state_program=compiled_clear.compiled_base64_to_bytes,
|
|
1628
|
+
compiled_clear=compiled_clear,
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
@staticmethod
|
|
1632
|
+
def _expose_logic_error_static( # noqa: C901
|
|
1633
|
+
*,
|
|
1634
|
+
e: Exception,
|
|
1635
|
+
app_spec: arc56.Arc56Contract,
|
|
1636
|
+
is_clear_state_program: bool = False,
|
|
1637
|
+
approval_source_map: ProgramSourceMap | None = None,
|
|
1638
|
+
clear_source_map: ProgramSourceMap | None = None,
|
|
1639
|
+
program: bytes | None = None,
|
|
1640
|
+
approval_source_info: arc56.ProgramSourceInfo | None = None,
|
|
1641
|
+
clear_source_info: arc56.ProgramSourceInfo | None = None,
|
|
1642
|
+
) -> LogicError | Exception:
|
|
1643
|
+
source_map = clear_source_map if is_clear_state_program else approval_source_map
|
|
1644
|
+
|
|
1645
|
+
error_details = parse_logic_error(str(e))
|
|
1646
|
+
if not error_details:
|
|
1647
|
+
return e
|
|
1648
|
+
|
|
1649
|
+
# The PC value to find in the ARC56 SourceInfo
|
|
1650
|
+
arc56_pc = error_details["pc"]
|
|
1651
|
+
|
|
1652
|
+
program_source_info = clear_source_info if is_clear_state_program else approval_source_info
|
|
1653
|
+
|
|
1654
|
+
# The offset to apply to the PC if using the cblocks pc offset method
|
|
1655
|
+
cblocks_offset = 0
|
|
1656
|
+
|
|
1657
|
+
# If the program uses cblocks offset, then we need to adjust the PC accordingly
|
|
1658
|
+
if program_source_info and program_source_info.pc_offset_method == arc56.PcOffsetMethod.CBLOCKS:
|
|
1659
|
+
if not program:
|
|
1660
|
+
raise Exception("Program bytes are required to calculate the ARC56 cblocks PC offset")
|
|
1661
|
+
|
|
1662
|
+
cblocks_offset = get_constant_block_offset(program)
|
|
1663
|
+
arc56_pc = error_details["pc"] - cblocks_offset
|
|
1664
|
+
|
|
1665
|
+
# Find the source info for this PC and get the error message
|
|
1666
|
+
source_info = None
|
|
1667
|
+
if program_source_info and program_source_info.source_info:
|
|
1668
|
+
source_info = next(
|
|
1669
|
+
(s for s in program_source_info.source_info if isinstance(s, arc56.SourceInfo) and arc56_pc in s.pc),
|
|
1670
|
+
None,
|
|
1671
|
+
)
|
|
1672
|
+
error_message = source_info.error_message if source_info else None
|
|
1673
|
+
|
|
1674
|
+
# If we have the source we can display the TEAL in the error message
|
|
1675
|
+
if hasattr(app_spec, "source"):
|
|
1676
|
+
program_source = (
|
|
1677
|
+
(
|
|
1678
|
+
app_spec.source.get_decoded_clear()
|
|
1679
|
+
if is_clear_state_program
|
|
1680
|
+
else app_spec.source.get_decoded_approval()
|
|
1681
|
+
)
|
|
1682
|
+
if app_spec.source
|
|
1683
|
+
else None
|
|
1684
|
+
)
|
|
1685
|
+
custom_get_line_for_pc = None
|
|
1686
|
+
|
|
1687
|
+
def get_line_for_pc(input_pc: int) -> int | None:
|
|
1688
|
+
if not program_source_info:
|
|
1689
|
+
return None
|
|
1690
|
+
teal = [line.teal for line in program_source_info.source_info if input_pc - cblocks_offset in line.pc]
|
|
1691
|
+
return teal[0] if teal else None
|
|
1692
|
+
|
|
1693
|
+
if not source_map:
|
|
1694
|
+
custom_get_line_for_pc = get_line_for_pc
|
|
1695
|
+
|
|
1696
|
+
if program_source:
|
|
1697
|
+
# Preserve traces from TransactionComposerError if available
|
|
1698
|
+
traces = getattr(e, "traces", None)
|
|
1699
|
+
e = LogicError(
|
|
1700
|
+
logic_error_str=str(e),
|
|
1701
|
+
program=program_source,
|
|
1702
|
+
source_map=source_map,
|
|
1703
|
+
transaction_id=error_details["transaction_id"],
|
|
1704
|
+
message=error_details["message"],
|
|
1705
|
+
pc=error_details["pc"],
|
|
1706
|
+
logic_error=e,
|
|
1707
|
+
get_line_for_pc=custom_get_line_for_pc,
|
|
1708
|
+
traces=traces,
|
|
1709
|
+
)
|
|
1710
|
+
if error_message:
|
|
1711
|
+
import re
|
|
1712
|
+
|
|
1713
|
+
message = e.logic_error_str if isinstance(e, LogicError) else str(e)
|
|
1714
|
+
app_id = re.search(r"(?<=app=)\d+", message)
|
|
1715
|
+
tx_id = re.search(r"(?<=transaction )\S+(?=:)", message)
|
|
1716
|
+
runtime_error_message = (
|
|
1717
|
+
f"Runtime error when executing {app_spec.name} "
|
|
1718
|
+
f"(appId: {app_id.group() if app_id else 'N/A'}) in transaction "
|
|
1719
|
+
f"{tx_id.group() if tx_id else 'N/A'}: {error_message}"
|
|
1720
|
+
)
|
|
1721
|
+
if isinstance(e, LogicError):
|
|
1722
|
+
e.message = runtime_error_message
|
|
1723
|
+
return e
|
|
1724
|
+
else:
|
|
1725
|
+
error = Exception(runtime_error_message)
|
|
1726
|
+
error.__cause__ = e
|
|
1727
|
+
return error
|
|
1728
|
+
|
|
1729
|
+
return e
|
|
1730
|
+
|
|
1731
|
+
def compile_app(
|
|
1732
|
+
self,
|
|
1733
|
+
compilation_params: AppClientCompilationParams | None = None,
|
|
1734
|
+
) -> AppClientCompilationResult:
|
|
1735
|
+
"""Compile the application's TEAL code.
|
|
1736
|
+
|
|
1737
|
+
:param compilation_params: Optional compilation parameters
|
|
1738
|
+
:return: The compilation result
|
|
1739
|
+
"""
|
|
1740
|
+
result = AppClient.compile(self._app_spec, self._algorand.app, compilation_params)
|
|
1741
|
+
|
|
1742
|
+
if result.compiled_approval:
|
|
1743
|
+
self._approval_source_map = result.compiled_approval.source_map
|
|
1744
|
+
if result.compiled_clear:
|
|
1745
|
+
self._clear_source_map = result.compiled_clear.source_map
|
|
1746
|
+
|
|
1747
|
+
# Store compiled programs for new app error filtering
|
|
1748
|
+
self._last_compiled["approval"] = result.approval_program
|
|
1749
|
+
self._last_compiled["clear"] = result.clear_state_program
|
|
1750
|
+
|
|
1751
|
+
return result
|
|
1752
|
+
|
|
1753
|
+
def clone(
|
|
1754
|
+
self,
|
|
1755
|
+
app_name: str | None = _MISSING, # type: ignore[assignment]
|
|
1756
|
+
default_sender: str | None = _MISSING, # type: ignore[assignment]
|
|
1757
|
+
default_signer: TransactionSigner | None = _MISSING, # type: ignore[assignment]
|
|
1758
|
+
approval_source_map: ProgramSourceMap | None = _MISSING, # type: ignore[assignment]
|
|
1759
|
+
clear_source_map: ProgramSourceMap | None = _MISSING, # type: ignore[assignment]
|
|
1760
|
+
) -> "AppClient":
|
|
1761
|
+
"""Create a cloned AppClient instance with optionally overridden parameters.
|
|
1762
|
+
|
|
1763
|
+
:param app_name: Optional new application name
|
|
1764
|
+
:param default_sender: Optional new default sender
|
|
1765
|
+
:param default_signer: Optional new default signer
|
|
1766
|
+
:param approval_source_map: Optional new approval source map
|
|
1767
|
+
:param clear_source_map: Optional new clear source map
|
|
1768
|
+
:return: A new AppClient instance
|
|
1769
|
+
|
|
1770
|
+
:example:
|
|
1771
|
+
>>> client = AppClient(params)
|
|
1772
|
+
>>> cloned_client = client.clone(app_name="Cloned App", default_sender="NEW_SENDER")
|
|
1773
|
+
"""
|
|
1774
|
+
return AppClient(
|
|
1775
|
+
AppClientParams(
|
|
1776
|
+
app_id=self._app_id,
|
|
1777
|
+
algorand=self._algorand,
|
|
1778
|
+
app_spec=self._app_spec,
|
|
1779
|
+
app_name=self._app_name if app_name is _MISSING else app_name,
|
|
1780
|
+
default_sender=self._default_sender if default_sender is _MISSING else default_sender,
|
|
1781
|
+
default_signer=self._default_signer if default_signer is _MISSING else default_signer,
|
|
1782
|
+
approval_source_map=(
|
|
1783
|
+
self._approval_source_map if approval_source_map is _MISSING else approval_source_map
|
|
1784
|
+
),
|
|
1785
|
+
clear_source_map=(self._clear_source_map if clear_source_map is _MISSING else clear_source_map),
|
|
1786
|
+
)
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
def export_source_maps(self) -> AppSourceMaps:
|
|
1790
|
+
"""Export the application's source maps.
|
|
1791
|
+
|
|
1792
|
+
:return: The application's source maps
|
|
1793
|
+
:raises ValueError: If source maps haven't been loaded
|
|
1794
|
+
"""
|
|
1795
|
+
if not self._approval_source_map or not self._clear_source_map:
|
|
1796
|
+
raise ValueError(
|
|
1797
|
+
"Unable to export source maps; they haven't been loaded into this client - "
|
|
1798
|
+
"you need to call create, update, or deploy first"
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
return AppSourceMaps(
|
|
1802
|
+
approval_source_map=self._approval_source_map,
|
|
1803
|
+
clear_source_map=self._clear_source_map,
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
def import_source_maps(self, source_maps: AppSourceMaps) -> None:
|
|
1807
|
+
"""Import source maps for the application.
|
|
1808
|
+
|
|
1809
|
+
:param source_maps: The source maps to import
|
|
1810
|
+
:raises ValueError: If source maps are invalid or missing
|
|
1811
|
+
"""
|
|
1812
|
+
if not source_maps.approval_source_map:
|
|
1813
|
+
raise ValueError("Approval source map is required")
|
|
1814
|
+
if not source_maps.clear_source_map:
|
|
1815
|
+
raise ValueError("Clear source map is required")
|
|
1816
|
+
|
|
1817
|
+
if not isinstance(source_maps.approval_source_map, dict | ProgramSourceMap):
|
|
1818
|
+
raise ValueError("Approval source map supplied is of invalid type. Must be a raw dict or `SourceMap`")
|
|
1819
|
+
if not isinstance(source_maps.clear_source_map, dict | ProgramSourceMap):
|
|
1820
|
+
raise ValueError("Clear source map supplied is of invalid type. Must be a raw dict or `SourceMap`")
|
|
1821
|
+
|
|
1822
|
+
self._approval_source_map = (
|
|
1823
|
+
ProgramSourceMap(source_map=source_maps.approval_source_map)
|
|
1824
|
+
if isinstance(source_maps.approval_source_map, dict)
|
|
1825
|
+
else source_maps.approval_source_map
|
|
1826
|
+
)
|
|
1827
|
+
self._clear_source_map = (
|
|
1828
|
+
ProgramSourceMap(source_map=source_maps.clear_source_map)
|
|
1829
|
+
if isinstance(source_maps.clear_source_map, dict)
|
|
1830
|
+
else source_maps.clear_source_map
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
def get_local_state(self, address: str) -> dict[str, AppState]:
|
|
1834
|
+
"""Get local state for an account.
|
|
1835
|
+
|
|
1836
|
+
:param address: The account address
|
|
1837
|
+
:return: The account's local state for this application
|
|
1838
|
+
"""
|
|
1839
|
+
return self._state_accessor.get_local_state(address)
|
|
1840
|
+
|
|
1841
|
+
def get_global_state(self) -> dict[str, AppState]:
|
|
1842
|
+
"""Get the application's global state.
|
|
1843
|
+
|
|
1844
|
+
:return: The application's global state
|
|
1845
|
+
:example:
|
|
1846
|
+
>>> global_state = client.get_global_state()
|
|
1847
|
+
"""
|
|
1848
|
+
return self._state_accessor.get_global_state()
|
|
1849
|
+
|
|
1850
|
+
def get_box_names(self) -> list[BoxName]:
|
|
1851
|
+
"""Get all box names for the application.
|
|
1852
|
+
|
|
1853
|
+
:return: List of box names
|
|
1854
|
+
|
|
1855
|
+
:example:
|
|
1856
|
+
>>> box_names = client.get_box_names()
|
|
1857
|
+
"""
|
|
1858
|
+
return self._algorand.app.get_box_names(self._app_id)
|
|
1859
|
+
|
|
1860
|
+
def get_box_value(self, name: BoxIdentifier) -> bytes:
|
|
1861
|
+
"""Get the value of a box.
|
|
1862
|
+
|
|
1863
|
+
:param name: The box identifier
|
|
1864
|
+
:return: The box value as bytes
|
|
1865
|
+
|
|
1866
|
+
:example:
|
|
1867
|
+
>>> box_value = client.get_box_value(box_name)
|
|
1868
|
+
"""
|
|
1869
|
+
return self._algorand.app.get_box_value(self._app_id, name)
|
|
1870
|
+
|
|
1871
|
+
def get_box_value_from_abi_type(self, name: BoxIdentifier, abi_type: ABIType) -> ABIValue:
|
|
1872
|
+
"""Get a box value decoded according to an ABI type.
|
|
1873
|
+
|
|
1874
|
+
:param name: The box identifier
|
|
1875
|
+
:param abi_type: The ABI type to decode as
|
|
1876
|
+
:return: The decoded box value
|
|
1877
|
+
|
|
1878
|
+
:example:
|
|
1879
|
+
>>> box_value = client.get_box_value_from_abi_type(box_name, abi_type)
|
|
1880
|
+
"""
|
|
1881
|
+
return self._algorand.app.get_box_value_from_abi_type(self._app_id, name, abi_type)
|
|
1882
|
+
|
|
1883
|
+
def get_box_values(self, filter_func: Callable[[BoxName], bool] | None = None) -> list[BoxValue]:
|
|
1884
|
+
"""Get values for multiple boxes.
|
|
1885
|
+
|
|
1886
|
+
:param filter_func: Optional function to filter box names
|
|
1887
|
+
:return: List of box values
|
|
1888
|
+
|
|
1889
|
+
:example:
|
|
1890
|
+
>>> box_values = client.get_box_values()
|
|
1891
|
+
"""
|
|
1892
|
+
names = [n for n in self.get_box_names() if not filter_func or filter_func(n)]
|
|
1893
|
+
values = self._algorand.app.get_box_values(self.app_id, [n.name_raw for n in names])
|
|
1894
|
+
return [BoxValue(name=n, value=v) for n, v in zip(names, values, strict=False)]
|
|
1895
|
+
|
|
1896
|
+
def get_box_values_from_abi_type(
|
|
1897
|
+
self, abi_type: ABIType, filter_func: Callable[[BoxName], bool] | None = None
|
|
1898
|
+
) -> list[BoxABIValue]:
|
|
1899
|
+
"""Get multiple box values decoded according to an ABI type.
|
|
1900
|
+
|
|
1901
|
+
:param abi_type: The ABI type to decode as
|
|
1902
|
+
:param filter_func: Optional function to filter box names
|
|
1903
|
+
:return: List of decoded box values
|
|
1904
|
+
|
|
1905
|
+
:example:
|
|
1906
|
+
>>> box_values = client.get_box_values_from_abi_type(abi_type)
|
|
1907
|
+
"""
|
|
1908
|
+
names = self.get_box_names()
|
|
1909
|
+
if filter_func:
|
|
1910
|
+
names = [name for name in names if filter_func(name)]
|
|
1911
|
+
|
|
1912
|
+
values = self._algorand.app.get_box_values_from_abi_type(
|
|
1913
|
+
self.app_id, [name.name_raw for name in names], abi_type
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
return [BoxABIValue(name=name, value=values[i]) for i, name in enumerate(names)]
|
|
1917
|
+
|
|
1918
|
+
def fund_app_account(
|
|
1919
|
+
self, params: FundAppAccountParams, send_params: SendParams | None = None
|
|
1920
|
+
) -> SendSingleTransactionResult:
|
|
1921
|
+
"""Fund the application's account.
|
|
1922
|
+
|
|
1923
|
+
:param params: The funding parameters
|
|
1924
|
+
:param send_params: Send parameters, defaults to None
|
|
1925
|
+
:return: The transaction result
|
|
1926
|
+
|
|
1927
|
+
:example:
|
|
1928
|
+
>>> result = client.fund_app_account(params)
|
|
1929
|
+
"""
|
|
1930
|
+
return self.send.fund_app_account(params, send_params)
|
|
1931
|
+
|
|
1932
|
+
def _expose_logic_error(self, e: Exception, *, is_clear_state_program: bool = False) -> Exception:
|
|
1933
|
+
source_info = None
|
|
1934
|
+
if hasattr(self._app_spec, "source_info") and self._app_spec.source_info:
|
|
1935
|
+
source_info = (
|
|
1936
|
+
self._app_spec.source_info.clear if is_clear_state_program else self._app_spec.source_info.approval
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
pc_offset_method = source_info.pc_offset_method if source_info else None
|
|
1940
|
+
|
|
1941
|
+
program: bytes | None = None
|
|
1942
|
+
if pc_offset_method == "cblocks":
|
|
1943
|
+
app_info = self._algorand.app.get_by_id(self.app_id)
|
|
1944
|
+
program = app_info.clear_state_program if is_clear_state_program else app_info.approval_program
|
|
1945
|
+
|
|
1946
|
+
return AppClient._expose_logic_error_static(
|
|
1947
|
+
e=e,
|
|
1948
|
+
app_spec=self._app_spec,
|
|
1949
|
+
is_clear_state_program=is_clear_state_program,
|
|
1950
|
+
approval_source_map=self._approval_source_map,
|
|
1951
|
+
clear_source_map=self._clear_source_map,
|
|
1952
|
+
program=program,
|
|
1953
|
+
approval_source_info=(self._app_spec.source_info.approval if self._app_spec.source_info else None),
|
|
1954
|
+
clear_source_info=(self._app_spec.source_info.clear if self._app_spec.source_info else None),
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
def _handle_call_errors(self, call: Callable[[], T]) -> T:
|
|
1958
|
+
try:
|
|
1959
|
+
return call()
|
|
1960
|
+
except Exception as e:
|
|
1961
|
+
raise self._expose_logic_error(e=e) from None
|
|
1962
|
+
|
|
1963
|
+
def _is_new_app_error_for_this_app(self, error: Exception) -> bool:
|
|
1964
|
+
"""Check if an error from a new app (app_id=0) is for this specific app by comparing program bytecode."""
|
|
1965
|
+
if not hasattr(error, "sent_transactions") or not error.sent_transactions:
|
|
1966
|
+
return False
|
|
1967
|
+
|
|
1968
|
+
# Find the transaction that caused the error
|
|
1969
|
+
txn = None
|
|
1970
|
+
for t in error.sent_transactions:
|
|
1971
|
+
if hasattr(t, "get_txid") and t.get_txid() in str(error):
|
|
1972
|
+
txn = t
|
|
1973
|
+
break
|
|
1974
|
+
|
|
1975
|
+
app_call = getattr(txn, "app_call", None)
|
|
1976
|
+
if not txn or app_call is None:
|
|
1977
|
+
return False
|
|
1978
|
+
|
|
1979
|
+
return bool(
|
|
1980
|
+
app_call.clear_state_program == self._last_compiled.get("clear")
|
|
1981
|
+
and app_call.approval_program == self._last_compiled.get("approval")
|
|
1982
|
+
)
|
|
1983
|
+
|
|
1984
|
+
def _handle_call_errors_transform(self, error: Exception) -> Exception:
|
|
1985
|
+
"""Error transformer function for app-specific logic errors.
|
|
1986
|
+
|
|
1987
|
+
This will be called by the transaction composer when errors occur during
|
|
1988
|
+
simulate or send operations to provide better error messages with source maps.
|
|
1989
|
+
|
|
1990
|
+
:param error: The error to potentially transform
|
|
1991
|
+
:return: The transformed error if it's an app logic error, otherwise the original error
|
|
1992
|
+
"""
|
|
1993
|
+
try:
|
|
1994
|
+
# Check if this is a logic error that we can parse
|
|
1995
|
+
from algokit_utils.errors.logic_error import parse_logic_error
|
|
1996
|
+
|
|
1997
|
+
error_details = parse_logic_error(str(error))
|
|
1998
|
+
if not error_details:
|
|
1999
|
+
# Not a logic error, return unchanged
|
|
2000
|
+
return error
|
|
2001
|
+
|
|
2002
|
+
# Check if this error is for our app
|
|
2003
|
+
should_transform = False
|
|
2004
|
+
|
|
2005
|
+
if self._app_id == 0:
|
|
2006
|
+
# For new apps (app_id == 0), we can't use app ID filtering
|
|
2007
|
+
# Instead check the programs to identify if this is the correct app
|
|
2008
|
+
should_transform = self._is_new_app_error_for_this_app(error)
|
|
2009
|
+
else:
|
|
2010
|
+
# Only handle errors for this specific app
|
|
2011
|
+
app_id_string = f"app={self._app_id}"
|
|
2012
|
+
should_transform = app_id_string in str(error)
|
|
2013
|
+
|
|
2014
|
+
if not should_transform:
|
|
2015
|
+
# Error is not for this app, return unchanged
|
|
2016
|
+
return error
|
|
2017
|
+
|
|
2018
|
+
# This is a logic error for our app, transform it
|
|
2019
|
+
return self._expose_logic_error(e=error)
|
|
2020
|
+
|
|
2021
|
+
except Exception:
|
|
2022
|
+
# If transformation fails, return the original error
|
|
2023
|
+
return error
|
|
2024
|
+
|
|
2025
|
+
def _get_sender(self, sender: str | None) -> str:
|
|
2026
|
+
if not sender and not self._default_sender:
|
|
2027
|
+
raise Exception(
|
|
2028
|
+
f"No sender provided and no default sender present in app client for call to app {self.app_name}"
|
|
2029
|
+
)
|
|
2030
|
+
return sender or self._default_sender # type: ignore[return-value]
|
|
2031
|
+
|
|
2032
|
+
def _get_signer(
|
|
2033
|
+
self, sender: str | None, signer: TransactionSigner | AddressWithTransactionSigner | None
|
|
2034
|
+
) -> TransactionSigner | AddressWithTransactionSigner | None:
|
|
2035
|
+
return signer or (self._default_signer if not sender or sender == self._default_sender else None)
|
|
2036
|
+
|
|
2037
|
+
def _get_bare_params(self, params: dict[str, Any], on_complete: OnApplicationComplete) -> dict[str, Any]:
|
|
2038
|
+
sender = self._get_sender(params.get("sender"))
|
|
2039
|
+
return {
|
|
2040
|
+
**params,
|
|
2041
|
+
"app_id": self._app_id,
|
|
2042
|
+
"sender": sender,
|
|
2043
|
+
"signer": self._get_signer(params.get("sender"), params.get("signer")),
|
|
2044
|
+
"on_complete": on_complete,
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
def _get_abi_args_with_default_values(
|
|
2048
|
+
self,
|
|
2049
|
+
*,
|
|
2050
|
+
method_name_or_signature: str,
|
|
2051
|
+
args: Sequence[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None,
|
|
2052
|
+
sender: str,
|
|
2053
|
+
) -> list[Any]:
|
|
2054
|
+
method = self._app_spec.get_abi_method(method_name_or_signature)
|
|
2055
|
+
result = list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None]()
|
|
2056
|
+
|
|
2057
|
+
if args and len(method.args) < len(args):
|
|
2058
|
+
raise ValueError(
|
|
2059
|
+
f"Unexpected arg at position {len(method.args)}. {method.name} only expects {len(method.args)} args"
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
for i, method_arg in enumerate(method.args):
|
|
2063
|
+
arg_value = args[i] if args and i < len(args) else None
|
|
2064
|
+
|
|
2065
|
+
if arg_value is not None:
|
|
2066
|
+
result.append(arg_value)
|
|
2067
|
+
continue
|
|
2068
|
+
|
|
2069
|
+
default_value = method_arg.default_value
|
|
2070
|
+
arg_type = method_arg.type
|
|
2071
|
+
arg_name = method_arg.name or f"arg{i + 1}"
|
|
2072
|
+
if isinstance(arg_type, arc56.TransactionType):
|
|
2073
|
+
result.append(None)
|
|
2074
|
+
elif default_value:
|
|
2075
|
+
assert isinstance(arg_type, arc56.ReferenceType | abi.ABIType)
|
|
2076
|
+
result.append(self._get_abi_arg_default_value(arg_name, arg_type, default_value, sender))
|
|
2077
|
+
else:
|
|
2078
|
+
raise ValueError(f"No value provided for required argument {arg_name} in call to method {method.name}")
|
|
2079
|
+
|
|
2080
|
+
return result
|
|
2081
|
+
|
|
2082
|
+
def _get_abi_arg_default_value(
|
|
2083
|
+
self,
|
|
2084
|
+
arg_name: str,
|
|
2085
|
+
arg_type: abi.ABIType | arc56.ReferenceType,
|
|
2086
|
+
default_value: arc56.DefaultValue,
|
|
2087
|
+
sender: str,
|
|
2088
|
+
) -> ABIValue:
|
|
2089
|
+
match default_value.source:
|
|
2090
|
+
case "literal":
|
|
2091
|
+
value_raw = base64.b64decode(default_value.data)
|
|
2092
|
+
value_type = default_value.type or arg_type
|
|
2093
|
+
return get_abi_decoded_value(value_raw, value_type)
|
|
2094
|
+
|
|
2095
|
+
case "method":
|
|
2096
|
+
default_method = self._app_spec.get_abi_method(default_value.data)
|
|
2097
|
+
empty_args = [None] * len(default_method.args)
|
|
2098
|
+
call_result = self.send.call(
|
|
2099
|
+
AppClientMethodCallParams(
|
|
2100
|
+
method=default_value.data,
|
|
2101
|
+
args=empty_args,
|
|
2102
|
+
sender=sender,
|
|
2103
|
+
)
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
if call_result.abi_return is None:
|
|
2107
|
+
raise ValueError("Default value method call did not return a value")
|
|
2108
|
+
assert isinstance(default_method.returns.type, abi.ABIType)
|
|
2109
|
+
return call_result.abi_return
|
|
2110
|
+
|
|
2111
|
+
case "local" | "global" | "box":
|
|
2112
|
+
key = base64.b64decode(default_value.data)
|
|
2113
|
+
try:
|
|
2114
|
+
value, storage_key = self._get_storage_value(default_value.source, key, sender)
|
|
2115
|
+
except KeyError:
|
|
2116
|
+
raise ValueError(
|
|
2117
|
+
f"Key '{default_value.data}' not found in {default_value.source} "
|
|
2118
|
+
f"storage for argument {arg_name}"
|
|
2119
|
+
) from None
|
|
2120
|
+
|
|
2121
|
+
decoded_value: ABIValue
|
|
2122
|
+
if isinstance(value, bytes):
|
|
2123
|
+
# special case to convert raw AVM bytes to a native string type suitable for encoding
|
|
2124
|
+
if storage_key.value_type == arc56.AVMType.BYTES and isinstance(arg_type, abi.StringType):
|
|
2125
|
+
decoded_value = value.decode("utf-8")
|
|
2126
|
+
else:
|
|
2127
|
+
decoded_value = get_abi_decoded_value(value, storage_key.value_type)
|
|
2128
|
+
else:
|
|
2129
|
+
decoded_value = value
|
|
2130
|
+
return decoded_value
|
|
2131
|
+
case _:
|
|
2132
|
+
assert_never(default_value.source)
|
|
2133
|
+
|
|
2134
|
+
def _get_storage_value(
|
|
2135
|
+
self, source: Literal["local", "global", "box"], key: bytes, sender: str
|
|
2136
|
+
) -> tuple[bytes | int | str, arc56.StorageKey]:
|
|
2137
|
+
state_keys = self.app_spec.state.keys
|
|
2138
|
+
if source == "global":
|
|
2139
|
+
state = {s.key_raw: s for s in self.get_global_state().values()}[key]
|
|
2140
|
+
value = state.value_raw if state.value_raw is not None else state.value
|
|
2141
|
+
storage_keys = state_keys.global_state
|
|
2142
|
+
elif source == "local":
|
|
2143
|
+
state = {s.key_raw: s for s in self.get_local_state(sender).values()}[key]
|
|
2144
|
+
value = state.value_raw if state.value_raw is not None else state.value
|
|
2145
|
+
storage_keys = state_keys.local_state
|
|
2146
|
+
elif source == "box":
|
|
2147
|
+
value = self.get_box_value(key)
|
|
2148
|
+
storage_keys = state_keys.box
|
|
2149
|
+
else:
|
|
2150
|
+
assert_never(source)
|
|
2151
|
+
|
|
2152
|
+
key_base64 = base64.b64encode(key).decode("ascii")
|
|
2153
|
+
storage_key = {sk.key: sk for sk in storage_keys.values()}[key_base64]
|
|
2154
|
+
return value, storage_key
|
|
2155
|
+
|
|
2156
|
+
def _get_abi_params(self, params: dict[str, Any], on_complete: OnApplicationComplete) -> dict[str, Any]:
|
|
2157
|
+
sender = self._get_sender(params.get("sender"))
|
|
2158
|
+
method = self._app_spec.get_abi_method(params["method"])
|
|
2159
|
+
args = self._get_abi_args_with_default_values(
|
|
2160
|
+
method_name_or_signature=params["method"], args=params.get("args"), sender=sender
|
|
2161
|
+
)
|
|
2162
|
+
return {
|
|
2163
|
+
**params,
|
|
2164
|
+
"appId": self._app_id,
|
|
2165
|
+
"sender": sender,
|
|
2166
|
+
"signer": self._get_signer(params.get("sender"), params.get("signer")),
|
|
2167
|
+
"method": method,
|
|
2168
|
+
"onComplete": on_complete,
|
|
2169
|
+
"args": args,
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
def _process_method_call_return(
|
|
2173
|
+
self,
|
|
2174
|
+
result: Callable[[], SendAppUpdateTransactionResult[ABIReturn] | SendAppTransactionResult[ABIReturn]],
|
|
2175
|
+
method: arc56.Method,
|
|
2176
|
+
) -> SendAppUpdateTransactionResult[Arc56ReturnValueType] | SendAppTransactionResult[Arc56ReturnValueType]:
|
|
2177
|
+
_ = method # kept for compatibility
|
|
2178
|
+
result_value = result()
|
|
2179
|
+
abi_return_value: Arc56ReturnValueType
|
|
2180
|
+
if isinstance(result_value.abi_return, ABIReturn):
|
|
2181
|
+
if result_value.abi_return.decode_error:
|
|
2182
|
+
raise ValueError(result_value.abi_return.decode_error)
|
|
2183
|
+
abi_return_value = result_value.abi_return.value
|
|
2184
|
+
else:
|
|
2185
|
+
abi_return_value = None
|
|
2186
|
+
|
|
2187
|
+
if isinstance(result_value, SendAppUpdateTransactionResult):
|
|
2188
|
+
return SendAppUpdateTransactionResult[Arc56ReturnValueType](
|
|
2189
|
+
**{**result_value.__dict__, "abi_return": abi_return_value}
|
|
2190
|
+
)
|
|
2191
|
+
return SendAppTransactionResult[Arc56ReturnValueType](
|
|
2192
|
+
**{**result_value.__dict__, "abi_return": abi_return_value}
|
|
2193
|
+
)
|