From 0755c87dbd17a74e9b1a3a3caf558420cfd84eeb Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 28 Jan 2025 01:40:31 +0100 Subject: [PATCH] docs: further refinements in capabilities .md files --- docs/source/capabilities/account.md | 160 +++--- docs/source/capabilities/algorand-client.md | 230 ++++++-- docs/source/capabilities/amount.md | 52 +- docs/source/capabilities/app-client.md | 575 ++++++-------------- docs/source/capabilities/app-deploy.md | 65 ++- docs/source/capabilities/app-manager.md | 9 +- docs/source/capabilities/client.md | 26 +- docs/source/capabilities/debugger.md | 41 +- docs/source/capabilities/transaction.md | 12 +- docs/source/capabilities/transfer.md | 28 +- 10 files changed, 550 insertions(+), 648 deletions(-) diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index cb7190a0..3864be4f 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -4,39 +4,38 @@ Account management is one of the core capabilities provided by AlgoKit Utils. It ## `AccountManager` -The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using transaction composition to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! +The `AccountManager` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the `TransactionComposer` to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! -To get an instance of `AccountManager`, you can either use the `AlgorandClient` via `algorand.account` or instantiate it directly: +To get an instance of `AccountManager`, you can use either `AlgorandClient` via `algorand.account` or instantiate it directly: ```python -from algokit_utils import AccountManager +from algokit_utils.accounts.account_manager import AccountManager account_manager = AccountManager(client_manager) ``` -## `Account` and Transaction Signing +## `TransactionSignerAccount` -The core type that holds information about a signer/sender pair for a transaction in Python is the `Account` class, which represents both the signing capability and sender address in one object. This is different from the TypeScript implementation which uses `TransactionSignerAccount` interface that combines an `algosdk.TransactionSigner` with a sender address. +The core internal type that holds information about a signer/sender pair for a transaction is `TransactionSignerAccount`, which represents an `algosdk.TransactionSigner` (`signer`) along with a sender address (`addr`) as the encoded string address. -The Python `Account` class provides: - -- `address` - The encoded string address -- `private_key` - The private key for signing -- `signer` - An `AccountTransactionSigner` that can sign transactions -- `public_key` - The public key associated with this account +Many methods in `AccountManager` expose a `TransactionSignerAccount`. `TransactionSignerAccount` can be used with `AtomicTransactionComposer`, `TransactionComposer` and other Algorand SDK tools. ## Registering a signer -The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by the transaction composition functionality to automatically sign transactions by that sender. Any of the [methods](#accounts) within `AccountManager` that return an account will automatically register the signer with the sender. +The `AccountManager` keeps track of which signer is associated with a given sender address. This is used by `AlgorandClient` to automatically sign transactions by that sender. Any of the methods within `AccountManager` that return an account will automatically register the signer with the sender. If however, you are creating a signer external to the `AccountManager`, then you need to register the signer with the `AccountManager` if you want it to be able to automatically sign transactions from that sender. -There are two methods that can be used for this: +There are two methods that can be used for this, `set_signer_from_account`, which takes any number of account based objects that combine signer and sender (`TransactionSignerAccount`, `SigningAccount`, `LogicSigAccount`, `MultiSigAccount`), or `set_signer` which takes the sender address and the `TransactionSigner`: ```python -# Register an account object that has both signer and sender -account_manager.set_signer_from_account(account) - -# Register just a signer for a given sender address -account_manager.set_signer("SENDER_ADDRESS", transaction_signer) +algorand.account\ + .set_signer_from_account(SigningAccount.new_account())\ + .set_signer_from_account(LogicSigAccount(algosdk.transaction.LogicSigAccount(program, args)))\ + .set_signer_from_account(MultiSigAccount( + MultisigMetadata(version=1, threshold=1, addresses=["ADDRESS1...", "ADDRESS2..."]), + [account1, account2] + ))\ + .set_signer_from_account(TransactionSignerAccount(address="SENDERADDRESS", signer=transaction_signer))\ + .set_signer("SENDERADDRESS", transaction_signer) ``` ## Default signer @@ -44,103 +43,95 @@ account_manager.set_signer("SENDER_ADDRESS", transaction_signer) If you want to have a default signer that is used to sign transactions without a registered signer (rather than throwing an exception) then you can register a default signer: ```python -account_manager.set_default_signer(my_default_signer) +algorand.account.set_default_signer(my_default_signer) ``` ## Get a signer -The library will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: +`AlgorandClient` will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can retrieve the signer for a given sender address: ```python -signer = account_manager.get_signer("SENDER_ADDRESS") +signer = algorand.account.get_signer("SENDER_ADDRESS") ``` -If there is no signer registered for that sender address it will either return the default signer (if registered) or raise an exception. +If there is no signer registered for that sender address it will either return the default signer (if registered) or raise a `ValueError`. ## Accounts In order to get/register accounts for signing operations you can use the following methods on `AccountManager`: -- `from_environment(name: str, fund_with: AlgoAmount | None = None) -> Account` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `{NAME}_MNEMONIC` and (optionally) `{NAME}_SENDER` (if account is rekeyed) - - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code - - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD -- `from_mnemonic(mnemonic_secret: str) -> Account` - Registers and returns an account with secret key loaded by taking the mnemonic secret -- `multisig(version: int, threshold: int, addrs: list[str], signing_accounts: list[Account]) -> MultisigAccount` - Registers and returns a multisig account with one or more signing keys loaded -- `rekeyed(sender: Account | str, account: Account) -> Account` - Registers and returns an account representing the given rekeyed sender/signer combination -- `random() -> Account` - Returns a new, cryptographically randomly generated account with private key loaded -- `from_kmd(name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an account with private key loaded from the given KMD wallet -- `logic_sig(program: bytes, args: list[bytes] | None = None) -> LogicSigAccount` - Returns an account that represents a logic signature +- `algorand.account.from_environment(name, fund_with)` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `os.getenv('{NAME}_MNEMONIC')` and (optionally) `os.getenv('{NAME}_SENDER')` (if account is rekeyed) +- `algorand.account.from_mnemonic(mnemonic=mnemonic, sender=None)` - Registers and returns an account with secret key loaded by taking the mnemonic secret +- `algorand.account.multisig(metadata, signing_accounts)` - Registers and returns a multisig account with one or more signing keys loaded +- `algorand.account.rekeyed(sender=sender, account=account)` - Registers and returns an account representing the given rekeyed sender/signer combination +- `algorand.account.random()` - Returns a new, cryptographically randomly generated account with private key loaded +- `algorand.account.from_kmd(name, predicate=None, sender=None)` - Returns an account with private key loaded from the given KMD wallet (identified by name) +- `algorand.account.logicsig(program, args=None)` - Returns an account that represents a logic signature ### Underlying account classes -While `Account` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer: +While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. -- `Account` - The main account class that combines address and private key -- `LogicSigAccount` - An in-built algosdk `LogicSigAccount` object for logic signature accounts -- `MultisigAccount` - An abstraction around multisig accounts that supports multisig accounts with one or more signers present +- `SigningAccount` - A class that holds the private key and address for an account, with support for rekeyed accounts +- `LogicSigAccount` - A wrapper around `algosdk.transaction.LogicSigAccount` object +- `MultiSigAccount` - A wrapper around Algorand SDK's multisig functionality that supports multisig accounts with one or more signers present +- `MultisigMetadata` - A dataclass containing the version, threshold and addresses for a multisig account ### Dispenser -- `dispenser_from_environment() -> Account` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present -- `localnet_dispenser() -> Account` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account +- `algorand.account.dispenser_from_environment()` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- `algorand.account.localnet_dispenser()` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account ## Rekey account One of the unique features of Algorand is the ability to change the private key that can authorise transactions for an account. This is called [rekeying](https://developer.algorand.org/docs/get-details/accounts/rekey/). -```{warning} -Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. -``` +> [!WARNING] +> Rekeying should be done with caution as a rekey transaction can result in permanent loss of control of an account. -You can issue a transaction to rekey an account by using the `rekey_account` method: +You can issue a transaction to rekey an account by using the `algorand.account.rekey_account(account, rekey_to, **kwargs)` function: -```python -account_manager.rekey_account( - account="ACCOUNTADDRESS", # str | Account - rekey_to="NEWADDRESS", # str | Account - # Optional parameters - signer=None, # TransactionSigner - note=None, # bytes - lease=None, # bytes - static_fee=None, # AlgoAmount - extra_fee=None, # AlgoAmount - max_fee=None, # AlgoAmount - validity_window=None, # int - first_valid_round=None, # int - last_valid_round=None, # int - suppress_log=None # bool -) -``` - -You can also pass in `rekey_to` as a common transaction parameter to any transaction. +- `account: str | TransactionSignerAccount` - The account address or signing account of the account that will be rekeyed +- `rekey_to: str | TransactionSignerAccountProtocol` - The account address or signing account of the account that will be used to authorise transactions for the rekeyed account going forward. If a signing account is provided that will now be tracked as the signer for `account` in the `AccountManager` instance. +- Optional keyword arguments: + - Common transaction parameters + - Execution parameters ### Examples ```python # Basic example (with string addresses) -account_manager.rekey_account(account="ACCOUNTADDRESS", rekey_to="NEWADDRESS") +algorand.account.rekey_account( + account="ACCOUNTADDRESS", + rekey_to="NEWADDRESS" +) # Basic example (with signer accounts) -account_manager.rekey_account(account=account1, rekey_to=new_signer_account) +algorand.account.rekey_account( + account=account1, + rekey_to=new_signer_account +) # Advanced example -account_manager.rekey_account( +algorand.account.rekey_account( account="ACCOUNTADDRESS", rekey_to="NEWADDRESS", - lease="lease", - note="note", + lease=b"lease", + note=b"note", first_valid_round=1000, validity_window=10, - extra_fee=1000, # microAlgos - static_fee=1000, # microAlgos - max_fee=3000, # microAlgos - max_rounds_to_wait_for_confirmation=5, - suppress_log=True, + extra_fee=AlgoAmount.from_micro_algos(1000), + static_fee=AlgoAmount.from_micro_algos(1000), + # Max fee doesn't make sense with extraFee AND staticFee + # already specified, but here for completeness + max_fee=AlgoAmount.from_micro_algos(3000), + suppress_log=True ) # Using a rekeyed account -# Note: if a signing account is passed into account_manager.rekey_account then you don't need to call rekeyed_account to register the new signer -rekeyed_account = account_manager.rekeyed(account, new_account) +# Note: if a signing account is passed into algorand.account.rekey_account +# then you don't need to call rekeyed to register the new signer +rekeyed_account = algorand.account.rekeyed(sender=account, account=new_account) # rekeyed_account can be used to sign transactions on behalf of account... ``` @@ -153,10 +144,10 @@ When running LocalNet, you have an instance of the [Key Management Daemon](https The KMD SDK is fairly low level so to make use of it there is a fair bit of boilerplate code that's needed. This code has been abstracted away into the `KmdAccountManager` class. -To get an instance of the `KmdAccountManager` class you can access it from `AccountManager` via `account_manager.kmd` or instantiate it directly (passing in a `ClientManager`): +To get an instance of the `KmdAccountManager` class you can access it from `AlgorandClient` via `algorand.account.kmd` or instantiate it directly: ```python -from algokit_utils import KmdAccountManager +from algokit_utils.accounts.kmd_account_manager import KmdAccountManager # Algod client only kmd_account_manager = KmdAccountManager(client_manager) @@ -164,32 +155,35 @@ kmd_account_manager = KmdAccountManager(client_manager) The methods that are available are: -- `get_wallet_account(wallet_name: str, predicate: Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) -> Account` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). -- `get_or_create_wallet_account(name: str, fund_with: AlgoAmount | None = None) -> Account` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. -- `get_localnet_dispenser_account() -> Account` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) +- `get_wallet_account(wallet_name, predicate=None, sender=None)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- `get_or_create_wallet_account(name, fund_with=None)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- `get_localnet_dispenser_account()` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) ```python # Get a wallet account that seeded the LocalNet network default_dispenser_account = kmd_account_manager.get_wallet_account( "unencrypted-default-wallet", - lambda a: a.status != "Offline" and a.amount > 1_000_000_000, + lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000 ) # Same as above, but dedicated method call for convenience localnet_dispenser_account = kmd_account_manager.get_localnet_dispenser_account() # Idempotently get (if exists) or create (if it doesn't exist yet) an account by name using KMD # if creating it then fund it with 2 ALGO from the default dispenser account -new_account = kmd_account_manager.get_or_create_wallet_account("account1", AlgoAmount.from_algo(2)) +new_account = kmd_account_manager.get_or_create_wallet_account( + "account1", + AlgoAmount.from_algos(2) +) # This will return the same account as above since the name matches existing_account = kmd_account_manager.get_or_create_wallet_account("account1") ``` -Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions: +Some of this functionality is directly exposed from `AccountManager`, which has the added benefit of registering the account as a signer so they can be automatically used to sign transactions when using via `AlgorandClient`: ```python # Get and register LocalNet dispenser -localnet_dispenser = account_manager.localnet_dispenser() +localnet_dispenser = algorand.account.localnet_dispenser() # Get and register a dispenser by environment variable, or if not set then LocalNet dispenser via KMD -dispenser = account_manager.dispenser_from_environment() +dispenser = algorand.account.dispenser_from_environment() # Get / create and register account from KMD idempotently by name -account1 = account_manager.from_kmd("account1", AlgoAmount.from_algo(2)) +account1 = algorand.account.from_kmd("account1", fund_with=AlgoAmount.from_algos(2)) ``` diff --git a/docs/source/capabilities/algorand-client.md b/docs/source/capabilities/algorand-client.md index 181751dc..3e7c517f 100644 --- a/docs/source/capabilities/algorand-client.md +++ b/docs/source/capabilities/algorand-client.md @@ -1,36 +1,31 @@ # Algorand client -`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the default entrypoint into AlgoKit Utils functionality. +`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the [default entrypoint](../../../README.md) into AlgoKit Utils functionality. -The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class. You can get started by using one of the static initialization methods to create an Algorand client: +The main entrypoint to the bulk of the functionality in AlgoKit Utils is the `AlgorandClient` class, most of the time you can get started by typing `AlgorandClient.` and choosing one of the static initialisation methods to create an [Algorand client](todo_paste_url), e.g.: ```python # Point to the network configured through environment variables or -# if no environment variables it will point to the default LocalNet configuration +# if no environment variables it will point to the default LocalNet +# configuration algorand = AlgorandClient.from_environment() # Point to default LocalNet configuration -algorand = AlgorandClient.default_localnet() +algorand = AlgorandClient.default_local_net() # Point to TestNet using AlgoNode free tier algorand = AlgorandClient.testnet() # Point to MainNet using AlgoNode free tier algorand = AlgorandClient.mainnet() -# Point to a pre-created algod client(s) -algorand = AlgorandClient.from_clients( - AlgoSdkClients( - algod=..., - indexer=..., - kmd=..., - ) -) -# Point to custom configuration +# Point to a pre-created algod client +algorand = AlgorandClient.from_clients(algod=algod) +# Point to pre-created algod, indexer and kmd clients +algorand = AlgorandClient.from_clients(algod=algod, indexer=indexer, kmd=kmd) +# Point to custom configuration for algod +algorand = AlgorandClient.from_config(algod_config=algod_config) +# Point to custom configuration for algod, indexer and kmd algorand = AlgorandClient.from_config( - AlgoClientConfigs( - algod_config=AlgoClientConfig( - server="http://localhost:4001", token="my-token", port=4001 - ), - indexer_config=None, - kmd_config=None, - ) + algod_config=algod_config, + indexer_config=indexer_config, + kmd_config=kmd_config, ) ``` @@ -39,7 +34,7 @@ algorand = AlgorandClient.from_config( Once you have an `AlgorandClient` instance, you can access the SDK clients for the various Algorand APIs via the `algorand.client` property. ```python -algorand = AlgorandClient.default_localnet() +algorand = AlgorandClient.default_local_net() algod_client = algorand.client.algod indexer_client = algorand.client.indexer @@ -48,38 +43,195 @@ kmd_client = algorand.client.kmd ## Accessing manager class instances -The `AlgorandClient` has several manager class instances that help you quickly access advanced functionality: +The `AlgorandClient` has a number of manager class instances that help you quickly use intellisense to get access to advanced functionality. -- `AccountManager` via `algorand.account`, with chainable convenience methods: - - `algorand.set_default_signer(signer)` +- [`AccountManager`](todo_paste_url) via `algorand.account`, there are also some chainable convenience methods which wrap specific methods in `AccountManager`: + - `algorand.set_default_signer(signer)` - + - `algorand.set_signer_from_account(account)` - - `algorand.set_signer(sender, signer)` -- `AssetManager` via `algorand.asset` -- `ClientManager` via `algorand.client` -- `AppManager` via `algorand.app` -- `AppDeployer` via `algorand.app_deployer` +- [`AssetManager`](todo_paste_url) via `algorand.asset` +- [`ClientManager`](todo_paste_url) via `algorand.client` ## Creating and issuing transactions -`AlgorandClient` exposes methods to create, execute, and compose groups of transactions via the `TransactionComposer`. +`AlgorandClient` exposes a series of methods that allow you to create, execute, and compose groups of transactions (all via the [`TransactionComposer`](todo_paste_url)). -### Transaction configuration +### Creating transactions + +You can compose a transaction via `algorand.create_transaction.`, which gives you an instance of the [`AlgorandClientTransactionCreator`](todo_paste_url) class. Intellisense will guide you on the different options. + +The signature for the calls to send a single transaction usually look like: + +```python +def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> Transaction: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonTransactionParams - Common transaction parameters + returns: Transaction - An unsigned algosdk.Transaction object, ready to be signed and sent + """ +``` + +- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space or cmd+space). +- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) +- [`CommonTransactionParams`](todo_paste_url) are the [common transaction parameters](todo_paste_url) that can be specified for every single transaction +- `Transaction` is an unsigned `algosdk.Transaction` object, ready to be signed and sent + +The return type for the ABI method call methods are slightly different: + +```python +def app_call_type_method_call(self, *, params: ComposerTransactionTypeParams, **common_params) -> BuiltTransactions: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonTransactionParams - Common transaction parameters + returns: BuiltTransactions - Container for transactions, method calls and signers + """ +``` -AlgorandClient caches network provided transaction values automatically to reduce network traffic. You can configure this behavior: +Where `BuiltTransactions` looks like this: -- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds the transaction will be valid). Defaults to 10. -- `algorand.set_suggested_params(suggested_params, until?)` - Set the suggested network parameters to use (optionally until the given time) -- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout for caching suggested network parameters (default 3 seconds) -- `algorand.get_suggested_params()` - Get current suggested network parameters +```python +@dataclass +class BuiltTransactions: + """Container for built transactions and associated metadata""" + # The built transactions + transactions: list[Transaction] + # Any ABIMethod objects associated with any of the transactions in a dict keyed by transaction index + method_calls: dict[int, ABIMethod] + # Any TransactionSigner objects associated with any of the transactions in a dict keyed by transaction index + signers: dict[int, TransactionSigner] +``` + +This signifies the fact that an ABI method call can actually result in multiple transactions (which in turn may have different signers), that you need ABI metadata to be able to extract the return value from the transaction result. + +### Sending a single transaction + +You can compose a single transaction via `algorand.send...`, which gives you an instance of the [`AlgorandClientTransactionSender`](todo_paste_url) class. Intellisense will guide you on the different options. + +Further documentation is present in the related capabilities: + +- [App management](todo_paste_url) +- [Asset management](todo_paste_url) +- [Algo transfers](todo_paste_url) -### Creating transaction groups +The signature for the calls to send a single transaction usually look like: -You can compose a group of transactions using the `new_group()` method which returns a `TransactionComposer` instance: +```python +def method(self, *, params: ComposerTransactionTypeParams, **common_params) -> SingleSendTransactionResult: + """ + params: ComposerTransactionTypeParams - Transaction type specific parameters + common_params: CommonAppCallParams & SendParams - Common parameters for app calls and transaction sending + returns: SingleSendTransactionResult - Result of sending a single transaction + """ +``` + +- To get intellisense on the params, use your IDE's intellisense keyboard shortcut (e.g. ctrl+space). +- `ComposerTransactionTypeParams` will be the parameters that are specific to that transaction type e.g. `PaymentParams`, [see the full list](todo_paste_url) +- [`CommonAppCallParams`](todo_paste_url) are the [common app call transaction parameters](todo_paste_url) that can be specified for every single app transaction +- [`SendParams`](todo_paste_url) are the [parameters](todo_paste_url) that control execution semantics when sending transactions to the network +- [`SendSingleTransactionResult`](todo_paste_url) is all of the information that is relevant when [sending a single transaction to the network](todo_paste_url) + +Generally, the functions to immediately send a single transaction will emit log messages before and/or after sending the transaction. You can opt-out of this by sending `suppress_log=True`. + +### Composing a group of transactions + +You can compose a group of transactions for execution by using the `new_group()` method on `AlgorandClient` and then use the various `.add_{type}()` methods on [`TransactionComposer`](todo_paste_url) to add a series of transactions. ```python result = ( - algorand.new_group() - .add_payment(sender="SENDERADDRESS", receiver="RECEIVERADDRESS", amount=1_000) + algorand + .new_group() + .add_payment( + sender="SENDERADDRESS", + receiver="RECEIVERADDRESS", + amount=microalgos(1) + ) .add_asset_opt_in(sender="SENDERADDRESS", asset_id=12345) .send() ) ``` + +`new_group()` returns a new [`TransactionComposer`](todo_paste_url) instance, which can also return the group of transactions, simulate them and other things. + +### Transaction parameters + +To create a transaction you define a set of parameters as a Python params dataclass instance. + +The type [`TxnParams`](todo_paste_url) is a union type representing all of the transaction parameters that can be specified for constructing any Algorand transaction type. + +- `sender: str` - The address of the account sending the transaction +- `signer: TransactionSigner | TransactionSignerAccountProtocol | None` - The function used to sign transaction(s); if not specified then an attempt will be made to find a registered signer for the given `sender` or use a default signer (if configured) +- `rekey_to: Optional[str]` - Change the signing key of the sender to the given address. **Warning:** Please be careful and read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/) +- `note: Optional[bytes | str]` - Note to attach to transaction (UTF-8 encoded if string). Max 1000 bytes +- `lease: Optional[bytes | str]` - Prevent duplicate transactions with same lease (max 32 bytes). [Lease documentation](https://developer.algorand.org/articles/leased-transactions-securing-advanced-smart-contract-design/) +- Fee management: + - `static_fee: Optional[AlgoAmount]` - Fixed transaction fee (use `extra_fee` instead unless setting to 0) + - `extra_fee: Optional[AlgoAmount]` - Additional fee to cover inner transactions + - `max_fee: Optional[AlgoAmount]` - Maximum allowed fee (prevents overspending during congestion) +- Validity management: + + - `validity_window: Optional[int]` - Number of rounds transaction is valid (default: 10) + - `first_valid_round: Optional[int]` - Explicit first valid round (use with caution) + - `last_valid_round: Optional[int]` - Explicit last valid round (prefer `validity_window`) + +- [`SendParams`](todo_paste_url) + - `max_rounds_to_wait_for_confirmation: Optional[int]` - Maximum rounds to wait for confirmation + - `suppress_log: bool` - Suppress log messages (default: False) + - `populate_app_call_resources: bool` - Auto-populate app call resources using simulation (default: from config) + - `cover_app_call_inner_transaction_fees: bool` - Automatically cover inner transaction fees via simulation + +Some more transaction-specific parameters extend these base types: + +#### Payment Transactions (`PaymentParams`) + +- `receiver: str` - Recipient address +- `amount: AlgoAmount` - Amount to send +- `close_remainder_to: Optional[str]` - Address to send remaining funds to (for account closure) + +#### Asset Transactions + +- `AssetTransferParams`: Asset transfers including opt-in +- `AssetCreateParams`: Asset creation +- `AssetConfigParams`: Asset configuration +- `AssetFreezeParams`: Asset freezing +- `AssetDestroyParams`: Asset destruction + +#### Application Transactions + +- `AppCallParams`: Generic application calls +- `AppCreateParams`: Application creation +- `AppUpdateParams`: Application update +- `AppDeleteParams`: Application deletion + +#### Key Registration + +- `OnlineKeyRegistrationParams`: Register online participation keys +- `OfflineKeyRegistrationParams`: Take account offline + +Usage example with `AlgorandClient`: + +```python +# Create transaction +payment = client.create_transaction.payment( + PaymentParams(sender=account.address, receiver=receiver, amount=AlgoAmount(1)) +) + +# Send transaction +result = client.send.send_transaction(payment, SendParams()) +``` + +These parameters are used with the [`TransactionComposer`](todo_paste_url) class which handles: + +- Automatic fee calculation +- Validity window management +- Transaction grouping +- ABI method handling +- Simulation-based resource population + +### Transaction configuration + +AlgorandClient caches network provided transaction values for you automatically to reduce network traffic. It has a set of default configurations that control this behaviour, but you have the ability to override and change the configuration of this behaviour: + +- `algorand.set_default_validity_window(validity_window)` - Set the default validity window (number of rounds from the current known round that the transaction will be valid to be accepted for), having a smallish value for this is usually ideal to avoid transactions that are valid for a long future period and may be submitted even after you think it failed to submit if waiting for a particular number of rounds for the transaction to be successfully submitted. The validity window defaults to `10`, except localnet environments where it's set to `1000`. +- `algorand.set_suggested_params(suggested_params, until=None)` - Set the suggested network parameters to use (optionally until the given time) +- `algorand.set_suggested_params_timeout(timeout)` - Set the timeout that is used to cache the suggested network parameters (by default 3 seconds) +- `algorand.get_suggested_params()` - Get the current suggested network parameters object, either the cached value, or if the cache has expired a fresh value diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md index e1c3a7be..4478c918 100644 --- a/docs/source/capabilities/amount.md +++ b/docs/source/capabilities/amount.md @@ -2,39 +2,37 @@ Algo amount handling is one of the core capabilities provided by AlgoKit Utils. It allows you to reliably and tersely specify amounts of microAlgo and Algo and safely convert between them. -Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function you can safely and explicitly convert to microAlgo or Algo. +Any AlgoKit Utils function that needs an Algo amount will take an `AlgoAmount` object, which ensures that there is never any confusion about what value is being passed around. Whenever an AlgoKit Utils function calls into an underlying algosdk function, or if you need to take an `AlgoAmount` and pass it into an underlying algosdk function (per the [modularity principle](todo_paste_url)) you can safely and explicitly convert to microAlgo or Algo. -To see some usage examples check out the automated tests in the repository. Alternatively, you can refer to the reference documentation for `AlgoAmount`. +To see some usage examples check out the automated tests. Alternatively, you can see the reference documentation for `AlgoAmount`. ## `AlgoAmount` -The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or exiting the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). +The `AlgoAmount` class provides a safe wrapper around an underlying amount of microAlgo where any value entering or existing the `AlgoAmount` class must be explicitly stated to be in microAlgo or Algo. This makes it much safer to handle Algo amounts rather than passing them around as raw numbers where it's easy to make a (potentially costly!) mistake and not perform a conversion when one is needed (or perform one when it shouldn't be!). To import the AlgoAmount class you can access it via: ```python -from algokit_utils.models import AlgoAmount +from algokit_utils import AlgoAmount ``` ### Creating an `AlgoAmount` -There are several ways to create an `AlgoAmount`: +There are a few ways to create an `AlgoAmount`: - Algo - - Constructor: `AlgoAmount({"algo": 10})` - - Static helper: `AlgoAmount.from_algo(10)` - - Static helper (plural): `AlgoAmount.from_algos(10)` + - Constructor: `AlgoAmount({"algo": 10})` or `AlgoAmount({"algos": 10})` + - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` - microAlgo - - Constructor: `AlgoAmount({"microAlgo": 10_000})` - - Static helper: `AlgoAmount.from_micro_algo(10_000)` - - Static helper (plural): `AlgoAmount.from_micro_algos(10_000)` + - Constructor: `AlgoAmount({"microAlgo": 10_000})` or `AlgoAmount({"microAlgos": 10_000})` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` ### Extracting a value from `AlgoAmount` The `AlgoAmount` class has properties to return Algo and microAlgo: -- `amount.algo` or `amount.algos` - Returns the value in Algo -- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo +- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer `AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. @@ -42,28 +40,16 @@ You can also call `str(amount)` or use an `AlgoAmount` directly in string interp ### Additional Features -The `AlgoAmount` class also supports: +The `AlgoAmount` class supports arithmetic operations: -- Arithmetic operations (`+`, `-`) with other `AlgoAmount` objects or integers -- Comparison operations (`<`, `<=`, `>`, `>=`, `==`, `!=`) -- In-place arithmetic (`+=`, `-=`) +- Addition: `amount1 + amount2` +- Subtraction: `amount1 - amount2` +- Comparison operations: `<`, `<=`, `>`, `>=`, `==`, `!=` -Example usage: +Example: ```python -from algokit_utils.models import AlgoAmount - -# Create amounts -amount1 = AlgoAmount.from_algo(1.5) # 1.5 Algos -amount2 = AlgoAmount.from_micro_algos(500_000) # 0.5 Algos - -# Arithmetic -total = amount1 + amount2 # 2 Algos -difference = amount1 - amount2 # 1 Algo - -# Comparisons -is_greater = amount1 > amount2 # True - -# String representation -print(amount1) # "1,500,000 µALGO" +amount1 = AlgoAmount({"algo": 1}) +amount2 = AlgoAmount({"microAlgo": 500_000}) +total = amount1 + amount2 # Results in 1.5 Algo ``` diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 91a9982e..4d43e02d 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -1,9 +1,9 @@ -# App Client and App Factory +# App client and App factory > [!NOTE] > This page covers the untyped app client, but we recommend using typed clients (coming soon), which will give you a better developer experience with strong typing specific to the app itself. -App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](./app-deploy.md) and [App management](./app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. +App client and App factory are higher-order use case capabilities provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App deployment](../../markdown/capabilities/app-deploy.md) and [App management](../../markdown/capabilities/app.md). They allow you to access high productivity application clients that work with [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) and [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application spec defined smart contracts, which you can use to create, update, delete, deploy and call a smart contract and access state data for it. > [!NOTE] > If you are confused about when to use the factory vs client the mental model is: use the client if you know the app ID, use the factory if you don't know the app ID (deferred knowledge or the instance doesn't exist yet on the blockchain) or you have multiple app IDs @@ -15,26 +15,22 @@ The `AppFactory` is a class that, for a given app spec, allows you to create and To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: ```python -from algokit_utils.clients import AlgorandClient - -# Create an Algorand client -algorand = AlgorandClient.from_environment() - # Minimal example factory = algorand.get_app_factory( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", ) # Advanced example factory = algorand.get_app_factory( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec - app_name="MyApp", - default_sender="SENDER_ADDRESS", - default_signer=signer, - version="1.0.0", - updatable=True, - deletable=True, - deploy_time_params={"TMPL_VALUE": "value"}, + app_spec=parsed_arc32_or_arc56_app_spec, + default_sender="SENDERADDRESS", + app_name="OverriddenAppName", + version="2.0.0", + compilation_params={ + "updatable": True, + "deletable": False, + "deploy_time_params": { "ONE": 1, "TWO": "value" }, + } ) ``` @@ -42,201 +38,125 @@ factory = algorand.get_app_factory( The `AppClient` is a class that, for a given app spec, allows you to manage calls and state for a specific deployed instance of an app (with a known app ID). -To get an instance of `AppClient` you can use `AlgorandClient` via `get_app_client_by_id` or use the factory methods: +To get an instance of `AppClient` you can use either `AlgorandClient` or instantiate it directly: ```python -from algokit_utils.clients import AlgorandClient - -# Create an Algorand client -algorand = AlgorandClient.from_environment() - -# Get client by ID -client = algorand.get_app_client_by_id( - app_spec=app_spec, # ARC-0032 or ARC-0056 app spec - app_id=existing_app_id, # Use 0 for new app - app_name="MyApp", # Optional: Name of the app - default_sender="SENDER_ADDRESS", # Optional: Default sender address - default_signer=signer, # Optional: Default signer for transactions - approval_source_map=approval_map, # Optional: Source map for approval program - clear_source_map=clear_map, # Optional: Source map for clear program +# Minimal examples +app_client = AppClient.from_creator_and_name( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + creator_address="CREATORADDRESS", + algorand=algorand, +) + +app_client = AppClient( + AppClientParams( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + app_id=12345, + algorand=algorand, + ) ) -# Get client by creator and name using factory -factory = algorand.get_app_factory(app_spec=app_spec) -client = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", - app_name="MyApp", - ignore_cache=False, # Optional: Whether to ignore app lookup cache +app_client = AppClient.from_network( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", + algorand=algorand, +) + +# Advanced example +app_client = AppClient( + AppClientParams( + app_spec=parsed_app_spec, + app_id=12345, + algorand=algorand, + app_name="OverriddenAppName", + default_sender="SENDERADDRESS", + approval_source_map=approval_teal_source_map, + clear_source_map=clear_teal_source_map, + ) ) ``` -You can get the `app_id` and `app_address` at any time as properties on the `AppClient` along with `app_name` and `app_spec`. +You can access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. ## Dynamically creating clients for a given app spec -As well as allowing you to control creation and deployment of apps, the `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. +The `AppFactory` allows you to conveniently create multiple `AppClient` instances on-the-fly with information pre-populated. This is possible via two methods on the app factory: -- `factory.get_app_client_by_id` - Returns a new `AppClient` client for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. -- `factory.get_app_client_by_creator_and_name` - Returns a new `AppClient` client, resolving the app by creator address and name using AlgoKit app deployment semantics (i.e. looking for the app creation transaction note). Automatically populates app_name, default_sender and source maps from the factory if not specified in the params. +- `factory.get_app_client_by_id(app_id, ...)` - Returns a new `AppClient` for an app instance of the given ID. Automatically populates app_name, default_sender and source maps from the factory if not specified. +- `factory.get_app_client_by_creator_and_name(creator_address, app_name, ...)` - Returns a new `AppClient`, resolving the app by creator address and name using AlgoKit app deployment semantics. Automatically populates app_name, default_sender and source maps from the factory if not specified. ```python -# Get clients by ID -client1 = factory.get_app_client_by_id(app_id=12345) -client2 = factory.get_app_client_by_id(app_id=12346) -client3 = factory.get_app_client_by_id( +app_client1 = factory.get_app_client_by_id(app_id=12345) +app_client2 = factory.get_app_client_by_id(app_id=12346) +app_client3 = factory.get_app_client_by_id( app_id=12345, - default_sender="SENDER2_ADDRESS" + default_sender="SENDER2ADDRESS" ) -# Get clients by creator and name -client4 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" ) -client5 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", - app_name="NonDefaultAppName", +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" ) -client6 = factory.get_app_client_by_creator_and_name( - creator_address="CREATOR_ADDRESS", +app_client6 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", app_name="NonDefaultAppName", ignore_cache=True, # Perform fresh indexer lookups - default_sender="SENDER2_ADDRESS", + default_sender="SENDER2ADDRESS" ) ``` ## Creating and deploying an app -Once you have an [app factory](#appfactory) you can perform the following actions: +Once you have an app factory you can perform the following actions: -- `factory.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app +- `factory.send.bare.create(params?)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app - `factory.deploy(params)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app ### Create The create method is a wrapper over the `app_create` (bare calls) and `app_create_method_call` (ABI method calls) methods, with the following differences: -- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec (noting you can override the `schema`) -- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) -- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control; these values can also be passed into the `AppFactory` constructor instead and if so will be used if not defined in the params to the create call +- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec +- `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control ```python # Use no-argument bare-call -create_response = factory.send.bare.create() +result, app_client = factory.send.bare.create() # Specify parameters for bare-call and override other parameters -create_response = factory.send.bare.create( - params=factory.params.bare.create( - args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptInOC, - deploy_time_params={ - "ONE": 1, - "TWO": "two", - }, - updatable=True, - deletable=False, - populate_app_call_resources=True, - ) -) - -## Or passing params directly -create_response = factory.send.bare.create( - AppFactoryCreateWithSendParams( +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptInOC, - deploy_time_params={ + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { "ONE": 1, "TWO": "two", }, - updatable=True, - deletable=False, - populate_app_call_resources=True, - ) + "updatable": True, + "deletable": False, + } ) # Specify parameters for ABI method call -create_response = factory.send.create( - params=factory.params.create( +result, app_client = factory.send.create( + AppClientMethodCallParams( method="create_application", - args=[1, "something"], + args=[1, "something"] ) ) ``` -If you want to construct a custom create call, you can get params objects: - -- `factory.params.create(params)` - ABI method create call for deploy method -- `factory.params.bare.create(params)` - Bare create call for deploy method - -### Deploy - -The deploy method is a wrapper over the `AppDeployer`'s `deploy` method, with the following differences: - -- You don't need to specify the `approval_program`, `clear_state_program`, or `schema` in the `create_params` because these are all specified or calculated from the app spec (noting you can override the `schema`) -- `sender` is optional for `create_params`, `update_params` and `delete_params` and if not specified then the `default_sender` from the `AppFactory` constructor is used (if it was specified, otherwise an error is thrown) -- You don't need to pass in `metadata` to the deploy params - it's calculated from: - - `updatable` and `deletable`, which you can optionally pass in directly to the method params - - `version` and `name`, which are optionally passed into the `AppFactory` constructor -- `deploy_time_params`, `updatable` and `deletable` can all be passed into the `AppFactory` and if so will be used if not defined in the params to the deploy call for deploy-time parameter replacements and deploy-time immutability and permanence control -- `create_params`, `update_params` and `delete_params` are optional, if they aren't specified then default values are used for everything and a no-argument bare call will be made for any create/update/delete calls -- If you want to call an ABI method for create/update/delete calls then you can pass in a string for `method`, which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature - -```python -# Use no-argument bare-calls to deploy with default behaviour -# for when update or schema break detected (fail the deployment) -client, response = factory.deploy({}) - -# Specify parameters for bare-calls and override the schema break behaviour -client, response = factory.deploy( - create_params=factory.params.bare.create( - args=[bytes([1, 2, 3, 4])], - on_complete=OnComplete.OptIn, - ), - update_params=factory.params.bare.deploy_update( - args=[bytes([1, 2, 3])], - ), - delete_params=factory.params.bare.deploy_delete( - args=[bytes([1, 2])], - ), - deploy_time_params={ - "ONE": 1, - "TWO": "two", - }, - on_update=OnUpdate.UpdateApp, - on_schema_break=OnSchemaBreak.ReplaceApp, - updatable=True, - deletable=True, -) - -# Specify parameters for ABI method calls -client, response = factory.deploy( - create_params=factory.params.create( - method="create_application", - args=[1, "something"], - ), - update_params=factory.params.deploy_update( - method="update", - ), - delete_params=factory.params.deploy_delete( - method="delete_app(uint64,uint64,uint64)uint64", - args=[1, 2, 3], - ), -) -``` - -If you want to construct a custom deploy call, you can get params objects for the `create_params`, `update_params` and `delete_params`: - -- `factory.params.create(params)` - ABI method create call for deploy method -- `factory.params.deploy_update(params)` - ABI method update call for deploy method -- `factory.params.deploy_delete(params)` - ABI method delete call for deploy method -- `factory.params.bare.create(params)` - Bare create call for deploy method -- `factory.params.bare.deploy_update(params)` - Bare update call for deploy method -- `factory.params.bare.deploy_delete(params)` - Bare delete call for deploy method - ## Updating and deleting an app -Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than [other calls](#calling-the-app), with the caveat that the update call is a bit different to the others since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. +Deploy method aside, the ability to make update and delete calls happens after there is an instance of an app so are done via `AppClient`. The semantics of this are no different than other calls, with the caveat that the update call is a bit different since the code will be compiled when constructing the update params and the update calls thus optionally takes compilation parameters (`deploy_time_params`, `updatable` and `deletable`) for deploy-time parameter replacements and deploy-time immutability and permanence control. ## Calling the app @@ -244,14 +164,14 @@ You can construct a params object, transaction(s) and sign and send a transactio This is done via the following properties: -- `client.params.{on_complete}(params)` - Params for an ABI method call -- `client.params.bare.{on_complete}(params)` - Params for a bare call -- `client.create_transaction.{on_complete}(params)` - Transaction(s) for an ABI method call -- `client.create_transaction.bare.{on_complete}(params)` - Transaction for a bare call -- `client.send.{on_complete}(params)` - Sign and send an ABI method call -- `client.send.bare.{on_complete}(params)` - Sign and send a bare call +- `app_client.params.{method}(params)` - Params for an ABI method call +- `app_client.params.bare.{method}(params)` - Params for a bare call +- `app_client.create_transaction.{method}(params)` - Transaction(s) for an ABI method call +- `app_client.create_transaction.bare.{method}(params)` - Transaction for a bare call +- `app_client.send.{method}(params)` - Sign and send an ABI method call +- `app_client.send.bare.{method}(params)` - Sign and send a bare call -To make one of these calls `{on_complete}` needs to be swapped with the [on complete action](https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#the-lifecycle-of-a-smart-contract) that should be made: +Where `{method}` is one of: - `update` - An update call - `opt_in` - An opt-in call @@ -260,115 +180,66 @@ To make one of these calls `{on_complete}` needs to be swapped with the [on comp - `close_out` - A close-out call - `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) -The input payload for all of these calls is the same as the underlying app methods with the caveat that the `app_id` is not passed in (since the `AppClient` already knows the app ID), `sender` is optional (it uses `default_sender` from the `AppClient` constructor if it was specified) and `method` (for ABI method calls) is a string rather than an `ABIMethod` object (which can either be the method name, or if you need to disambiguate between multiple methods of the same name it can be the ABI signature). - ```python -update_call = client.send.update( - params=client.params.update( +call1 = app_client.send.update( + AppClientMethodCallParams( method="update_abi", args=["string_io"], - deploy_time_params=deploy_time_params, - ) + ), + compilation_params={"deploy_time_params": deploy_time_params} ) -delete_call = client.send.delete( - params=client.params.delete( + +call2 = app_client.send.delete( + AppClientMethodCallParams( method="delete_abi", - args=["string_io"], - ) -) -opt_in_call = client.send.opt_in( - params=client.params.opt_in( - method="opt_in" + args=["string_io"] ) ) -clear_state_call = client.send.bare.clear_state() -transaction = client.create_transaction.bare.close_out( - params=client.params.bare.close_out( - args=[bytes([1, 2, 3])], - ) +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") ) -params = client.params.opt_in(method="optin") -``` - -### Nested ABI Method Call Transactions - -The ARC4 ABI specification supports ABI method calls as arguments to other ABI method calls, enabling some interesting use cases. While this conceptually resembles a function call hierarchy, in practice, the transactions are organized as a flat, ordered transaction group. Unfortunately, this logically hierarchical structure cannot always be correctly represented as a flat transaction group, making some scenarios impossible. - -To illustrate this, let's consider an example of two ABI methods with the following signatures: - -- `myMethod(pay,appl)void` -- `myOtherMethod(pay)void` - -These signatures are compatible, so `myOtherMethod` can be passed as an ABI method call argument to `myMethod`, which would look like: +call4 = app_client.send.bare.clear_state() -Hierarchical method call - -``` -myMethod(pay, myOtherMethod(pay)) -``` - -Flat transaction group - -``` -pay (pay) -appl (myOtherMethod) -appl (myMethod) -``` - -An important limitation to note is that the flat transaction group representation does not allow having two different pay transactions. This invariant is represented in the hierarchical call interface of the app client by passing `None` for the value. This acts as a placeholder and tells the app client that another ABI method call argument will supply the value for this argument. For example: - -```python -payment = client.algorand.create_transaction.payment( - sender="SENDER_ADDRESS", - receiver="RECEIVER_ADDRESS", - amount=1_000_000, # 1 Algo -) - -my_other_method_call = client.params.call( - method="myOtherMethod", - args=[payment], +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) ) -my_method_call = client.send.call( - params=client.params.call( - method="myMethod", - args=[None, my_other_method_call], - ) +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") ) ``` -`my_other_method_call` supplies the pay transaction to the transaction group and, by association, `my_other_method_call` has access to it as defined in its signature. -To ensure the app client builds the correct transaction group, you must supply a value for every argument in a method call signature. - ## Funding the app account -Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you `fund_app_account(params)`. +Often there is a need to fund an app account to cover minimum balance requirements for boxes and other scenarios. There is an app client method that will do this for you via `fund_app_account(params)`. The input parameters are: -- A `FundAppParams`, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). +- A `FundAppAccountParams` object, which has the same properties as a payment transaction except `receiver` is not required and `sender` is optional (if not specified then it will be set to the app client's default sender if configured). Note: If you are passing the funding payment in as an ABI argument so it can be validated by the ABI method then you'll want to get the funding call as a transaction, e.g.: ```python -result = client.send.call( - params=client.params.call( +result = app_client.send.call( + AppClientMethodCallParams( method="bootstrap", args=[ - client.create_transaction.fund_app_account( - params=client.params.fund_app_account( - amount=200_000, # microAlgos + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) ) - ), + ) ], - box_references=["Box1"], + box_references=["Box1"] ) ) ``` -You can also get the funding call as a params object via `client.params.fund_app_account(params)`. +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. ## Reading state @@ -376,52 +247,58 @@ You can also get the funding call as a params object via `client.params.fund_app ### App spec methods -The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the [generic methods](#generic-methods) give you. +The ARC-56 app spec can specify detailed information about the encoding format of state values and as such allows for a more advanced ability to automatically read state values and decode them as their high-level language types rather than the limited `int` / `bytes` / `str` ability that the generic methods give you. You can access this functionality via: -- `client.state.global_state.{method}()` - Global state -- `client.state.local_state(address).{method}()` - Local state -- `client.state.box.{method}()` - Box storage +- `app_client.state.global_state.{method}()` - Global state +- `app_client.state.local_state(address).{method}()` - Local state +- `app_client.state.box.{method}()` - Box storage Where `{method}` is one of: -- `get_all()` - Returns all single-key state values in a record keyed by the key name and the value a decoded ABI value. +- `get_all()` - Returns all single-key state values in a dict keyed by the key name and the value a decoded ABI value. - `get_value(name)` - Returns a single state value for the current app with the value a decoded ABI value. -- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be a `bytes` with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` -- `get_map(map_name)` - Returns all map values for the given map in a key=>value record. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. +- `get_map_value(map_name, key)` - Returns a single value from the given map for the current app with the value a decoded ABI value. Key can either be bytes with the binary value of the key value on-chain (without the map prefix) or the high level (decoded) value that will be encoded to bytes for the app spec specified `key_type` +- `get_map(map_name)` - Returns all map values for the given map in a key=>value dict. It's recommended that this is only done when you have a unique `prefix` for the map otherwise there's a high risk that incorrect values will be included in the map. ```python -values = client.state.global_state.get_all() -value = client.state.local_state("ADDRESS").get_value("value1") -map_value = client.state.box.get_map_value("map1", "mapKey") -map_values = client.state.global_state.get_map("myMap") +values = app_client.state.global_state.get_all() +value = app_client.state.local_state("ADDRESS").get_value("value1") +map_value = app_client.state.box.get_map_value("map1", "mapKey") +map_dict = app_client.state.global_state.get_map("myMap") ``` ### Generic methods There are various methods defined that let you read state from the smart contract app: -- `get_global_state()` - Gets the current global state -- `get_local_state(address: str)` - Gets the current local state for the given account address -- `get_box_names()` - Gets the current box names -- `get_box_value(name)` - Gets the current value of the given box -- `get_box_value_from_abi_type(name, abi_type)` - Gets the current value of the given box decoded using the specified ABI type -- `get_box_values(filter)` - Gets the current values of the boxes -- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes decoded using the specified ABI type +- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](todo_paste_url) +- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](todo_paste_url). +- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](todo_paste_url). +- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](todo_paste_url). +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](todo_paste_url). +- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](todo_paste_url). +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](todo_paste_url). ```python -global_state = client.get_global_state() -local_state = client.get_local_state("ACCOUNT_ADDRESS") - -box_name = "my-box" -box_name2 = "my-box2" - -box_names = client.get_box_names() -box_value = client.get_box_value(box_name) -box_values = client.get_box_values([box_name, box_name2]) -box_abi_value = client.get_box_value_from_abi_type(box_name, algosdk.ABIStringType()) -box_abi_values = client.get_box_values_from_abi_type([box_name, box_name2], algosdk.ABIStringType()) +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = "my-box" +box_name2: BoxReference = "my-box2" + +box_names = app_client.get_box_names() +box_value = app_client.get_box_value(box_name) +box_values = app_client.get_box_values([box_name, box_name2]) +box_abi_value = app_client.get_box_value_from_abi_type( + box_name, + algosdk.ABIStringType +) +box_abi_values = app_client.get_box_values_from_abi_type( + [box_name, box_name2], + algosdk.ABIStringType +) ``` ## Handling logic errors and diagnosing errors @@ -430,160 +307,38 @@ Often when calling a smart contract during development you will get logic errors When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the source map from compilation you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. +The information in that error message can be parsed and when combined with the [source map from compilation](todo_paste_url) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. -The app client and app factory automatically provide this functionality for all smart contract calls. When an error is thrown then the resulting error that is re-thrown will be a `LogicError` object, which has the following fields: +The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -- `logic_error_str: str` - The original error message -- `program: str` - The TEAL program -- `source_map: AlgoSourceMap | None` - The source map if available -- `transaction_id: str` - The transaction ID that triggered the error -- `message: str` - The error message -- `pc: int` - The program counter value -- `traces: list[SimulationTrace] | None` - Any traces that were included in the error -- `line_no: int | None` - The line number in the TEAL program that triggered the error -- `lines: list[str]` - The TEAL program split into lines +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](todo_paste_url), which has the following fields: + +- `message: str` - The formatted error message `{ERROR_MESSAGE}. at:{TEAL_LINE}. {ERROR_DESCRIPTION}` +- `stack: str` - A stack trace of the TEAL code showing where the error was with the 5 lines either side of it +- `led: LogicErrorDetails` - The parsed [logic error details](todo_paste_url) from the error message, with the following properties: + - `tx_id: str` - The transaction ID that triggered the error + - `pc: int` - The program counter + - `msg: str` - The raw error message + - `desc: str` - The full error description + - `traces: List[Dict[str, Any]]` - Any traces that were included in the error +- `program: List[str]` - The TEAL program split by line +- `teal_line: int` - The line number in the TEAL program that triggered the error Note: This information will only show if the app client / app factory has a source map. This will occur if: - You have called `create`, `update` or `deploy` - You have called `import_source_maps(source_maps)` and provided the source maps (which you can get by calling `export_source_maps()` after variously calling `create`, `update`, or `deploy` and it returns a serialisable value) -- You had source maps present in an app factory and then used it to create an app client (they are automatically passed through) +- You had source maps present in an app factory and then used it to [create an app client](todo_paste_url) (they are automatically passed through) -If you want to go a step further and automatically issue a simulated transaction and get trace information when there is an error when an ABI method is called you can turn on debug mode: +If you want to go a step further and automatically issue a [simulated transaction](https://algorand.github.io/js-algorand-sdk/classes/modelsv2.SimulateTransactionResult.html) and get trace information when there is an error when an ABI method is called you can turn on debug mode: ```python -from algokit_utils.config import config - -config.configure(debug=True) +Config.configure({"debug": True}) ``` If you do that then the exception will have the `traces` property within the underlying exception will have key information from the simulation within it and this will get populated into the `led.traces` property of the thrown error. -When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the project root is also configured. - -Example error handling: - -```python -from algokit_utils.config import config - -# Enable debug mode for detailed error information -config.configure(debug=True) - -try: - client.send.call( - params=client.params.call( - method="will_fail", - args=["test"] - ) - ) -except algokit_utils.LogicError as e: - print(f"Error at line {e.value.line_no}") # Access via value property - print(f"Error message: {e.value.message}") - print(f"Transaction ID: {e.value.transaction_id}") - print(e.value.trace()) # Shows TEAL execution trace with source mapping - - if e.value.traces: # Available when debug mode is active - for trace in e.value.traces: - print(f"PC: {trace['pc']}, Stack: {trace['stack']}") -``` - -## Best Practices - -1. Use typed ABI methods when possible for better type safety -2. Always handle potential logic errors with proper error handling -3. Use transaction composition for atomic operations -4. Leverage source maps and debug mode for development -5. Use idempotent deployment patterns with versioning -6. Properly manage box references to avoid transaction failures -7. Use template values for flexible application deployment -8. Implement proper state management with type safety -9. Use the client's parameter builders for type-safe transaction creation -10. Leverage the state accessor patterns for cleaner state management - -## Common Patterns - -### Idempotent Deployment - -```python -# Deploy with idempotency and version tracking -client, response = factory.deploy( - version="1.0.0", - deploy_time_params={"TMPL_VALUE": "value"}, - on_update=algokit_utils.OnUpdate.UpdateApp, - on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, - create_params=factory.params.create( - method="create", - args=["initial_value"], - ), -) - -if response.app.app_id != 0: - print(f"Deployed app ID: {response.app.app_id}") - if response.operation_performed == algokit_utils.OperationPerformed.Create: - print("New application deployed") - else: - print("Existing application found") -``` - -### Application State Migration - -```python -# Deploy with state migration -client, response = factory.deploy( - version="2.0.0", - on_schema_break=algokit_utils.OnSchemaBreak.ReplaceApp, - on_update=algokit_utils.OnUpdate.UpdateApp, - create_params=factory.params.create( - method="create", - args=["initial_value"], - schema={ - "global_ints": 1, - "global_byte_slices": 1, - "local_ints": 0, - "local_byte_slices": 0, - }, - ), -) - -if response.operation_performed == algokit_utils.OperationPerformed.Replace: - # Migrate state from old to new app - # Note: Migration logic should be implemented in the smart contract - client.send.call( - params=client.params.call( - method="migrate_state", - args=[response.old_app_id], - ) - ) -``` - -### Opt-in Management - -```python -# Create opt-in parameters -opt_in_params = client.params.opt_in( - method="initialize", # Optional: Method to call during opt-in - args=["initial_value"], # Optional: Arguments for initialization - boxes=[("user_data", "ACCOUNT_ADDRESS")], # Optional: Box allocation -) - -# Create and send opt-in transaction -transaction = client.create_transaction.opt_in(opt_in_params) -result = client.send.opt_in(opt_in_params) - -# Check if account is opted in -is_opted_in = client.is_opted_in("ACCOUNT_ADDRESS") - -# Create close-out parameters -close_out_params = client.params.close_out( - method="cleanup", # Optional: Method to call during close-out - args=["cleanup_value"], # Optional: Arguments for cleanup -) - -# Create and send close-out transaction -transaction = client.create_transaction.close_out(close_out_params) -result = client.send.close_out(close_out_params) -``` +When this debug flag is set, it will also emit debugging symbols to allow break-point debugging of the calls if the [project root is also configured](todo_paste_url). ## Default arguments diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index d041576e..cd95d4e7 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -8,8 +8,6 @@ TEAL template substitution. To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). -(design)= - ## Design The architecture design behind app deployment is articulated in an [architecture decision record](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md). @@ -46,8 +44,7 @@ Furthermore, the implementation contains the following implementation characteri ## Finding apps by creator -There is a method `algokit.get_creator_apps(creatorAccount, indexer)`, which performs a series of indexer lookups that return all apps created by the given creator. These are indexed by the name it -was deployed under if the creation transaction contained the following payload in the transaction note field: +The `AppDeployer.get_creator_apps_by_name()` method performs indexer lookups to find all apps created by an account that were deployed using this framework. The results are cached in an `ApplicationLookup` object. ``` ALGOKIT_DEPLOYER:j{name:string, version:string, updatable?:boolean, deletable?:boolean} @@ -61,39 +58,45 @@ fresh version. ## Deploying an application -The method that performs the deployment logic is the instance method `ApplicationClient.deploy`. It performs an idempotent (safely retryable) deployment. It will detect if the app already -exists and if it doesn't it will create it. If the app does already exist then it will: +The class that performs the deployment logic is `AppDeployer` with the `deploy` method. It performs an idempotent (safely retryable) deployment. It will detect if the app already exists and if it doesn't it will create it. If the app does already exist then it will: - Detect if the app has been updated (i.e. the logic has changed) and either fail or perform either an update or a replacement based on the deployment configuration. -- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the - deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than was originally requested) and either fail or perform a replacement based on the deployment configuration. It will automatically add metadata to the transaction note of the create or update calls that indicates the name, version, updatability and deletability of the contract. -This metadata works in concert with `get_creator_apps` to allow the app to be reliably retrieved against that creator in it's currently deployed state. `deploy` automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability. ### Input parameters -The following inputs are used when deploying an App +The `AppDeployParams` dataclass accepts these key parameters: + +- `metadata`: Required AppDeploymentMetaData containing name, version, deletable and updatable flags +- `deploy_time_params`: Optional TealTemplateParams for TEAL template substitution +- `on_schema_break`: Optional behavior for schema breaks - can be string literal "replace", "fail", "append" or OnSchemaBreak enum +- `on_update`: Optional behavior for updates - can be string literal "update", "replace", "fail", "append" or OnUpdate enum +- `create_params`: AppCreateParams or AppCreateMethodCallParams specifying app creation parameters +- `update_params`: AppUpdateParams or AppUpdateMethodCallParams specifying app update parameters +- `delete_params`: AppDeleteParams or AppDeleteMethodCallParams specifying app deletion parameters +- `existing_deployments`: Optional ApplicationLookup to cache and reduce indexer calls +- `ignore_cache`: When true, forces fresh indexer lookup even if creator apps are cached +- `max_fee`: Maximum microalgos to spend on any single transaction +- `send_params`: Additional transaction sending parameters (fee, signer, etc.) + +### Error Handling -- `version`: The version string for the app defined in app_spec, if not specified the version will automatically increment for existing apps that are updated, and set to 1.0 for new apps -- `signer`, `sender`: Optional signer and sender for deployment operations, sender must be the same as the creator specified -- `allow_update`, `allow_delete`: Control the updatability and deletability of the app, used to populate `TMPL_UPDATABLE` and `TMPL_DELETABLE` template values -- `on_update`: Determines what should happen if an update to the smart contract is detected (e.g. the TEAL code has changed since last deployment) -- `on_schema_break`: Determines what should happen if a breaking change to the schema is detected (e.g. if you need more global or local state that was previously requested when the contract was originally created) -- `create_args`: Args to use if a create operation is performed -- `update_args`: Args to use if an update operation is performed -- `delete_args`: Args to use if a delete operation is performed -- `template_values`: Values to use for automatic substitution of [deploy-time parameter values](#design) is mapping of `key: value` that will result in `TMPL_{key}` being replaced with `value` +Specific error cases that throw ValueError: + +- Schema break with on_schema_break=fail +- Update attempt on non-updatable app +- Replacement attempt on non-deletable app +- Invalid `existing_deployments` cache provided ### Idempotency `deploy` is idempotent which means you can safely call it again multiple times, and it will only apply any changes it detects. If you call it again straight after calling it then it will do nothing. This also means it can be used to find an existing app based on the supplied creator and app_spec or name. -(compilation-and-template-substitution)= - ### Compilation and template substitution When compiling TEAL template code, the capabilities described in the [design above](#design) are present, namely the ability to supply deploy-time parameters and the ability to control immutability and permanence of the smart contract at deploy-time. @@ -104,18 +107,14 @@ In order for a smart contract to be able to use this functionality, it must have - `TMPL_UPDATABLE` - Which will be replaced with a `1` if an app should be updatable and `0` if it shouldn't (immutable) - `TMPL_DELETABLE` - Which will be replaced with a `1` if an app should be deletable and `0` if it shouldn't (permanent) -If you are building a smart contract using the [beaker_production AlgoKit template](https://github.com/algorandfoundation/algokit-beaker-default-template) if provides a reference implementation out of the box for the deploy-time immutability and permanence control. +If you are building a smart contract using the [Python AlgoKit template](https://github.com/algorandfoundation/algokit-python-template) it provides a reference implementation out of the box for the deploy-time immutability and permanence control. ### Return value -`deploy` returns a `DeployResponse` object, that describes the action taken. - -- `action_taken`: Describes what happened during deployment - - `Create` - The smart contract app is created. - - `Update` - The smart contract app is updated - - `Replace` - The smart contract app was deleted and created again (in an atomic transaction) - - `Nothing` - Nothing was done since an existing up-to-date app was found -- `create_response`: If action taken was `Create` or `Replace`, the result of the create transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `update_response`: If action taken was `Update`, the result of the update transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `delete_response`: If action taken was `Replace`, the result of the delete transaction. Can be a `TransactionResponse` or `ABITransactionResponse` depending on the method used -- `app`: An `AppMetaData` object, describing the final app state +`deploy` returns an `AppDeployResult` object containing: + +- `operation_performed`: Enum indicating action taken (Create/Update/Replace/Nothing) +- `app`: ApplicationMetaData with final app state +- `create_result`: Transaction result if creation occurred +- `update_result`: Transaction result if update occurred +- `delete_result`: Transaction result if replacement occurred diff --git a/docs/source/capabilities/app-manager.md b/docs/source/capabilities/app-manager.md index 3fcfd4fa..55d30b17 100644 --- a/docs/source/capabilities/app-manager.md +++ b/docs/source/capabilities/app-manager.md @@ -38,10 +38,13 @@ compilation_result = app_manager.compile_teal_template( template_params=template_params ) -# Compile with deployment metadata (for updatable/deletable control) +# Compile with deployment control (updatable/deletable) +control_template = f"""#pragma version 8 +int {UPDATABLE_TEMPLATE_NAME} +int {DELETABLE_TEMPLATE_NAME}""" deployment_metadata = {"updatable": True, "deletable": True} compilation_result = app_manager.compile_teal_template( - template_code, + control_template, deployment_metadata=deployment_metadata ) ``` @@ -134,7 +137,7 @@ box_ref = b"my_box" box_ref = account_signer # Box reference with app ID -box_ref = BoxReference(app_id=123, name="my_box") +box_ref = BoxReference(app_id=123, name=b"my_box") ``` ## Common app parameters diff --git a/docs/source/capabilities/client.md b/docs/source/capabilities/client.md index 08614682..206c3c47 100644 --- a/docs/source/capabilities/client.md +++ b/docs/source/capabilities/client.md @@ -14,15 +14,23 @@ To get an instance of `ClientManager` you can instantiate it directly: ```python from algokit_utils import ClientManager - -# Algod client only -client_manager = ClientManager(algod=algod_client) -# All clients -client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client) -# Algod config only -client_manager = ClientManager(algod_config=algod_config) -# All client configs -client_manager = ClientManager(algod_config=algod_config, indexer_config=indexer_config, kmd_config=kmd_config) +from algosdk.v2client.algod import AlgodClient + +algod_client = AlgodClient(...) +algorand_client = ... # Get AlgorandClient instance from somewhere + +# Using existing client instances +client_manager = ClientManager( + {"algod": algod_client, "indexer": indexer_client, "kmd": kmd_client}, + algorand_client=algorand_client +) + +# Using configs +algod_config = {"server": "https://..."} +client_manager = ClientManager( + {"algod_config": algod_config}, + algorand_client=algorand_client +) ``` ## Network configuration diff --git a/docs/source/capabilities/debugger.md b/docs/source/capabilities/debugger.md index a6274393..948fed17 100644 --- a/docs/source/capabilities/debugger.md +++ b/docs/source/capabilities/debugger.md @@ -48,24 +48,29 @@ config.configure( When debug mode is enabled, AlgoKit Utils will automatically: -- `simulate_and_persist_response`: This method simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, and persists the simulation response to an AVM Debugger compliant JSON file. It takes an `AtomicTransactionComposer` object representing the atomic transactions to be simulated and persisted, a `Path` object representing the root directory of the project, an `AlgodClient` object representing the Algorand client, and a float representing the size of the trace buffer in megabytes. - -The following methods are provided for scenarios where you want to manually persist sourcemaps and traces: - -- `persist_sourcemaps`: This method persists the sourcemaps for the - given sources as AVM Debugger compliant artifacts. It takes a list of - `PersistSourceMapInput` objects, a `Path` object representing the root - directory of the project, an `AlgodClient` object for interacting with the - Algorand blockchain, and a boolean indicating whether to dump teal source - files along with sourcemaps. -- `simulate_and_persist_response`: This method simulates the atomic - transactions using the provided `AtomicTransactionComposer` object and - `AlgodClient` object, and persists the simulation response to an AVM - Debugger compliant JSON file. It takes an `AtomicTransactionComposer` - object representing the atomic transactions to be simulated and persisted, - a `Path` object representing the root directory of the project, an - `AlgodClient` object representing the Algorand client, and a float - representing the size of the trace buffer in megabytes. +- Generate transaction traces compatible with the AVM Debugger +- Manage trace file storage with automatic cleanup +- Provide source map generation for TEAL contracts + +The following methods are provided for manual debugging operations: + +- `persist_sourcemaps`: Persists sourcemaps for given TEAL contracts as AVM Debugger-compliant artifacts. Parameters: + + - `sources`: List of TEAL sources to generate sourcemaps for + - `project_root`: Project root directory for storage + - `client`: AlgodClient instance + - `with_sources`: Whether to include TEAL source files (default: True) + +- `simulate_and_persist_response`: Simulates transactions and persists debug traces. Parameters: + - `atc`: AtomicTransactionComposer containing transactions + - `project_root`: Project root directory for storage + - `algod_client`: AlgodClient instance + - `buffer_size_mb`: Maximum trace storage in MB (default: 256) + - `allow_empty_signatures`: Allow unsigned transactions (default: True) + - `allow_unnamed_resources`: Allow unnamed resources (default: True) + - `extra_opcode_budget`: Additional opcode budget + - `exec_trace_config`: Custom trace configuration + - `simulation_round`: Specific round to simulate ### Trace filename format diff --git a/docs/source/capabilities/transaction.md b/docs/source/capabilities/transaction.md index 34f3c45d..e10a8e7b 100644 --- a/docs/source/capabilities/transaction.md +++ b/docs/source/capabilities/transaction.md @@ -16,11 +16,11 @@ class SendSingleTransactionResult: transaction: TransactionWrapper # Last transaction confirmation: AlgodResponseType # Last confirmation group_id: str - tx_id: str | None = None - tx_ids: list[str] # Full array of transaction IDs + tx_id: str | None = None # Transaction ID of the last transaction + tx_ids: list[str] # All transaction IDs in the group transactions: list[TransactionWrapper] confirmations: list[AlgodResponseType] - returns: list[ABIReturn] | None = None + returns: list[ABIReturn] | None = None # ABI returns if applicable ``` Common variations include: @@ -72,9 +72,9 @@ Different interfaces return different result types: - `.send.payment()` → `SendSingleTransactionResult` - `.send.asset_create()` → `SendSingleAssetCreateTransactionResult` - - `.send.app_call()` → `SendAppTransactionResult` - - `.send.app_create()` → `SendAppCreateTransactionResult` - - `.send.app_update()` → `SendAppUpdateTransactionResult` + - `.send.app_call()` → `SendAppTransactionResult` (contains raw ABI return) + - `.send.app_create()` → `SendAppCreateTransactionResult` (with app ID/address) + - `.send.app_update()` → `SendAppUpdateTransactionResult` (with compilation info) 3. **AppClient Methods** diff --git a/docs/source/capabilities/transfer.md b/docs/source/capabilities/transfer.md index 0e965981..f2299170 100644 --- a/docs/source/capabilities/transfer.md +++ b/docs/source/capabilities/transfer.md @@ -16,7 +16,7 @@ The base type for specifying a payment transaction is `PaymentParams`, which has ```python # Minimal example -result = algod.send.payment( +result = algorand_client.send.payment( PaymentParams( sender="SENDERADDRESS", receiver="RECEIVERADDRESS", @@ -25,7 +25,7 @@ result = algod.send.payment( ) # Advanced example -result2 = algod.send.payment( +result2 = algorand_client.send.payment( PaymentParams( sender="SENDERADDRESS", receiver="RECEIVERADDRESS", @@ -33,7 +33,7 @@ result2 = algod.send.payment( close_remainder_to="CLOSEREMAINDERTOADDRESS", lease="lease", note=b"note", - # Use this with caution, it's generally better to use algod.account.rekey_account + # Use this with caution, it's generally better to use algorand_client.account.rekey_account rekey_to="REKEYTOADDRESS", # You wouldn't normally set this field first_valid_round=1000, @@ -59,12 +59,12 @@ The `ensure_funded` function automatically funds an account to maintain a minimu There are 3 variants of this function: -- `algod.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). -- `algod.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded(account_to_fund, dispenser_account, min_spending_balance, options?)` - Funds a given account using a dispenser account as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_environment(account_to_fund, min_spending_balance, options?)` - Funds a given account using a dispenser account retrieved from the environment, per the `dispenser_from_environment` method, as a funding source such that the given account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). - **Note:** requires environment variables to be set. - The dispenser account is retrieved from the account mnemonic stored in `DISPENSER_MNEMONIC` and optionally `DISPENSER_SENDER` if it's a rekeyed account, or against default LocalNet if no environment variables present. -- `algod.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). +- `algorand_client.account.ensure_funded_from_testnet_dispenser_api(account_to_fund, dispenser_client, min_spending_balance, options)` - Funds a given account using the [TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md) as a funding source such that the account has a certain amount of Algo free to spend (accounting for Algo locked in minimum balance requirement). The general structure of these calls is similar, they all take: @@ -85,9 +85,9 @@ The general structure of these calls is similar, they all take: # From account # Basic example -algod.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) +algorand_client.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo")) # With configuration -algod.account.ensure_funded( +algorand_client.account.ensure_funded( "ACCOUNTADDRESS", "DISPENSERADDRESS", AlgoAmount(1, "algo"), @@ -99,9 +99,9 @@ algod.account.ensure_funded( # From environment # Basic example -algod.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) +algorand_client.account.ensure_funded_from_environment("ACCOUNTADDRESS", AlgoAmount(1, "algo")) # With configuration -algod.account.ensure_funded_from_environment( +algorand_client.account.ensure_funded_from_environment( "ACCOUNTADDRESS", AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), @@ -112,15 +112,15 @@ algod.account.ensure_funded_from_environment( # TestNet Dispenser API # Basic example -algod.account.ensure_funded_from_testnet_dispenser_api( +algorand_client.account.ensure_funded_from_testnet_dispenser_api( "ACCOUNTADDRESS", - algod.client.get_testnet_dispenser_from_environment(), + algorand_client.client.get_testnet_dispenser_from_environment(), AlgoAmount(1, "algo") ) # With configuration -algod.account.ensure_funded_from_testnet_dispenser_api( +algorand_client.account.ensure_funded_from_testnet_dispenser_api( "ACCOUNTADDRESS", - algod.client.get_testnet_dispenser_from_environment(), + algorand_client.client.get_testnet_dispenser_from_environment(), AlgoAmount(1, "algo"), min_funding_increment=AlgoAmount(2, "algo"), )