diff --git a/docs/markdown/index.md b/docs/markdown/index.md index a3fa051..7197256 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -7,46 +7,47 @@ The goal of this library is to provide intuitive, productive utility functions t Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks. #### NOTE + If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts). [Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]() # Contents -* [Account management](capabilities/account.md) - * [`Account`](capabilities/account.md#account) -* [Client management](capabilities/client.md) - * [Network configuration](capabilities/client.md#network-configuration) - * [Clients](capabilities/client.md#clients) -* [App client](capabilities/app-client.md) - * [Design](capabilities/app-client.md#design) - * [Creating an application client](capabilities/app-client.md#creating-an-application-client) - * [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) - * [Composing calls](capabilities/app-client.md#composing-calls) - * [Reading state](capabilities/app-client.md#reading-state) - * [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) -* [App deployment](capabilities/app-deploy.md) - * [Design](capabilities/app-deploy.md#design) - * [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) - * [Deploying an application](capabilities/app-deploy.md#deploying-an-application) -* [Algo transfers](capabilities/transfer.md) - * [Transferring Algos](capabilities/transfer.md#transferring-algos) - * [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) - * [Transfering Assets](capabilities/transfer.md#transfering-assets) - * [Dispenser](capabilities/transfer.md#dispenser) -* [TestNet Dispenser Client](capabilities/dispenser-client.md) - * [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) - * [Funding an Account](capabilities/dispenser-client.md#funding-an-account) - * [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) - * [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) - * [Error Handling](capabilities/dispenser-client.md#error-handling) -* [Debugger](capabilities/debugger.md) - * [Configuration](capabilities/debugger.md#configuration) - * [Debugging Utilities](capabilities/debugger.md#debugging-utilities) -* [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) - * [Data](apidocs/algokit_utils/algokit_utils.md#data) - * [Classes](apidocs/algokit_utils/algokit_utils.md#classes) - * [Functions](apidocs/algokit_utils/algokit_utils.md#functions) +- [Account management](capabilities/account.md) + - [`Account`](capabilities/account.md#account) +- [Client management](capabilities/client.md) + - [Network configuration](capabilities/client.md#network-configuration) + - [Clients](capabilities/client.md#clients) +- [App client](capabilities/app-client.md) + - [Design](capabilities/app-client.md#design) + - [Creating an application client](capabilities/app-client.md#creating-an-application-client) + - [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app) + - [Composing calls](capabilities/app-client.md#composing-calls) + - [Reading state](capabilities/app-client.md#reading-state) + - [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors) +- [App deployment](capabilities/app-deploy.md) + - [Design](capabilities/app-deploy.md#design) + - [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator) + - [Deploying an application](capabilities/app-deploy.md#deploying-an-application) +- [Algo transfers](capabilities/transfer.md) + - [Transferring Algos](capabilities/transfer.md#transferring-algos) + - [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos) + - [Transfering Assets](capabilities/transfer.md#transfering-assets) + - [Dispenser](capabilities/transfer.md#dispenser) +- [TestNet Dispenser Client](capabilities/dispenser-client.md) + - [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client) + - [Funding an Account](capabilities/dispenser-client.md#funding-an-account) + - [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund) + - [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit) + - [Error Handling](capabilities/dispenser-client.md#error-handling) +- [Debugger](capabilities/debugger.md) + - [Configuration](capabilities/debugger.md#configuration) + - [Debugging Utilities](capabilities/debugger.md#debugging-utilities) +- [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md) + - [Data](apidocs/algokit_utils/algokit_utils.md#data) + - [Classes](apidocs/algokit_utils/algokit_utils.md#classes) + - [Functions](apidocs/algokit_utils/algokit_utils.md#functions) diff --git a/src/algokit_utils/_legacy_v2/_ensure_funded.py b/src/algokit_utils/_legacy_v2/_ensure_funded.py index 2db90f3..7f9d662 100644 --- a/src/algokit_utils/_legacy_v2/_ensure_funded.py +++ b/src/algokit_utils/_legacy_v2/_ensure_funded.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AccountTransactionSigner from algosdk.transaction import SuggestedParams from algosdk.v2client.algod import AlgodClient +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.account import get_dispenser_account @@ -115,6 +116,7 @@ def _fund_using_transfer( return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt) +@deprecated("Deprecated") def ensure_funded( client: AlgodClient, parameters: EnsureBalanceParameters, diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index d98a875..f72a646 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -5,6 +5,7 @@ from algosdk.account import address_from_private_key from algosdk.mnemonic import from_private_key, to_private_key from algosdk.util import algos_to_microalgos +from typing_extensions import deprecated from algokit_utils._legacy_v2._transfer import TransferParameters, transfer from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet @@ -30,6 +31,7 @@ _DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000 +@deprecated("Deprecated") def get_account_from_mnemonic(mnemonic: str) -> Account: """Convert a mnemonic (25 word passphrase) into an Account""" private_key = to_private_key(mnemonic) @@ -37,6 +39,7 @@ def get_account_from_mnemonic(mnemonic: str) -> Account: return Account(private_key=private_key, address=address) +@deprecated("Deprecated") def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: """Creates a wallet with specified name""" wallet_id = kmd_client.create_wallet(name, "")["id"] @@ -50,6 +53,7 @@ def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account: return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated("Deprecated") def get_or_create_kmd_wallet_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: @@ -90,6 +94,7 @@ def _is_default_account(account: dict[str, Any]) -> bool: return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE) +@deprecated("Deprecated") def get_localnet_default_account(client: "AlgodClient") -> Account: """Returns the default Account in a LocalNet instance""" if not is_localnet(client): @@ -102,6 +107,7 @@ def get_localnet_default_account(client: "AlgodClient") -> Account: return account +@deprecated("Deprecated") def get_dispenser_account(client: "AlgodClient") -> Account: """Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet""" if is_localnet(client): @@ -109,6 +115,7 @@ def get_dispenser_account(client: "AlgodClient") -> Account: return get_account(client, "DISPENSER") +@deprecated("Deprecated") def get_kmd_wallet_account( client: "AlgodClient", kmd_client: "KMDClient", @@ -142,6 +149,7 @@ def get_kmd_wallet_account( return get_account_from_mnemonic(from_private_key(private_account_key)) +@deprecated("Deprecated") def get_account( client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None ) -> Account: diff --git a/src/algokit_utils/_legacy_v2/application_client.py b/src/algokit_utils/_legacy_v2/application_client.py index 33dfe95..254041b 100644 --- a/src/algokit_utils/_legacy_v2/application_client.py +++ b/src/algokit_utils/_legacy_v2/application_client.py @@ -27,6 +27,7 @@ from algosdk.constants import APP_PAGE_MAX_SIZE from algosdk.logic import get_application_address from algosdk.source_map import SourceMap +from typing_extensions import deprecated import algokit_utils._legacy_v2.application_specification as au_spec import algokit_utils._legacy_v2.deploy as au_deploy @@ -83,6 +84,7 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) +@deprecated("Use AppClient from algokit_utils.applications instead") class ApplicationClient: """A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app""" @@ -1254,6 +1256,7 @@ def _try_convert_to_logic_error( return None +@deprecated("Deprecated") def execute_atc_with_logic_error( atc: AtomicTransactionComposer, algod_client: "AlgodClient", diff --git a/src/algokit_utils/_legacy_v2/application_specification.py b/src/algokit_utils/_legacy_v2/application_specification.py index 865dece..887f9c1 100644 --- a/src/algokit_utils/_legacy_v2/application_specification.py +++ b/src/algokit_utils/_legacy_v2/application_specification.py @@ -8,6 +8,7 @@ from algosdk.abi import Contract from algosdk.abi.method import MethodDict from algosdk.transaction import StateSchema +from typing_extensions import deprecated __all__ = [ "CallConfig", @@ -136,6 +137,7 @@ def _decode_state_schema(data: dict[str, int]) -> StateSchema: ) +@deprecated("Deprecated") @dataclasses.dataclass(kw_only=True) class ApplicationSpecification: """ARC-0032 application specification diff --git a/src/algokit_utils/_legacy_v2/asset.py b/src/algokit_utils/_legacy_v2/asset.py index 2f71cbf..bc001ce 100644 --- a/src/algokit_utils/_legacy_v2/asset.py +++ b/src/algokit_utils/_legacy_v2/asset.py @@ -4,6 +4,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionWithSigner from algosdk.constants import TX_GROUP_LIMIT from algosdk.transaction import AssetTransferTxn +from typing_extensions import deprecated if TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -68,6 +69,7 @@ def _ensure_asset_balance_conditions( raise ValueError(error_message) +@deprecated("Deprecated") def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt-in to a list of assets on the Algorand blockchain. Before an account can receive a specific asset, @@ -116,6 +118,7 @@ def opt_in(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) return result +@deprecated("Deprecated") def opt_out(algod_client: "AlgodClient", account: Account, asset_ids: list[int]) -> dict[int, str]: """ Opt out from a list of Algorand Standard Assets (ASAs) by transferring them back to their creators. diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index 0aadb72..62c17ad 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -11,7 +11,7 @@ from algosdk import transaction from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner from algosdk.transaction import StateSchema -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._legacy_v2.application_specification import ( ApplicationSpecification, @@ -175,6 +175,7 @@ def _parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: return None +@deprecated("Deprecated") def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -> AppLookup: """Returns a mapping of Application names to {py:class}`AppMetaData` for all Applications created by specified creator that have a transaction note containing {py:class}`AppDeployMetaData` @@ -255,6 +256,7 @@ class AppChanges: schema_change_description: str | None +@deprecated("Deprecated") def check_for_app_changes( algod_client: "AlgodClient", *, @@ -412,7 +414,7 @@ def check_template_variables(approval_program: str, template_values: TemplateVal logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided") -@deprecated(reason="Use `AppManager.replace_template_variables` instead", version="3.0.0") +@deprecated("Use `AppManager.replace_template_variables` instead") def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str: """Replaces `TMPL_*` variables in `program` with `template_values` diff --git a/src/algokit_utils/_legacy_v2/logic_error.py b/src/algokit_utils/_legacy_v2/logic_error.py index 0878887..a3fb9ac 100644 --- a/src/algokit_utils/_legacy_v2/logic_error.py +++ b/src/algokit_utils/_legacy_v2/logic_error.py @@ -2,7 +2,7 @@ from copy import copy from typing import TYPE_CHECKING, TypedDict -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._legacy_v2.models import SimulationTrace @@ -39,7 +39,7 @@ def parse_logic_error( } -@deprecated(reason="Use algokit_utils.models.error.LogicError instead", version="3.0.0") +@deprecated("Use algokit_utils.models.error.LogicError instead") class LogicError(Exception): def __init__( self, diff --git a/src/algokit_utils/_legacy_v2/models.py b/src/algokit_utils/_legacy_v2/models.py index d20bed8..7887cb6 100644 --- a/src/algokit_utils/_legacy_v2/models.py +++ b/src/algokit_utils/_legacy_v2/models.py @@ -9,7 +9,7 @@ SimulateAtomicTransactionResponse, TransactionSigner, ) -from deprecated import deprecated +from typing_extensions import deprecated # Imports from latest sdk version that rely on models previously used in legacy v2 (but moved to root models/*) @@ -185,17 +185,17 @@ class CreateCallParametersDict(OnCompleteCallParametersDict, total=False): # Pre 1.3.1 backwards compatibility -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class RawTransactionParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParameters instead", version="1.3.1") +@deprecated("Use TransactionParameters instead") class CommonCallParameters(TransactionParameters): """Deprecated, use TransactionParameters instead""" -@deprecated(reason="Use TransactionParametersDict instead", version="1.3.1") +@deprecated("Use TransactionParametersDict instead") class CommonCallParametersDict(TransactionParametersDict): """Deprecated, use TransactionParametersDict instead""" diff --git a/src/algokit_utils/_legacy_v2/network_clients.py b/src/algokit_utils/_legacy_v2/network_clients.py index b1bcc2c..ab4a4db 100644 --- a/src/algokit_utils/_legacy_v2/network_clients.py +++ b/src/algokit_utils/_legacy_v2/network_clients.py @@ -6,6 +6,7 @@ from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +from typing_extensions import deprecated __all__ = [ "AlgoClientConfig", @@ -40,12 +41,14 @@ class AlgoClientConfigs: kmd_config: AlgoClientConfig | None +@deprecated("Deprecated") def get_default_localnet_config(config: Literal["algod", "indexer", "kmd"]) -> AlgoClientConfig: """Returns the client configuration to point to the default LocalNet""" port = {"algod": 4001, "indexer": 8980, "kmd": 4002}[config] return AlgoClientConfig(server=f"http://localhost:{port}", token="a" * 64) +@deprecated("Deprecated") def get_algonode_config( network: Literal["testnet", "mainnet"], config: Literal["algod", "indexer"], token: str ) -> AlgoClientConfig: @@ -56,6 +59,7 @@ def get_algonode_config( ) +@deprecated("Deprecated") def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: """Returns an {py:class}`algosdk.v2client.algod.AlgodClient` from `config` or environment @@ -65,6 +69,7 @@ def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient: return AlgodClient(config.token, config.server, headers) +@deprecated("Deprecated") def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from `config` or environment @@ -73,6 +78,7 @@ def get_kmd_client(config: AlgoClientConfig | None = None) -> KMDClient: return KMDClient(config.token, config.server) +@deprecated("Deprecated") def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: """Returns an {py:class}`algosdk.v2client.indexer.IndexerClient` from `config` or environment. @@ -82,24 +88,28 @@ def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient: return IndexerClient(config.token, config.server, headers) +@deprecated("Deprecated") def is_localnet(client: AlgodClient) -> bool: """Returns True if client genesis is `devnet-v1` or `sandnet-v1`""" params = client.suggested_params() return params.gen in ["devnet-v1", "sandnet-v1", "dockernet-v1"] +@deprecated("Deprecated") def is_mainnet(client: AlgodClient) -> bool: """Returns True if client genesis is `mainnet-v1`""" params = client.suggested_params() return params.gen in ["mainnet-v1.0", "mainnet-v1", "mainnet"] +@deprecated("Deprecated") def is_testnet(client: AlgodClient) -> bool: """Returns True if client genesis is `testnet-v1`""" params = client.suggested_params() return params.gen in ["testnet-v1.0", "testnet-v1", "testnet"] +@deprecated("Deprecated") def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient: """Returns an {py:class}`algosdk.kmd.KMDClient` from supplied `client` diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index d4d95d1..9ef0e89 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -4,18 +4,28 @@ from algosdk.account import generate_account from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner +from algosdk.transaction import SuggestedParams from typing_extensions import Self from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account from algokit_utils.clients.client_manager import ClientManager +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer +from algokit_utils.transactions.transaction_sender import SendSingleTransactionResult -@dataclass +@dataclass(frozen=True, kw_only=True) class AddressAndSigner: address: str signer: TransactionSigner +@dataclass(frozen=True, kw_only=True) +class EnsureFundedResponse(SendSingleTransactionResult): + transaction_id: str + amount_funded: AlgoAmount + + class AccountManager: """Creates and keeps track of addresses and signers""" @@ -112,9 +122,9 @@ def random(self) -> AddressAndSigner: (sk, addr) = generate_account() signer = AccountTransactionSigner(sk) - self.set_signer(addr, signer) + self.set_signer(str(addr), signer) - return AddressAndSigner(address=addr, signer=signer) + return AddressAndSigner(address=str(addr), signer=signer) def dispenser(self) -> AddressAndSigner: """ @@ -138,3 +148,107 @@ def localnet_dispenser(self) -> AddressAndSigner: acct = get_localnet_default_account(self._client_manager.algod) self.set_signer(acct.address, acct.signer) return AddressAndSigner(address=acct.address, signer=acct.signer) + + def ensure_funded( # noqa: PLR0913 + self, + account_fo_fund: str, + dispenser_account: str, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount, + # Sender params + max_rounds_to_wait: int | None = None, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, + # Common txn params + signer: TransactionSigner | None = None, + rekey_to: str | None = None, + note: bytes | None = None, + lease: bytes | None = None, + static_fee: AlgoAmount | None = None, + extra_fee: AlgoAmount | None = None, + max_fee: AlgoAmount | None = None, + validity_window: int | None = None, + first_valid_round: int | None = None, + last_valid_round: int | None = None, + ) -> EnsureFundedResponse | None: + amount_funded = self._get_ensure_funded_amount(account_fo_fund, min_spending_balance, min_funding_increment) + + if not amount_funded: + return None + + result = ( + self._get_composer() + .add_payment( + PaymentParams( + sender=dispenser_account, + receiver=account_fo_fund, + amount=amount_funded, + signer=signer, + rekey_to=rekey_to, + note=note, + lease=lease, + static_fee=static_fee, + extra_fee=extra_fee, + max_fee=max_fee, + validity_window=validity_window, + first_valid_round=first_valid_round, + last_valid_round=last_valid_round, + ) + ) + .send( + max_rounds_to_wait=max_rounds_to_wait, + suppress_log=suppress_log, + populate_app_call_resources=populate_app_call_resources, + ) + ) + + return EnsureFundedResponse( + returns=result.returns, + transactions=result.transactions, + confirmations=result.confirmations, + tx_ids=result.tx_ids, + group_id=result.group_id, + transaction_id=result.tx_ids[0], + confirmation=result.confirmations[0], + transaction=result.transactions[0], + amount_funded=amount_funded, + ) + + def _get_composer(self, get_suggested_params: Callable[[], SuggestedParams] | None = None) -> TransactionComposer: + if get_suggested_params is None: + + def _get_suggested_params() -> SuggestedParams: + return self._client_manager.algod.suggested_params() + + get_suggested_params = _get_suggested_params + + return TransactionComposer( + algod=self._client_manager.algod, get_signer=self.get_signer, get_suggested_params=get_suggested_params + ) + + def _calculate_fund_amount( + self, + min_spending_balance: int, + current_spending_balance: int, + min_funding_increment: int, + ) -> int | None: + if min_spending_balance > current_spending_balance: + min_fund_amount = min_spending_balance - current_spending_balance + return max(min_fund_amount, min_funding_increment) + return None + + def _get_ensure_funded_amount( + self, + sender: str, + min_spending_balance: AlgoAmount, + min_funding_increment: AlgoAmount | None = None, + ) -> AlgoAmount | None: + account_info = self.get_information(sender) + current_spending_balance = account_info["amount"] - account_info["min-balance"] + + min_increment = min_funding_increment.micro_algo if min_funding_increment else 0 + amount_funded = self._calculate_fund_amount( + min_spending_balance.micro_algo, current_spending_balance, min_increment + ) + + return AlgoAmount.from_micro_algo(amount_funded) if amount_funded is not None else None diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 38a2339..4cb40c9 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -127,14 +127,14 @@ def get_constant_block_offset(program: bytes) -> int: # noqa: C901 return max(bytecblock_offset or 0, intcblock_offset or 0) -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientCompilationParams: deploy_time_params: TealTemplateParams | None = None updatable: bool | None = None deletable: bool | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class ExposedLogicErrorDetails: is_clear_state_program: bool = False approval_source_map: SourceMap | None = None @@ -145,20 +145,14 @@ class ExposedLogicErrorDetails: @dataclass(kw_only=True, frozen=True) -class _AppClientParamsBase: - """Base parameters for creating an app client""" +class AppClientParams: + """Full parameters for creating an app client""" - app_id: int app_spec: ( Arc56Contract | ApplicationSpecification | str ) # Using string quotes since these types may be defined elsewhere algorand: AlgorandClientProtocol # Using string quotes since this type may be defined elsewhere - - -@dataclass(kw_only=True, frozen=True) -class AppClientParams(_AppClientParamsBase): - """Full parameters for creating an app client""" - + app_id: int app_name: str | None = None default_sender: str | bytes | None = None # Address can be string or bytes default_signer: TransactionSigner | None = None @@ -166,7 +160,7 @@ class AppClientParams(_AppClientParamsBase): clear_source_map: SourceMap | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientCompilationResult: approval_program: bytes clear_state_program: bytes @@ -174,7 +168,7 @@ class AppClientCompilationResult: compiled_clear: CompiledTeal | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class CommonTxnParams: sender: str signer: TransactionSigner | None = None @@ -189,7 +183,7 @@ class CommonTxnParams: last_valid_round: int | None = None -@dataclass(kw_only=True, frozen=True) +@dataclass(kw_only=True) class FundAppAccountParams: sender: str | None = None signer: TransactionSigner | None = None @@ -210,7 +204,7 @@ class FundAppAccountParams: on_complete: algosdk.transaction.OnComplete | None = None -@dataclass(kw_only=True, frozen=True) +@dataclass(kw_only=True) class AppClientCallParams: method: str | None = None # If calling ABI method, name or signature args: list | None = None # Arguments to pass to the method @@ -224,7 +218,7 @@ class AppClientCallParams: send_params: dict | None = None # Parameters to control transaction sending -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientMethodCallParams: method: str args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None = None @@ -246,69 +240,69 @@ class AppClientMethodCallParams: on_complete: algosdk.transaction.OnComplete | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithCompilationParams(AppClientMethodCallParams, AppClientCompilationParams): """Combined parameters for method calls with compilation""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithSendParams(AppClientMethodCallParams, SendParams): """Combined parameters for method calls with send options""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientMethodCallWithCompilationAndSendParams( AppClientMethodCallParams, AppClientCompilationParams, SendParams ): """Combined parameters for method calls with compilation and send options""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientBareCallParams: - signer: TransactionSigner | None - rekey_to: str | None - lease: bytes | None - static_fee: AlgoAmount | None - extra_fee: AlgoAmount | None - max_fee: AlgoAmount | None - validity_window: int | None - first_valid_round: int | None - last_valid_round: int | None - sender: str | None - note: bytes | None - args: list[bytes] | None - account_references: list[str] | None - app_references: list[int] | None - asset_references: list[int] | None - box_references: list[BoxReference | BoxIdentifier] | None - - -@dataclass(frozen=True, kw_only=True) + signer: TransactionSigner | None = None + rekey_to: str | None = None + lease: bytes | None = None + static_fee: AlgoAmount | None = None + extra_fee: AlgoAmount | None = None + max_fee: AlgoAmount | None = None + validity_window: int | None = None + first_valid_round: int | None = None + last_valid_round: int | None = None + sender: str | None = None + note: bytes | None = None + args: list[bytes] | None = None + account_references: list[str] | None = None + app_references: list[int] | None = None + asset_references: list[int] | None = None + box_references: list[BoxReference | BoxIdentifier] | None = None + + +@dataclass(kw_only=True, frozen=True) class CallOnComplete: on_complete: algosdk.transaction.OnComplete -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCompilationParams(AppClientBareCallParams, AppClientCompilationParams): """Combined parameters for bare calls with compilation""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientBareCallWithSendParams(AppClientBareCallParams, SendParams): """Combined parameters for bare calls with send options""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCompilationAndSendParams(AppClientBareCallParams, AppClientCompilationParams, SendParams): """Combined parameters for bare calls with compilation and send options""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppClientBareCallWithCallOnCompleteParams(AppClientBareCallParams, CallOnComplete): """Combined parameters for bare calls with an OnComplete value""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class ResolveAppClientByNetwork: app_spec: Arc56Contract | ApplicationSpecification | str algorand: AlgorandClientProtocol @@ -319,7 +313,7 @@ class ResolveAppClientByNetwork: clear_source_map: SourceMap | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppSourceMaps: approval_source_map: SourceMap | None = None clear_source_map: SourceMap | None = None @@ -785,7 +779,6 @@ def call(self, params: AppClientMethodCallWithSendParams) -> SendAppTransactionR ) return SendAppTransactionResult( - tx_id=simulate_response.tx_ids[-1], tx_ids=simulate_response.tx_ids, transactions=simulate_response.transactions, transaction=simulate_response.transactions[-1], @@ -921,6 +914,12 @@ def compile( updatable: bool | None = None, deletable: bool | None = None, ) -> AppClientCompilationResult: + def is_base64(s: str) -> bool: + try: + return base64.b64encode(base64.b64decode(s)).decode() == s + except Exception: + return False + if not app_spec.source: if not app_spec.byte_code or not app_spec.byte_code.get("approval") or not app_spec.byte_code.get("clear"): raise ValueError(f"Attempt to compile app {app_spec.name} without source or byte_code") @@ -930,19 +929,24 @@ def compile( clear_state_program=base64.b64decode(app_spec.byte_code.get("clear", "")), ) - approval_template: str = base64.b64decode(app_spec.source.get("approval", "")).decode("utf-8") # type: ignore[assignment] - deployment_metadata = ( - {"updatable": updatable or False, "deletable": deletable or False} - if updatable is not None or deletable is not None - else None + approval_source = app_spec.source.get("approval", "") + approval_template: str = ( + base64.b64decode(approval_source).decode("utf-8") if is_base64(approval_source) else approval_source ) compiled_approval = app_manager.compile_teal_template( approval_template, template_params=deploy_time_params, - deployment_metadata=deployment_metadata, + deployment_metadata=( + {"updatable": updatable or False, "deletable": deletable or False} + if updatable is not None or deletable is not None + else None + ), ) - clear_template: str = base64.b64decode(app_spec.source.get("clear", "")).decode("utf-8") # type: ignore[assignment] + clear_source = app_spec.source.get("clear", "") + clear_template: str = ( + base64.b64decode(clear_source).decode("utf-8") if is_base64(clear_source) else clear_source + ) compiled_clear = app_manager.compile_teal_template( clear_template, template_params=deploy_time_params, diff --git a/src/algokit_utils/applications/app_deployer.py b/src/algokit_utils/applications/app_deployer.py index 8b18714..6e75ad5 100644 --- a/src/algokit_utils/applications/app_deployer.py +++ b/src/algokit_utils/applications/app_deployer.py @@ -3,10 +3,12 @@ import json import logging from dataclasses import dataclass -from typing import Any, Literal +from typing import Literal +import algosdk from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.logic import get_application_address +from algosdk.transaction import OnComplete, Transaction from algosdk.v2client.indexer import IndexerClient from algokit_utils._legacy_v2.deploy import ( @@ -15,8 +17,9 @@ AppMetaData, OnSchemaBreak, OnUpdate, + OperationPerformed, ) -from algokit_utils.applications.app_manager import AppManager +from algokit_utils.applications.app_manager import AppManager, TealTemplateParams from algokit_utils.models.abi import ABIValue from algokit_utils.transactions.transaction_composer import ( AppCreateMethodCall, @@ -27,7 +30,6 @@ ) from algokit_utils.transactions.transaction_sender import ( AlgorandClientTransactionSender, - SendAppTransactionResult, ) APP_DEPLOY_NOTE_DAPP = "algokit_deployer" @@ -35,11 +37,12 @@ logger = logging.getLogger(__name__) -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True) class DeployAppUpdateParams: """Parameters for an update transaction in app deployment""" sender: str + on_complete: OnComplete = OnComplete.UpdateApplicationOC signer: TransactionSigner | None = None args: list[bytes] | None = None note: bytes | None = None @@ -51,11 +54,12 @@ class DeployAppUpdateParams: foreign_assets: list[int] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True) class DeployAppDeleteParams: """Parameters for a delete transaction in app deployment""" sender: str + on_complete: OnComplete = OnComplete.DeleteApplicationOC signer: TransactionSigner | None = None note: bytes | None = None lease: bytes | None = None @@ -66,12 +70,12 @@ class DeployAppDeleteParams: foreign_assets: list[int] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True) class AppDeployParams: """Parameters for deploying an app""" metadata: AppDeployMetaData - deploy_time_params: dict[str, Any] | None = None + deploy_time_params: TealTemplateParams | None = None on_schema_break: Literal["replace", "fail", "append"] | OnSchemaBreak = OnSchemaBreak.Fail on_update: Literal["update", "replace", "fail", "append"] | OnUpdate = OnUpdate.Fail create_params: AppCreateParams | AppCreateMethodCall @@ -84,16 +88,36 @@ class AppDeployParams: suppress_log: bool = False -@dataclass(frozen=True) -class AppDeploymentResult: - operation_performed: Literal["create", "update", "replace", "nothing"] - app_id: int - app_address: str - transaction: transaction.Transaction | None = None - confirmation: dict[str, Any] | None = None +@dataclass(kw_only=True, frozen=True) +class ConfirmedTransactionResult: + transaction: algosdk.transaction.Transaction + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + + +@dataclass(kw_only=True, frozen=True) +class AppDeployResult: + operation_performed: OperationPerformed + + # Common fields from AppMetadata + name: str + version: str + created_round: int + updated_round: int + deleted: bool + created_metadata: dict + deletable: bool | None = None + updatable: bool | None = None + + app_id: int | None = None + app_address: str | None = None + transaction: Transaction | None = None + confirmation: algosdk.v2client.algod.AlgodResponseType | None = None + compiled_approval: dict | None = None + compiled_clear: dict | None = None return_value: ABIValue | None = None delete_return: ABIValue | None = None - delete_result: dict[str, Any] | None = None + delete_result: ConfirmedTransactionResult | None = None class AppDeployer: @@ -118,7 +142,7 @@ def _create_deploy_note(self, metadata: AppDeployMetaData) -> bytes: } return json.dumps(note).encode() - def deploy(self, deployment: AppDeployParams) -> AppDeploymentResult | SendAppTransactionResult: + def deploy(self, deployment: AppDeployParams) -> AppDeployResult: # Create new instances with updated notes note = self._create_deploy_note(deployment.metadata) create_params = dataclasses.replace(deployment.create_params, note=note) @@ -217,8 +241,9 @@ def deploy(self, deployment: AppDeployParams) -> AppDeploymentResult | SendAppTr clear_program=clear_program, ) - return AppDeploymentResult( - operation_performed="nothing", + return AppDeployResult( + **existing_app.__dict__, + operation_performed=OperationPerformed.Nothing, app_id=existing_app.app_id, app_address=existing_app.app_address, ) @@ -228,44 +253,49 @@ def _create_app( deployment: AppDeployParams, approval_program: bytes, clear_program: bytes, - ) -> AppDeploymentResult: + ) -> AppDeployResult: """Create a new application""" if isinstance(deployment.create_params, AppCreateMethodCall): - create_params = AppCreateMethodCall( - **{ - **deployment.create_params.__dict__, - "approval_program": approval_program, - "clear_state_program": clear_program, - } + result = self._transaction_sender.app_create_method_call( + AppCreateMethodCall( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) ) - result = self._transaction_sender.app_create_method_call(create_params) else: - create_params = AppCreateParams( - **{ - **deployment.create_params.__dict__, - "approval_program": approval_program, - "clear_state_program": clear_program, - } + result = self._transaction_sender.app_create( + AppCreateParams( + **{ + **deployment.create_params.__dict__, + "approval_program": approval_program, + "clear_state_program": clear_program, + } + ) ) - result = self._transaction_sender.app_create(create_params) app_metadata = AppMetaData( app_id=result.app_id, app_address=get_application_address(result.app_id), **deployment.metadata.__dict__, created_metadata=deployment.metadata, - created_round=result.confirmation["confirmed-round"], - updated_round=result.confirmation["confirmed-round"], + created_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, + updated_round=result.confirmation.get("confirmed-round", 0) if isinstance(result.confirmation, dict) else 0, deleted=False, ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeploymentResult( - operation_performed="create", - app_id=result.app_id, - app_address=get_application_address(result.app_id), + app_metadata_dict = app_metadata.__dict__ + app_metadata_dict["operation_performed"] = OperationPerformed.Create + app_metadata_dict["app_id"] = result.app_id + app_metadata_dict["app_address"] = get_application_address(result.app_id) + + return AppDeployResult( + **app_metadata_dict, transaction=result.transaction, confirmation=result.confirmation, return_value=result.return_value, @@ -277,7 +307,7 @@ def _handle_schema_break( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeploymentResult: + ) -> AppDeployResult: if deployment.on_schema_break in (OnSchemaBreak.Fail, "fail"): raise ValueError( "Schema break detected and onSchemaBreak=OnSchemaBreak.Fail, stopping deployment. " @@ -299,7 +329,7 @@ def _handle_update( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeploymentResult: + ) -> AppDeployResult: if deployment.on_update in (OnUpdate.Fail, "fail"): raise ValueError( "Update detected and onUpdate=Fail, stopping deployment. " "Try a different onUpdate value to not fail." @@ -328,7 +358,7 @@ def _create_and_delete_app( existing_app: AppMetaData, approval_program: bytes, clear_program: bytes, - ) -> AppDeploymentResult: + ) -> AppDeployResult: composer = self._transaction_sender.new_group() # Add create transaction @@ -383,7 +413,7 @@ def _create_and_delete_app( ) self._update_app_lookup(deployment.create_params.sender, app_metadata) - return AppDeploymentResult( + return AppDeployResult( operation_performed="replace", app_id=app_id, app_address=get_application_address(app_id), diff --git a/src/algokit_utils/applications/app_factory.py b/src/algokit_utils/applications/app_factory.py index 676b34f..a522656 100644 --- a/src/algokit_utils/applications/app_factory.py +++ b/src/algokit_utils/applications/app_factory.py @@ -1,57 +1,61 @@ import base64 from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast +from typing import Any, TypeGuard, TypeVar +import algosdk from algosdk import transaction from algosdk.abi import Method from algosdk.atomic_transaction_composer import TransactionSigner +from algosdk.source_map import SourceMap +from algosdk.transaction import OnComplete, Transaction -from algokit_utils._legacy_v2.deploy import ( - AppDeployMetaData, - AppLookup, - OnSchemaBreak, - OnUpdate, -) +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils._legacy_v2.deploy import AppDeployMetaData, AppLookup, OnSchemaBreak, OnUpdate, OperationPerformed from algokit_utils.applications.app_client import ( AppClient, AppClientBareCallParams, AppClientCompilationParams, + AppClientCompilationResult, AppClientMethodCallParams, AppClientParams, + AppSourceMaps, ExposedLogicErrorDetails, ) +from algokit_utils.applications.app_deployer import AppDeployParams, DeployAppDeleteParams, DeployAppUpdateParams from algokit_utils.applications.app_manager import TealTemplateParams from algokit_utils.applications.utils import ( get_abi_decoded_value, - get_abi_struct_from_abi_tuple, get_abi_tuple_from_abi_struct, get_arc56_method, + get_arc56_return_value, +) +from algokit_utils.models.application import ( + DELETABLE_TEMPLATE_NAME, + UPDATABLE_TEMPLATE_NAME, + Arc56Contract, + Arc56Method, + CompiledTeal, + MethodArg, ) -from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME, Arc56Contract +from algokit_utils.models.transaction import SendParams from algokit_utils.protocols.application import AlgorandClientProtocol -from algokit_utils.transactions.transaction_composer import AppCreateParams +from algokit_utils.transactions.transaction_composer import ( + AppCreateMethodCall, + AppCreateParams, + AppDeleteMethodCall, + AppUpdateMethodCall, + BuiltTransactions, +) from algokit_utils.transactions.transaction_sender import SendAppTransactionResult -if TYPE_CHECKING: - from algosdk.source_map import SourceMap - - T = TypeVar("T") -class ParamsMethodsProtocol(Protocol): - def create(self, params: AppClientMethodCallParams) -> dict[str, Any]: ... - def deploy_update(self, params: AppClientMethodCallParams) -> dict[str, Any]: ... - def deploy_delete(self, params: AppClientMethodCallParams) -> dict[str, Any]: ... - - bare: dict[str, Callable[[AppClientBareCallParams | None], dict[str, Any]]] - - -@dataclass(kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppFactoryParams: - app_spec: Arc56Contract | str algorand: AlgorandClientProtocol + app_spec: Arc56Contract | ApplicationSpecification | str app_name: str | None = None default_sender: str | bytes | None = None default_signer: TransactionSigner | None = None @@ -68,6 +72,11 @@ class AppFactoryCreateParams(AppClientBareCallParams, AppClientCompilationParams extra_program_pages: int | None = None +@dataclass(kw_only=True, frozen=True) +class AppFactoryCreateWithSendParams(AppFactoryCreateParams, SendParams): + pass + + @dataclass(kw_only=True, frozen=True) class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompilationParams): on_complete: transaction.OnComplete | None = None @@ -76,73 +85,189 @@ class AppFactoryCreateMethodCallParams(AppClientMethodCallParams, AppClientCompi @dataclass(kw_only=True, frozen=True) -class AppFactoryDeployParams: - version: str | None = None - signer: TransactionSigner | None = None - sender: str | None = None - allow_update: bool | None = None - allow_delete: bool | None = None - on_update: OnUpdate = OnUpdate.Fail - on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail - template_values: TealTemplateParams | None = None - create_args: AppClientMethodCallParams | AppClientBareCallParams | None = None - update_args: AppClientMethodCallParams | AppClientBareCallParams | None = None - delete_args: AppClientMethodCallParams | AppClientBareCallParams | None = None - existing_deployments: AppLookup | None = None - ignore_cache: bool = False - updatable: bool | None = None - deletable: bool | None = None - app_name: str | None = None +class AppFactoryCreateMethodCallWithSendParams(AppFactoryCreateMethodCallParams, SendParams): + pass -class AppFactory: - def __init__(self, params: AppFactoryParams) -> None: - self._app_spec = AppClient.normalise_app_spec(params.app_spec) - self._app_name = params.app_name or self._app_spec.name - self._algorand = params.algorand - self._version = params.version or "1.0" - self._default_sender = params.default_sender - self._default_signer = params.default_signer - self._deploy_time_params = params.deploy_time_params - self._updatable = params.updatable - self._deletable = params.deletable - self._approval_source_map: SourceMap | None = None - self._clear_source_map: SourceMap | None = None +@dataclass(frozen=True, kw_only=True) +class AppFactoryCreateResult(SendAppTransactionResult): + """Result from creating an application via AppFactory""" - @property - def app_name(self) -> str: - return self._app_name + app_id: int + """The ID of the created application""" + app_address: str + """The address of the created application""" + compiled_approval: CompiledTeal | None = None + """The compiled approval program if source was provided""" + compiled_clear: CompiledTeal | None = None + """The compiled clear program if source was provided""" - @property - def app_spec(self) -> Arc56Contract: - return self._app_spec + +@dataclass(kw_only=True, frozen=True) +class AppFactoryDeployResult: + """Represents the result object from app deployment""" + + app_address: str + app_id: int + approval_program: bytes # Uint8Array + clear_state_program: bytes # Uint8Array + compiled_approval: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + compiled_clear: dict # Contains teal, compiled, compiledHash, compiledBase64ToBytes, sourceMap + confirmation: algosdk.v2client.algod.AlgodResponseType + confirmations: list[algosdk.v2client.algod.AlgodResponseType] | None = None + created_metadata: dict # {name: str, version: str, updatable: bool, deletable: bool} + created_round: int + deletable: bool + deleted: bool + delete_return: Any | None = None + group_id: str | None = None + name: str + operation_performed: OperationPerformed + return_value: Any | None = None + returns: list[Any] | None = None + transaction: Transaction + transactions: list[Transaction] + tx_id: str + tx_ids: list[str] + updatable: bool + updated_round: int + version: str + + +class _AppFactoryBareParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateParams | None = None) -> AppCreateParams: + create_args = {} + if params: + create_args = {**params.__dict__} + del create_args["schema"] + del create_args["sender"] + del create_args["on_complete"] + del create_args["deploy_time_params"] + del create_args["updatable"] + del create_args["deletable"] + compiled = self._factory.compile(params) + create_args["approval_program"] = compiled.approval_program + create_args["clear_state_program"] = compiled.clear_state_program + + return AppCreateParams( + **create_args, + schema=(params.schema if params else None) + or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + }, + sender=self._factory._get_sender(params.sender if params else None), + on_complete=(params.on_complete if params else None) or OnComplete.NoOpOC, + ) + + def deploy_update(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.UpdateApplicationOC, + } + + def deploy_delete(self, params: AppClientBareCallParams | None = None) -> dict[str, Any]: + return { + **(params.__dict__ if params else {}), + "sender": self._factory._get_sender(params.sender if params else None), + "on_complete": OnComplete.DeleteApplicationOC, + } + + +class _AppFactoryParamsAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareParamsAccessor(factory) @property - def algorand(self) -> AlgorandClientProtocol: - return self._algorand + def bare(self) -> _AppFactoryBareParamsAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> AppCreateMethodCall: + compiled = self._factory.compile(params) + params_dict = params.__dict__ + params_dict["schema"] = params.schema or { + "global_bytes": self._factory._app_spec.state.schemas["global"]["bytes"], + "global_ints": self._factory._app_spec.state.schemas["global"]["ints"], + "local_bytes": self._factory._app_spec.state.schemas["local"]["bytes"], + "local_ints": self._factory._app_spec.state.schemas["local"]["ints"], + } + params_dict["sender"] = self._factory._get_sender(params.sender) + params_dict["method"] = get_arc56_method(params.method, self._factory._app_spec) + params_dict["args"] = self._factory._get_create_abi_args_with_default_values(params.method, params.args) + params_dict["on_complete"] = params.on_complete or OnComplete.NoOpOC + del params_dict["deploy_time_params"] + del params_dict["updatable"] + del params_dict["deletable"] + return AppCreateMethodCall( + **params_dict, + app_id=0, + approval_program=compiled.approval_program, + clear_state_program=compiled.clear_state_program, + ) - def get_app_client_by_id(self, params: AppClientParams) -> AppClient: - return AppClient( - AppClientParams( - app_id=params.app_id, - algorand=self._algorand, - app_spec=self._app_spec, - app_name=params.app_name or self._app_name, - default_sender=params.default_sender or self._default_sender, - default_signer=params.default_signer or self._default_signer, - approval_source_map=params.approval_source_map or self._approval_source_map, - clear_source_map=params.clear_source_map or self._clear_source_map, - ) + def deploy_update(self, params: AppClientMethodCallParams) -> AppUpdateMethodCall: + return AppUpdateMethodCall( + **params.__dict__, + sender=self._factory._get_sender(params.sender), + method=get_arc56_method(params.method, self._factory._app_spec), + args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), + on_complete=OnComplete.UpdateApplicationOC, ) - def create_bare(self, params: AppFactoryCreateParams | None = None) -> tuple[AppClient, SendAppTransactionResult]: - updatable = params.updatable if params and params.updatable is not None else self._updatable - deletable = params.deletable if params and params.deletable is not None else self._deletable + def deploy_delete(self, params: AppClientMethodCallParams) -> AppDeleteMethodCall: + return AppDeleteMethodCall( + **params.__dict__, + sender=self._factory._get_sender(params.sender), + method=get_arc56_method(params.method, self._factory._app_spec), + args=self._factory._get_create_abi_args_with_default_values(params.method, params.args), + on_complete=OnComplete.DeleteApplicationOC, + ) + + +class _AppFactoryBareCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + + def create(self, params: AppFactoryCreateParams | None = None) -> Transaction: + return self._factory._algorand.create_transaction.app_create(self._factory.params.bare.create(params)) + + +class _AppFactoryCreateTransactionAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._bare = _AppFactoryBareCreateTransactionAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareCreateTransactionAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> BuiltTransactions: + return self._factory._algorand.create_transaction.app_create_method_call(self._factory.params.create(params)) + + +class _AppFactoryBareSendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + + def create(self, params: AppFactoryCreateWithSendParams | None = None) -> tuple[AppClient, AppFactoryCreateResult]: + updatable = params.updatable if params and params.updatable is not None else self._factory._updatable + deletable = params.deletable if params and params.deletable is not None else self._factory._deletable deploy_time_params = ( - params.deploy_time_params if params and params.deploy_time_params is not None else self._deploy_time_params + params.deploy_time_params + if params and params.deploy_time_params is not None + else self._factory._deploy_time_params ) - compiled = self.compile( + compiled = self._factory.compile( AppClientCompilationParams( deploy_time_params=deploy_time_params, updatable=updatable, @@ -150,35 +275,52 @@ def create_bare(self, params: AppFactoryCreateParams | None = None) -> tuple[App ) ) - result = self._handle_call_errors( + create_args = {} + if params: + create_args = {**params.__dict__} + del create_args["max_rounds_to_wait"] + del create_args["suppress_log"] + del create_args["populate_app_call_resources"] + + create_args["updatable"] = updatable + create_args["deletable"] = deletable + create_args["deploy_time_params"] = deploy_time_params + + result = self._factory._handle_call_errors( lambda: self._algorand.send.app_create( - self._get_bare_params( - params=AppCreateParams( - **(params.__dict__ if params else {}), - updatable=updatable, - deletable=deletable, - deploy_time_params=deploy_time_params, - ), - on_complete=params.on_complete if params else transaction.OnComplete.NoOpOC, - ) + self._factory.params.bare.create(AppFactoryCreateParams(**create_args)) ) - ) + ).__dict__ + + result["compiled_approval"] = compiled.compiled_approval + result["compiled_clear"] = compiled.compiled_clear return ( - self.get_app_client_by_id( - AppClientParams(app_id=result.app_id, app_spec=self._app_spec, algorand=self._algorand) + self._factory.get_app_client_by_id( + app_id=result["app_id"], ), - SendAppTransactionResult(**{**result.__dict__, **(compiled.__dict__ if compiled else {})}), + AppFactoryCreateResult(**result), ) - def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, SendAppTransactionResult]: - updatable = params.updatable if params.updatable is not None else self._updatable - deletable = params.deletable if params.deletable is not None else self._deletable + +class _AppFactorySendAccessor: + def __init__(self, factory: "AppFactory") -> None: + self._factory = factory + self._algorand = factory._algorand + self._bare = _AppFactoryBareSendAccessor(factory) + + @property + def bare(self) -> _AppFactoryBareSendAccessor: + return self._bare + + def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, AppFactoryDeployResult]: + updatable = params.updatable if params.updatable is not None else self._factory._updatable + deletable = params.deletable if params.deletable is not None else self._factory._deletable deploy_time_params = ( - params.deploy_time_params if params.deploy_time_params is not None else self._deploy_time_params + params.deploy_time_params if params.deploy_time_params is not None else self._factory._deploy_time_params ) - compiled = self.compile( + compiled = self._factory.compile( AppClientCompilationParams( deploy_time_params=deploy_time_params, updatable=updatable, @@ -186,34 +328,93 @@ def create(self, params: AppFactoryCreateMethodCallParams) -> tuple[AppClient, S ) ) - result = self._handle_call_errors( - lambda: self._get_arc56_return_value( - self._algorand.send.app_create_method_call( - self._get_abi_params( - { - **params.__dict__, - "updatable": updatable, - "deletable": deletable, - "deploy_time_params": deploy_time_params, - }, - params.on_complete or transaction.OnComplete.NoOpOC, + result = self._factory._handle_call_errors( + lambda: self._algorand.send.app_create_method_call( + self._factory.params.create( + AppFactoryCreateMethodCallParams( + **params.__dict__, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, ) - ), - get_arc56_method(params.method, self._app_spec), + ) ) ) return ( - self.get_app_client_by_id( - AppClientParams(app_id=result.app_id, app_spec=self._app_spec, algorand=self._algorand) + self._factory.get_app_client_by_id( + app_id=result.app_id, ), - SendAppTransactionResult(**{**result.__dict__, **(compiled.__dict__ if compiled else {})}), + AppFactoryDeployResult(**{**result.__dict__, **(compiled.__dict__ if compiled else {})}), ) - def deploy(self, params: AppFactoryDeployParams) -> tuple[AppClient, SendAppTransactionResult]: - updatable = params.updatable if params.updatable is not None else self._updatable - deletable = params.deletable if params.deletable is not None else self._deletable - deploy_time_params = params.template_values + +class AppFactory: + def __init__(self, params: AppFactoryParams) -> None: + self._app_spec = AppClient.normalise_app_spec(params.app_spec) + self._app_name = params.app_name or self._app_spec.name + self._algorand = params.algorand + self._version = params.version or "1.0" + self._default_sender = params.default_sender + self._default_signer = params.default_signer + self._deploy_time_params = params.deploy_time_params + self._updatable = params.updatable + self._deletable = params.deletable + self._approval_source_map: SourceMap | None = None + self._clear_source_map: SourceMap | None = None + self._params_accessor = _AppFactoryParamsAccessor(self) + self._send_accessor = _AppFactorySendAccessor(self) + self._create_transaction_accessor = _AppFactoryCreateTransactionAccessor(self) + + @property + def app_name(self) -> str: + return self._app_name + + @property + def app_spec(self) -> Arc56Contract: + return self._app_spec + + @property + def algorand(self) -> AlgorandClientProtocol: + return self._algorand + + @property + def params(self) -> _AppFactoryParamsAccessor: + return self._params_accessor + + @property + def send(self) -> _AppFactorySendAccessor: + return self._send_accessor + + @property + def create_transaction(self) -> _AppFactoryCreateTransactionAccessor: + return self._create_transaction_accessor + + def deploy( + self, + *, + deploy_time_params: TealTemplateParams | None = None, + on_update: OnUpdate = OnUpdate.Fail, + on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + create_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + update_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + delete_params: AppClientMethodCallParams | AppClientBareCallParams | None = None, + existing_deployments: AppLookup | None = None, + ignore_cache: bool = False, + updatable: bool | None = None, + deletable: bool | None = None, + app_name: str | None = None, + max_rounds_to_wait: int | None = None, + suppress_log: bool = False, + populate_app_call_resources: bool = False, + ) -> tuple[AppClient, AppFactoryDeployResult]: + updatable = ( + updatable if updatable is not None else self._updatable or self._get_deploy_time_control("updatable") + ) + deletable = ( + deletable if deletable is not None else self._deletable or self._get_deploy_time_control("deletable") + ) + deploy_time_params = deploy_time_params if deploy_time_params is not None else self._deploy_time_params compiled = self.compile( AppClientCompilationParams( @@ -223,102 +424,127 @@ def deploy(self, params: AppFactoryDeployParams) -> tuple[AppClient, SendAppTran ) ) - deploy_result = self._algorand.app_deployer.deploy( - { - **params.__dict__, - "create_params": ( - self._get_abi_params(params.create_args.__dict__, transaction.OnComplete.NoOpOC) - if params.create_args and hasattr(params.create_args, "method") - else self._get_bare_params( - params.create_args.__dict__ if params.create_args else {}, - transaction.OnComplete.NoOpOC, - ) + def _is_method_call_params( + params: AppClientMethodCallParams | AppClientBareCallParams | None, + ) -> TypeGuard[AppClientMethodCallParams]: + return params is not None and hasattr(params, "method") + + update_args: DeployAppUpdateParams | AppUpdateMethodCall + if _is_method_call_params(update_params): + update_args = self.params.deploy_update(update_params) # type: ignore[arg-type] + else: + update_args = DeployAppUpdateParams( + **self.params.bare.deploy_update( + update_params if isinstance(update_params, AppClientBareCallParams) else None + ) + ) + + delete_args: DeployAppDeleteParams | AppDeleteMethodCall + if _is_method_call_params(delete_params): + delete_args = self.params.deploy_delete(delete_params) # type: ignore[arg-type] + else: + delete_args = DeployAppDeleteParams( + **self.params.bare.deploy_delete( + delete_params if isinstance(delete_params, AppClientBareCallParams) else None ) - if params.create_args - else None, - "update_params": ( - self._get_abi_params(params.update_args.__dict__, transaction.OnComplete.UpdateApplicationOC) - if params.update_args and hasattr(params.update_args, "method") - else self._get_bare_params( - params.update_args.__dict__ if params.update_args else {}, - transaction.OnComplete.UpdateApplicationOC, + ) + + app_deploy_params = AppDeployParams( + deploy_time_params=deploy_time_params, + on_schema_break=on_schema_break, + on_update=on_update, + existing_deployments=existing_deployments, + ignore_cache=ignore_cache, + create_params=( + self.params.create( + AppFactoryCreateMethodCallParams( + **create_params.__dict__, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, ) ) - if params.update_args - else None, - "delete_params": ( - self._get_abi_params(params.delete_args.__dict__, transaction.OnComplete.DeleteApplicationOC) - if params.delete_args and hasattr(params.delete_args, "method") - else self._get_bare_params( - params.delete_args.__dict__ if params.delete_args else {}, - transaction.OnComplete.DeleteApplicationOC, + if create_params and hasattr(create_params, "method") + else self.params.bare.create( + AppFactoryCreateParams( + **create_params.__dict__ if create_params else {}, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, ) ) - if params.delete_args - else None, - "metadata": AppDeployMetaData( - name=params.app_name or self._app_name, - version=self._version, - updatable=updatable, - deletable=deletable, - ), - } + ), + update_params=update_args, + delete_params=delete_args, + metadata=AppDeployMetaData( + name=app_name or self._app_name, + version=self._version, + updatable=updatable, + deletable=deletable, + ), ) + deploy_result = self._algorand.app_deployer.deploy(app_deploy_params) app_client = self.get_app_client_by_id( - AppClientParams( - app_id=deploy_result.app_id, app_name=params.app_name, app_spec=self._app_spec, algorand=self._algorand - ) + app_id=deploy_result.app_id or 0, + app_name=app_name, + default_sender=self._default_sender, + default_signer=self._default_signer, ) result = {**deploy_result.__dict__, **(compiled.__dict__ if compiled else {})} - return_value = None - if hasattr(result, "return"): - if result["operationPerformed"] == "update": - if params.update_args and hasattr(params.update_args, "method"): - return_value = self._get_arc56_return_value( - result["return"], - get_arc56_method(params.update_args.method, self._app_spec), + if hasattr(result, "return_value"): + if result["operation_performed"] == "update": + if update_params and hasattr(update_params, "method"): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(update_params.method, self._app_spec), # type: ignore[arg-type] + self._app_spec.structs, ) - elif params.create_args and hasattr(params.create_args, "method"): - return_value = self._get_arc56_return_value( - result["return"], - get_arc56_method(params.create_args.method, self._app_spec), + elif create_params and hasattr(create_params, "method"): + result["return_value"] = get_arc56_return_value( + result["return_value"], + get_arc56_method(create_params.method, self._app_spec), # type: ignore[arg-type] + self._app_spec.structs, ) - delete_return = None - if hasattr(result, "deleteReturn") and params.delete_args and hasattr(params.delete_args, "method"): - delete_return = self._get_arc56_return_value( - result["deleteReturn"], - get_arc56_method(params.delete_args.method, self._app_spec), + if "delete_return" in result and delete_params and hasattr(delete_params, "method"): + result["delete_return"] = get_arc56_return_value( + result["delete_return"], + get_arc56_method(delete_params.method, self._app_spec), # type: ignore[arg-type] + self._app_spec.structs, ) - result["return"] = return_value - result["deleteReturn"] = delete_return - - return app_client, SendAppTransactionResult(**result) - - def compile(self, compilation: AppClientCompilationParams | None = None) -> Any: - result = AppClient.compile( - self._app_spec, - self._algorand.app, - cast(TealTemplateParams | None, compilation.deploy_time_params if compilation else None), + del result["delete_result"] + result["transactions"] = [] + result["tx_id"] = "" + result["tx_ids"] = [] + + return app_client, AppFactoryDeployResult(**result) + + def get_app_client_by_id( + self, + app_id: int, + app_name: str | None = None, + default_sender: str | bytes | None = None, # Address can be string or bytes + default_signer: TransactionSigner | None = None, + approval_source_map: SourceMap | None = None, + clear_source_map: SourceMap | None = None, + ) -> AppClient: + return AppClient( + AppClientParams( + app_id=app_id, + algorand=self._algorand, + app_spec=self._app_spec, + app_name=app_name or self._app_name, + default_sender=default_sender or self._default_sender, + default_signer=default_signer or self._default_signer, + approval_source_map=approval_source_map or self._approval_source_map, + clear_source_map=clear_source_map or self._clear_source_map, + ) ) - if result.compiled_approval: - self._approval_source_map = result.compiled_approval.source_map - if result.compiled_clear: - self._clear_source_map = result.compiled_clear.source_map - - return result - - def _handle_call_errors(self, call: Callable[[], T]) -> T: - try: - return call() - except Exception as e: - raise self.expose_logic_error(e) from None - def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) -> Exception: return AppClient.expose_logic_error_static( e, @@ -328,40 +554,53 @@ def expose_logic_error(self, e: Exception, is_clear_state_program: bool = False) approval_source_map=self._approval_source_map, clear_source_map=self._clear_source_map, program=None, - approval_source_info=None, - clear_source_info=None, + approval_source_info=( + self._app_spec.source_info.get("approval") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), + clear_source_info=( + self._app_spec.source_info.get("clear") + if self._app_spec.source_info and hasattr(self._app_spec, "source_info") + else None + ), ), ) - def _get_arc56_return_value(self, return_value: Any, method: Method) -> Any: - if method.returns.type == "void" or return_value is None: - return None - - if hasattr(return_value, "decode_error"): - raise ValueError(return_value["decode_error"]) + def export_source_maps(self) -> AppSourceMaps: + if not self._approval_source_map or not self._clear_source_map: + raise ValueError( + "Unable to export source maps; they haven't been loaded into this client - " + "you need to call create, update, or deploy first" + ) + return AppSourceMaps( + approval_source_map=self._approval_source_map, + clear_source_map=self._clear_source_map, + ) - raw_value = return_value.get("raw_return_value") + def import_source_maps(self, source_maps: AppSourceMaps) -> None: + self._approval_source_map = source_maps.approval_source_map + self._clear_source_map = source_maps.clear_source_map - if method.returns.type == "AVMBytes": - return raw_value - if method.returns.type == "AVMString" and raw_value: - return raw_value.decode("utf-8") - if method.returns.type == "AVMUint64" and raw_value: - return get_abi_decoded_value(raw_value, "uint64", self._app_spec.structs) + def compile(self, compilation: AppClientCompilationParams | None = None) -> AppClientCompilationResult: + result = AppClient.compile( + self._app_spec, + self._algorand.app, + deploy_time_params=compilation.deploy_time_params if compilation else None, + updatable=compilation.updatable if compilation else None, + deletable=compilation.deletable if compilation else None, + ) - if method.returns.struct and method.returns.struct in self._app_spec.structs: - return_tuple = return_value.get("return_value") - return get_abi_struct_from_abi_tuple( - return_tuple, self._app_spec.structs[method.returns.struct], self._app_spec.structs - ) + if result.compiled_approval: + self._approval_source_map = result.compiled_approval.source_map + if result.compiled_clear: + self._clear_source_map = result.compiled_clear.source_map - return return_value.get("return_value") + return result def _get_deploy_time_control(self, control: str) -> bool | None: approval = ( - base64.b64decode(self._app_spec.source["approval"]).decode("utf-8") - if self._app_spec.source and "approval" in self._app_spec.source - else None + self._app_spec.source["approval"] if self._app_spec.source and "approval" in self._app_spec.source else None ) template_name = UPDATABLE_TEMPLATE_NAME if control == "updatable" else DELETABLE_TEMPLATE_NAME @@ -370,79 +609,9 @@ def _get_deploy_time_control(self, control: str) -> bool | None: on_complete = "UpdateApplication" if control == "updatable" else "DeleteApplication" return on_complete in self._app_spec.bare_actions.get("call", []) or any( - m.actions.call and on_complete in m.actions.call for m in self._app_spec.methods + on_complete in m.actions.call for m in self._app_spec.methods if m.actions and m.actions.call ) - @property - def params(self) -> ParamsMethodsProtocol: - return cast(ParamsMethodsProtocol, self._get_params_methods()) - - def _get_params_methods(self) -> dict[str, Any]: - return { - "create": lambda params: self._get_abi_params( - { - **params.__dict__, - "deploy_time_params": params.deploy_time_params or self._deploy_time_params, - "schema": params.schema - or { - "global_bytes": self._app_spec.state.schemas["global"]["bytes"], - "global_ints": self._app_spec.state.schemas["global"]["ints"], - "local_bytes": self._app_spec.state.schemas["local"]["bytes"], - "local_ints": self._app_spec.state.schemas["local"]["ints"], - }, - "approval_program": self.compile(params).approval_program, - "clear_state_program": self.compile(params).clear_state_program, - }, - params.on_complete or transaction.OnComplete.NoOpOC, - ), - "deploy_update": lambda params: self._get_abi_params( - params.__dict__, transaction.OnComplete.UpdateApplicationOC - ), - "deploy_delete": lambda params: self._get_abi_params( - params.__dict__, transaction.OnComplete.DeleteApplicationOC - ), - "bare": { - "create": lambda params: self._get_bare_params( - { - **(params.__dict__ if params else {}), - "deploy_time_params": (params.deploy_time_params if params else None) - or self._deploy_time_params, - "schema": (params.schema if params else None) - or { - "global_bytes": self._app_spec.state.schemas["global"]["bytes"], - "global_ints": self._app_spec.state.schemas["global"]["ints"], - "local_bytes": self._app_spec.state.schemas["local"]["bytes"], - "local_ints": self._app_spec.state.schemas["local"]["ints"], - }, - **(self.compile(params).__dict__ if params else {}), - }, - (params.on_complete if params else None) or transaction.OnComplete.NoOpOC, - ), - "deploy_update": lambda params: self._get_bare_params( - params.__dict__ if params else {}, transaction.OnComplete.UpdateApplicationOC - ), - "deploy_delete": lambda params: self._get_bare_params( - params.__dict__ if params else {}, transaction.OnComplete.DeleteApplicationOC - ), - }, - } - - def _get_bare_params(self, params: dict[str, Any], on_complete: transaction.OnComplete) -> dict[str, Any]: - return { - **params, - "sender": self._get_sender(params.get("sender")), - "on_complete": on_complete, - } - - def _get_abi_params(self, params: dict[str, Any], on_complete: transaction.OnComplete) -> dict[str, Any]: - return { - **params, - "sender": self._get_sender(params.get("sender")), - "method": get_arc56_method(params["method"], self._app_spec), - "args": self._get_create_abi_args_with_default_values(params["method"], params.get("args")), - "on_complete": on_complete, - } - def _get_sender(self, sender: str | bytes | None) -> str: if not sender and not self._default_sender: raise Exception( @@ -450,35 +619,66 @@ def _get_sender(self, sender: str | bytes | None) -> str: ) return str(sender or self._default_sender) + def _handle_call_errors(self, call: Callable[[], T]) -> T: + try: + return call() + except Exception as e: + raise self.expose_logic_error(e) from None + + def _parse_method_call_return(self, result: SendAppTransactionResult, method: Method) -> SendAppTransactionResult: + return_value = result.return_value + if isinstance(return_value, dict): + return_value = get_arc56_return_value(return_value, method, self._app_spec.structs) + return SendAppTransactionResult( + **{ + **result.__dict__, + "return_value": return_value, + } + ) + def _get_create_abi_args_with_default_values( - self, method_name_or_signature: str, args: list[Any] | None + self, + method_name_or_signature: str | Arc56Method, + args: list[Any] | None, ) -> list[Any]: - method = get_arc56_method(method_name_or_signature, self._app_spec) + method = ( + get_arc56_method(method_name_or_signature, self._app_spec) + if isinstance(method_name_or_signature, str) + else method_name_or_signature + ) result = [] + def _has_struct(arg: Any) -> TypeGuard[MethodArg]: # noqa: ANN401 + return hasattr(arg, "struct") + for i, method_arg in enumerate(method.args): + arg = method_arg arg_value = args[i] if args and i < len(args) else None if arg_value is not None: - if hasattr(method_arg, "struct") and method_arg.struct and isinstance(arg_value, dict): + if _has_struct(arg) and arg.struct and isinstance(arg_value, dict): arg_value = get_abi_tuple_from_abi_struct( - arg_value, self._app_spec.structs[method_arg.struct], self._app_spec.structs + arg_value, + self._app_spec.structs[arg.struct], + self._app_spec.structs, ) result.append(arg_value) continue - if hasattr(method_arg, "default_value") and method_arg.default_value: - if method_arg.default_value.source == "literal": - value_raw = base64.b64decode(method_arg.default_value.data) - value_type = method_arg.default_value.type or str(method_arg.type) + default_value = getattr(arg, "default_value", None) + if default_value: + if default_value.source == "literal": + value_raw = base64.b64decode(default_value.data) + value_type = default_value.type or str(arg.type) result.append(get_abi_decoded_value(value_raw, value_type, self._app_spec.structs)) else: raise ValueError( - f"Can't provide default value for {method_arg.default_value.source} for a contract creation call" + f"Can't provide default value for {default_value.source} for a contract creation call" ) else: raise ValueError( - f"No value provided for required argument {method_arg.name or f'arg{i+1}'} in call to method {method.name}" + f"No value provided for required argument " + f"{arg.name or f'arg{i+1}'} in call to method {method.name}" ) return result diff --git a/src/algokit_utils/applications/app_manager.py b/src/algokit_utils/applications/app_manager.py index 98ed28c..09f6028 100644 --- a/src/algokit_utils/applications/app_manager.py +++ b/src/algokit_utils/applications/app_manager.py @@ -22,20 +22,20 @@ ) -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class BoxName: name: str name_raw: bytes name_base64: str -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class BoxValue: name: BoxName value: bytes -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class BoxABIValue: name: BoxName value: ABIValue @@ -130,7 +130,7 @@ def _find_unquoted_string(line: str, token: str, start: int = 0, end: int = -1) def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]: result: list[str] = [] match_count = 0 - token = f"TMPL_{template_variable}" + token = f"TMPL_{template_variable}" if not template_variable.startswith("TMPL_") else template_variable token_idx_offset = len(value) - len(token) for line in program_lines: comment_idx = _find_unquoted_string(line, "//") diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 8695614..1818471 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -14,7 +14,7 @@ ) -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AccountAssetInformation: """Information about an account's holding of a particular asset.""" @@ -28,7 +28,7 @@ class AccountAssetInformation: """The round this information was retrieved at.""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetInformation: """Information about an asset.""" @@ -66,7 +66,7 @@ class AssetInformation: """32-byte hash of some metadata that is relevant to the asset and/or asset holders.""" -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class BulkAssetOptInOutResult: """Individual result from performing a bulk opt-in or bulk opt-out for an account against a series of assets.""" diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index b55ff01..43ab921 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -1,12 +1,17 @@ from dataclasses import dataclass import algosdk +from algosdk.atomic_transaction_composer import TransactionSigner from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient # from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils._legacy_v2.application_specification import ApplicationSpecification +from algokit_utils.applications.app_factory import AppFactory, AppFactoryParams +from algokit_utils.applications.app_manager import TealTemplateParams from algokit_utils.clients.dispenser_api_client import TestNetDispenserApiClient +from algokit_utils.models.application import Arc56Contract from algokit_utils.network_clients import ( AlgoClientConfigs, get_algod_client, @@ -99,30 +104,33 @@ def get_testnet_dispenser( return TestNetDispenserApiClient(auth_token=auth_token) - # def get_app_factory( - # self, - # app_spec: Arc56Contract | ApplicationSpecification | str, - # app_name: str | None = None, - # default_sender: str | None = None, - # default_signer: TransactionSigner | None = None, - # version: str | None = None, - # updatable: bool | None = None, - # deletable: bool | None = None, - # deploy_time_params: TealTemplateParams | None = None, - # ) -> AppFactory: - # return AppFactory( - # AppFactoryParams( - # algorand=self._algorand, - # app_spec=app_spec, - # app_name=app_name, - # default_sender=default_sender, - # default_signer=default_signer, - # version=version, - # updatable=updatable, - # deletable=deletable, - # deploy_time_params=deploy_time_params, - # ) - # ) + def get_app_factory( + self, + app_spec: Arc56Contract | ApplicationSpecification | str, + app_name: str | None = None, + default_sender: str | None = None, + default_signer: TransactionSigner | None = None, + version: str | None = None, + updatable: bool | None = None, + deletable: bool | None = None, + deploy_time_params: TealTemplateParams | None = None, + ) -> AppFactory: + if not self._algorand: + raise ValueError("Attempt to get app factory from a ClientManager without an Algorand client") + + return AppFactory( + AppFactoryParams( + algorand=self._algorand, + app_spec=app_spec, + app_name=app_name, + default_sender=default_sender, + default_signer=default_signer, + version=version, + updatable=updatable, + deletable=deletable, + deploy_time_params=deploy_time_params, + ) + ) @staticmethod def genesis_id_is_local_net(genesis_id: str) -> bool: diff --git a/src/algokit_utils/models/application.py b/src/algokit_utils/models/application.py index 06c229b..c04312c 100644 --- a/src/algokit_utils/models/application.py +++ b/src/algokit_utils/models/application.py @@ -108,7 +108,7 @@ class Recommendations: assets: list[int] | None = None -@dataclass(kw_only=True, frozen=True) +@dataclass(kw_only=True) class Method: name: str desc: str | None = None @@ -211,7 +211,7 @@ class Arc56Contract: scratch_variables: dict[str, dict[str, int | ABITypeAlias | AVMType | StructName]] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppState: key_raw: bytes key_base64: str @@ -220,7 +220,7 @@ class AppState: value: str | int -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppInformation: app_id: int app_address: str @@ -235,7 +235,7 @@ class AppInformation: extra_program_pages: int | None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class CompiledTeal: teal: str compiled: bytes @@ -244,7 +244,7 @@ class CompiledTeal: source_map: algosdk.source_map.SourceMap | None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppCompilationResult: compiled_approval: CompiledTeal compiled_clear: CompiledTeal diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index dac07e1..cbdd60e 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -16,7 +16,7 @@ from algosdk.error import AlgodHTTPError from algosdk.transaction import OnComplete, Transaction from algosdk.v2client.algod import AlgodClient -from deprecated import deprecated +from typing_extensions import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response from algokit_utils.applications.app_manager import AppManager @@ -39,12 +39,12 @@ logger = logging.getLogger(__name__) -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class SenderParam: sender: str -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class CommonTxnParams(SendParams): """ Common transaction parameters. @@ -76,7 +76,7 @@ class CommonTxnParams(SendParams): last_valid_round: int | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class PaymentParams( CommonTxnParams, ): @@ -93,7 +93,7 @@ class PaymentParams( close_remainder_to: str | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetCreateParams( CommonTxnParams, ): @@ -129,7 +129,7 @@ class AssetCreateParams( metadata_hash: bytes | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetConfigParams( CommonTxnParams, ): @@ -153,7 +153,7 @@ class AssetConfigParams( clawback: str | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetFreezeParams( CommonTxnParams, ): @@ -170,7 +170,7 @@ class AssetFreezeParams( frozen: bool -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetDestroyParams( CommonTxnParams, ): @@ -183,7 +183,7 @@ class AssetDestroyParams( asset_id: int -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class OnlineKeyRegistrationParams( CommonTxnParams, ): @@ -209,7 +209,7 @@ class OnlineKeyRegistrationParams( state_proof_key: bytes | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetTransferParams( CommonTxnParams, ): @@ -230,7 +230,7 @@ class AssetTransferParams( close_asset_to: str | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetOptInParams( CommonTxnParams, ): @@ -243,7 +243,7 @@ class AssetOptInParams( asset_id: int -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AssetOptOutParams( CommonTxnParams, ): @@ -255,7 +255,7 @@ class AssetOptOutParams( creator: str -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppCallParams(CommonTxnParams, SenderParam): """ Application call parameters. @@ -286,7 +286,7 @@ class AppCallParams(CommonTxnParams, SenderParam): box_references: list[BoxReference] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppCreateParams(CommonTxnParams, SenderParam): """ Application create parameters. @@ -317,7 +317,7 @@ class AppCreateParams(CommonTxnParams, SenderParam): extra_program_pages: int | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppUpdateParams(CommonTxnParams, SenderParam): """ Application update parameters. @@ -340,7 +340,7 @@ class AppUpdateParams(CommonTxnParams, SenderParam): on_complete: OnComplete | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteParams( CommonTxnParams, SenderParam, @@ -355,7 +355,7 @@ class AppDeleteParams( on_complete: OnComplete = OnComplete.DeleteApplicationOC -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppMethodCall(CommonTxnParams, SenderParam): """Base class for ABI method calls.""" @@ -368,7 +368,7 @@ class AppMethodCall(CommonTxnParams, SenderParam): box_references: list[BoxReference] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppMethodCallParams(CommonTxnParams, SenderParam): """ Method call parameters. @@ -389,7 +389,7 @@ class AppMethodCallParams(CommonTxnParams, SenderParam): box_references: list[BoxReference] | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppCallMethodCall(AppMethodCall): """Parameters for a regular ABI method call. @@ -408,7 +408,7 @@ class AppCallMethodCall(AppMethodCall): on_complete: OnComplete | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppCreateMethodCall(AppMethodCall): """Parameters for an ABI method call that creates an application. @@ -426,7 +426,7 @@ class AppCreateMethodCall(AppMethodCall): extra_program_pages: int | None = None -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppUpdateMethodCall(AppMethodCall): """Parameters for an ABI method call that updates an application. @@ -441,7 +441,7 @@ class AppUpdateMethodCall(AppMethodCall): on_complete: OnComplete = OnComplete.UpdateApplicationOC -@dataclass(frozen=True, kw_only=True) +@dataclass(kw_only=True, frozen=True) class AppDeleteMethodCall(AppMethodCall): """Parameters for an ABI method call that deletes an application. @@ -484,7 +484,7 @@ class AppDeleteMethodCall(AppMethodCall): ] -@dataclass +@dataclass(frozen=True) class BuiltTransactions: """ Set of transactions built by TransactionComposer. @@ -510,7 +510,7 @@ class TransactionComposerBuildResult: class SendAtomicTransactionComposerResults: """Results from sending an AtomicTransactionComposer transaction group""" - group_id: str | None + group_id: str """The group ID if this was a transaction group""" confirmations: list[algosdk.v2client.algod.AlgodResponseType] """The confirmation info for each transaction""" @@ -529,7 +529,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 *, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, - suppress_log: bool = False, + suppress_log: bool | None = None, populate_resources: bool | None = None, # TODO: implement/clarify ) -> SendAtomicTransactionComposerResults: """Send an AtomicTransactionComposer transaction group @@ -597,7 +597,7 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912 # Return results return SendAtomicTransactionComposerResults( - group_id=group_id, + group_id=group_id or "", confirmations=confirmations or [], tx_ids=[t.get_txid() for t in transactions_to_send], transactions=transactions_to_send, @@ -829,7 +829,7 @@ def build_transactions(self) -> BuiltTransactions: return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) - @deprecated(reason="Use send() instead", version="3.0.0") + @deprecated("Use send() instead") def execute( self, *, @@ -843,8 +843,8 @@ def send( self, *, max_rounds_to_wait: int | None = None, - suppress_log: bool = False, - populate_app_call_resources: bool = False, + suppress_log: bool | None = None, + populate_app_call_resources: bool | None = None, ) -> SendAtomicTransactionComposerResults: group = self.build().transactions @@ -1066,6 +1066,8 @@ def _build_method_call( # noqa: C901, PLR0912 boxes=[AppManager.get_box_reference(ref) for ref in params.box_references] if params.box_references else None, + approval_program=params.approval_program if isinstance(params, AppCreateMethodCall) else None, # type: ignore[arg-type] + clear_program=params.clear_state_program if isinstance(params, AppCreateMethodCall) else None, # type: ignore[arg-type] ) return self._build_atc(method_atc) diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index eaeb46b..cfb4682 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -36,9 +36,8 @@ logger = getLogger(__name__) -@dataclass +@dataclass(frozen=True, kw_only=True) class SendSingleTransactionResult: - tx_id: str # Single transaction ID (last from txIds array) transaction: Transaction # Last transaction confirmation: algosdk.v2client.algod.AlgodResponseType # Last confirmation @@ -49,32 +48,29 @@ class SendSingleTransactionResult: confirmations: list[algosdk.v2client.algod.AlgodResponseType] returns: list[algosdk.atomic_transaction_composer.ABIResult] | None = None - # Fields from AssetCreateParams - asset_id: int | None = None +@dataclass(frozen=True, kw_only=True) +class SendSingleAssetCreateTransactionResult(SendSingleTransactionResult): + asset_id: int -@dataclass + +@dataclass(frozen=True) class SendAppTransactionResult(SendSingleTransactionResult): return_value: ABIValue | None = None -@dataclass +@dataclass(frozen=True) class SendAppUpdateTransactionResult(SendAppTransactionResult): compiled_approval: Any | None = None compiled_clear: Any | None = None -@dataclass -class _RequiredSendAppTransactionResult: +@dataclass(frozen=True, kw_only=True) +class SendAppCreateTransactionResult(SendAppUpdateTransactionResult): app_id: int app_address: str -@dataclass -class SendAppCreateTransactionResult(SendAppUpdateTransactionResult, _RequiredSendAppTransactionResult): - pass - - class LogConfig(TypedDict, total=False): pre_log: Callable[[TxnParams, Transaction], str] post_log: Callable[[TxnParams, AtomicTransactionResponse], str] @@ -123,7 +119,6 @@ def send_transaction(params: T) -> SendSingleTransactionResult: **raw_result_dict, confirmation=raw_result.confirmations[-1], transaction=raw_result.transactions[-1], - tx_id=raw_result.tx_ids[-1], ) if post_log: @@ -212,7 +207,7 @@ def payment(self, params: PaymentParams) -> SendSingleTransactionResult: ), )(params) - def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult: + def asset_create(self, params: AssetCreateParams) -> SendSingleAssetCreateTransactionResult: """Create a new Algorand Standard Asset.""" result = self._send( lambda c: c.add_asset_create, @@ -225,11 +220,10 @@ def asset_create(self, params: AssetCreateParams) -> SendSingleTransactionResult ), )(params) - result = SendSingleTransactionResult( + return SendSingleAssetCreateTransactionResult( **result.__dict__, + asset_id=int(result.confirmation["asset-index"]), # type: ignore[call-overload] ) - result.asset_id = int(result.confirmation["asset-index"]) # type: ignore[call-overload] - return result def asset_config(self, params: AssetConfigParams) -> SendSingleTransactionResult: """Configure an existing Algorand Standard Asset.""" diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 35c0c7e..32fe3b4 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -1,72 +1,138 @@ -# from pathlib import Path - -# import pytest - -# from algokit_utils.applications.app_factory import AppFactory, AppFactoryDeployParams -# from algokit_utils.clients.algorand_client import AlgorandClient -# from algokit_utils.models.account import Account - - -# @pytest.fixture -# def algorand(funded_account: Account) -> AlgorandClient: -# client = AlgorandClient.default_local_net() -# client.set_signer(sender=funded_account.address, signer=funded_account.signer) -# return client - - -# @pytest.fixture -# def factory(algorand: AlgorandClient, funded_account: Account) -> AppFactory: -# """Create AppFactory fixture""" -# raw_arc56_spec = (Path(__file__).parent.parent / "artifacts" / "hello_world" / "arc32_app_spec.json").read_text() -# return algorand.client.get_app_factory(app_spec=raw_arc56_spec, default_sender=funded_account.address) - - -# class TestARC56: -# def test_error_messages_with_template_vars(self, factory: AppFactory) -> None: -# """Test ARC56 error messages with dynamic template variables""" -# # Deploy app -# result = factory.deploy( -# AppFactoryDeployParams( -# create_params={"method": "createApplication"}, -# deploy_time_params={ -# "bytes64TmplVar": "0" * 64, -# "uint64TmplVar": 123, -# "bytes32TmplVar": "0" * 32, -# "bytesTmplVar": "foo", -# }, -# ) -# ) -# app_client = result.app_client - -# # Test error handling -# with pytest.raises(Exception) as exc: -# app_client.call(method="throwError") - -# assert "this is an error" in str(exc.value) - -# def test_undefined_error_message(self, factory: AppFactory) -> None: -# """Test ARC56 undefined error message with template variables""" -# # Deploy app -# result = factory.deploy( -# create_params={"method": "createApplication"}, -# deploy_time_params={ -# "bytes64TmplVar": "0" * 64, -# "uint64TmplVar": 0, -# "bytes32TmplVar": "0" * 32, -# "bytesTmplVar": "foo", -# }, -# ) -# app_id = result.app_id - -# # Create new client without source maps -# app_client = AppClient( -# app_id=app_id, algod=algod, app_spec=arc56_json, default_sender=get_localnet_default_account() -# ) - -# # Test error handling -# with pytest.raises(Exception) as exc: -# app_client.call(method="tmpl") - -# error_stack = "\n".join(line.strip() for line in str(exc.value).split("\n")) -# assert "assert <--- Error" in error_stack -# assert "intc 1 // TMPL_uint64TmplVar" in error_stack +from pathlib import Path + +import pytest +from algosdk.logic import get_application_address +from algosdk.transaction import ApplicationCreateTxn, OnComplete + +from algokit_utils._legacy_v2.deploy import OnSchemaBreak, OnUpdate, OperationPerformed +from algokit_utils.applications.app_client import AppClientMethodCallParams +from algokit_utils.applications.app_factory import AppFactory, AppFactoryCreateWithSendParams +from algokit_utils.clients.algorand_client import AlgorandClient +from algokit_utils.models.account import Account +from algokit_utils.models.amount import AlgoAmount + + +@pytest.fixture +def algorand(funded_account: Account) -> AlgorandClient: + client = AlgorandClient.default_local_net() + client.set_signer(sender=funded_account.address, signer=funded_account.signer) + return client + + +@pytest.fixture +def app_spec() -> str: + return (Path(__file__).parent.parent / "artifacts" / "testing_app" / "arc32_app_spec.json").read_text() + + +@pytest.fixture +def factory(algorand: AlgorandClient, funded_account: Account, app_spec: str) -> AppFactory: + """Create AppFactory fixture""" + return algorand.client.get_app_factory(app_spec=app_spec, default_sender=funded_account.address) + + +def test_create_app(factory: AppFactory) -> None: + """Test creating an app using the factory""" + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + } + ) + ) + + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + assert result.compiled_approval is not None + assert result.compiled_clear is not None + + +def test_create_app_with_constructor_deploy_time_params(algorand: AlgorandClient, app_spec: str) -> None: + """Test creating an app using the factory with constructor deploy time params""" + random_account = algorand.account.random() + dispenser_account = algorand.account.localnet_dispenser() + algorand.account.ensure_funded( + account_fo_fund=random_account.address, + dispenser_account=dispenser_account.address, + min_spending_balance=AlgoAmount.from_algo(10), + min_funding_increment=AlgoAmount.from_algo(1), + ) + + factory = algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=random_account.address, + deploy_time_params={ + # It should strip off the TMPL_ + "TMPL_UPDATABLE": 0, + "DELETABLE": 0, + "VALUE": 1, + }, + ) + + app_client, result = factory.send.bare.create() + + assert result.app_id > 0 + assert app_client.app_id == result.app_id + + +def test_create_app_with_oncomplete_overload(factory: AppFactory) -> None: + app_client, result = factory.send.bare.create( + params=AppFactoryCreateWithSendParams( + on_complete=OnComplete.OptInOC, + updatable=True, + deletable=True, + deploy_time_params={ + "VALUE": 1, + }, + ) + ) + + assert isinstance(result.transaction, ApplicationCreateTxn) + assert result.transaction.on_complete == OnComplete.OptInOC + assert app_client.app_id > 0 + assert app_client.app_address == get_application_address(app_client.app_id) + assert isinstance(result.confirmation, dict) + assert result.confirmation.get("application-index", 0) == app_client.app_id + + +def test_deploy_when_immutable_and_permanent(factory: AppFactory) -> None: + factory.deploy( + deletable=False, + updatable=False, + on_schema_break=OnSchemaBreak.Fail, + on_update=OnUpdate.Fail, + deploy_time_params={ + "VALUE": 1, + }, + ) + + +def test_deploy_app_create(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) + + +def test_deploy_app_create_abi(factory: AppFactory) -> None: + app_client, result = factory.deploy( + deploy_time_params={ + "VALUE": 1, + }, + create_params=AppClientMethodCallParams(method="create_abi", args=["arg_io"]), + ) + + assert result.operation_performed == OperationPerformed.Create + assert result.app_id > 0 + assert app_client.app_id == result.app_id == result.confirmation["application-index"] # type: ignore[call-overload] + assert app_client.app_address == get_application_address(app_client.app_id) diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index e6db07a..cd034db 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -222,7 +222,7 @@ def test_create_app_call_method_call_transaction(algorand: AlgorandClient, funde schema={"global_ints": 0, "global_bytes": 0, "local_ints": 0, "local_bytes": 0}, ) ) - app_id = algorand.client.algod.pending_transaction_info(create_result.tx_id)["application-index"] # type: ignore[call-overload] + app_id = algorand.client.algod.pending_transaction_info(create_result.tx_ids[0])["application-index"] # type: ignore[call-overload] # Then test creating a method call transaction result = algorand.create_transaction.app_call_method_call(