diff --git a/_ARCHIVE/app-client.md b/_ARCHIVE/app-client.md new file mode 100644 index 00000000..b6ebc12c --- /dev/null +++ b/_ARCHIVE/app-client.md @@ -0,0 +1,359 @@ +--- +title: "App client and App factory" +description: "> [!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." +--- + +> [!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/) and [App management](../app/). 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 + +## `AppFactory` + +The `AppFactory` is a class that, for a given app spec, allows you to create and deploy one or more app instances and to create one or more app clients to interact with those (or other) app instances. + +To get an instance of `AppFactory` you can use `AlgorandClient` via `algorand.get_app_factory`: + +```python +# Minimal example +factory = algorand.get_app_factory( + app_spec="{/* ARC-56 or ARC-32 compatible JSON */}", +) + +# Advanced example +factory = algorand.get_app_factory( + 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" }, + } +) +``` + +## `AppClient` + +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 either `AlgorandClient` or instantiate it directly: + +```python +# 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, + ) +) + +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 access `app_id`, `app_address`, `app_name` and `app_spec` as properties on the `AppClient`. + +## Dynamically creating clients for a given app spec + +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(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 +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="SENDER2ADDRESS" +) + +app_client4 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS" +) +app_client5 = factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="NonDefaultAppName" +) +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="SENDER2ADDRESS" +) +``` + +## Creating and deploying an app + +Once you have an app factory you can perform the following actions: + +- `factory.send.bare.create(...)` - 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(...)` - 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. + +> See API docs for details on parameter signatures. + +### 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 +- `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. Note these are consolidated under the `compilation_params` `TypedDict`, see API docs for details. + +```python +# Use no-argument bare-call +result, app_client = factory.send.bare.create() + +# Specify parameters for bare-call and override other parameters +result, app_client = factory.send.bare.create( + params=AppClientBareCallParams( + args=[bytes([1, 2, 3, 4])], + static_fee=AlgoAmount.from_microalgos(3000), + on_complete=OnComplete.OptIn, + ), + compilation_params={ + "deploy_time_params": { + "ONE": 1, + "TWO": "two", + }, + "updatable": True, + "deletable": False, + } +) + +# Specify parameters for ABI method call +result, app_client = factory.send.create( + AppClientMethodCallParams( + method="create_application", + args=[1, "something"] + ) +) +``` + +## 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 created 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 (`compilation_params`) for deploy-time parameter replacements and deploy-time immutability and permanence control. + +## Calling the app + +You can construct a params object, transaction(s) and sign and send a transaction to call the app that a given `AppClient` instance is pointing to. + +This is done via the following properties: + +- `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 + +Where `{method}` is one of: + +- `update` - An update call +- `opt_in` - An opt-in call +- `delete` - A delete application call +- `clear_state` - A clear state call (note: calls the clear program and only applies to bare calls) +- `close_out` - A close-out call +- `call` - A no-op call (or other call if `on_complete` is specified to anything other than update) + +```python +call1 = app_client.send.update( + AppClientMethodCallParams( + method="update_abi", + args=["string_io"], + ), + compilation_params={"deploy_time_params": deploy_time_params} +) + +call2 = app_client.send.delete( + AppClientMethodCallParams( + method="delete_abi", + args=["string_io"] + ) +) + +call3 = app_client.send.opt_in( + AppClientMethodCallParams(method="opt_in") +) + +call4 = app_client.send.bare.clear_state() + +transaction = app_client.create_transaction.bare.close_out( + AppClientBareCallParams( + args=[bytes([1, 2, 3])] + ) +) + +params = app_client.params.opt_in( + AppClientMethodCallParams(method="optin") +) +``` + +## 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 via `fund_app_account(params)`. + +The input parameters are: + +- 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 = app_client.send.call( + AppClientMethodCallParams( + method="bootstrap", + args=[ + app_client.create_transaction.fund_app_account( + FundAppAccountParams( + amount=AlgoAmount.from_microalgos(200_000) + ) + ) + ], + box_references=["Box1"] + ) +) +``` + +You can also get the funding call as a params object via `app_client.params.fund_app_account(params)`. + +## Reading state + +`AppClient` has a number of mechanisms to read state (global, local and box storage) from the app instance. + +### 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 give you. + +You can access this functionality via: + +- `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 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 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 = 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 using `algorand.app.get_global_state`. +- `get_local_state(address: str)` - Gets the current local state for the given account address using `algorand.app.get_local_state`. +- `get_box_names()` - Gets the current box names using `algorand.app.get_box_names`. +- `get_box_value(name)` - Gets the current value of the given box using `algorand.app.get_box_value`. +- `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`. +- `get_box_values(filter)` - Gets the current values of the boxes using `algorand.app.get_box_values`. +- `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`. + +```python +global_state = app_client.get_global_state() +local_state = app_client.get_local_state("ACCOUNTADDRESS") + +box_name: BoxReference = BoxReference(app_id=app_client.app_id, name="my-box") +box_name2: BoxReference = BoxReference(app_id=app_client.app_id, name="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 + +Often when calling a smart contract during development you will get logic errors that cause an exception to throw. This may be because of a failing assertion, a lack of fees, exhaustion of opcode budget, or any number of other reasons. + +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](../app-deploy/#compilation-and-template-substitution) 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 through an automatically registered error transformer. This error transformer: + +- Parses logic errors from blockchain responses +- Applies source map information when available to provide line numbers and context +- Filters errors to only handle those relevant to the specific application +- For new applications (app_id=0), compares program bytecode to ensure error handling is applied to the correct application instance + +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)`. + +For more information about error transformers and how to create custom ones, see the [Transaction Composer Error Transformers](../../advanced/transaction-composer/#error-transformers) documentation. + +When an error is thrown then the resulting error that is re-thrown will be a `LogicError`, which has the following fields: + +- `logic_error: Exception` - The original logic error exception +- `logic_error_str: str` - The string representation of the logic error +- `program: str` - The TEAL program source code +- `source_map: AlgoSourceMap | None` - The source map if available +- `transaction_id: str` - The transaction ID that triggered the error +- `message: str` - Combined error message with debugging information +- `pc: int` - The program counter value where error occurred +- `traces: list[SimulationTrace] | None` - Simulation traces if debug enabled +- `line_no: int | None` - The line number in the TEAL source code +- `lines: list[str]` - The TEAL program split into individual lines + +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](#dynamically-creating-clients-for-a-given-app-spec) (they are automatically passed through) + +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 +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](../../advanced/debugging/). + +## Default arguments + +If an ABI method call specifies default argument values for any of its arguments you can pass in `None` for the value of that argument for the default value to be automatically populated. diff --git a/_ARCHIVE/app-deploy.md b/_ARCHIVE/app-deploy.md new file mode 100644 index 00000000..063b27bd --- /dev/null +++ b/_ARCHIVE/app-deploy.md @@ -0,0 +1,261 @@ +--- +title: "App deployment" +description: "AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet)." +--- + +AlgoKit contains advanced smart contract deployment capabilities that allow you to have idempotent (safely retryable) deployment of a named app, including deploy-time immutability and permanence control and TEAL template substitution. This allows you to control the smart contract development lifecycle of a single-instance app across multiple environments (e.g. LocalNet, TestNet, MainNet). + +It's optional to use this functionality, since you can construct your own deployment logic using create / update / delete calls and your own mechanism to maintaining app metadata (like app IDs etc.), but this capability is an opinionated out-of-the-box solution that takes care of the heavy lifting for you. + +App deployment is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities, particularly [App management](../app/). + +To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_deploy_scenarios.py). + +## Smart contract development lifecycle + +The design behind the deployment capability is unique. 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). While the implementation will naturally evolve over time and diverge from this record, the principles and design goals behind the design are comprehensively explained. + +Namely, it described the concept of a smart contract development lifecycle: + +1. Development + 1. **Write** smart contracts + 2. **Transpile** smart contracts with development-time parameters (code configuration) to TEAL Templates + 3. **Verify** the TEAL Templates maintain [output stability](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/articles/output_stability.md) and any other static code quality checks +2. Deployment + 1. **Substitute** deploy-time parameters into TEAL Templates to create final TEAL code + 2. **Compile** the TEAL to create byte code using algod + 3. **Deploy** the byte code to one or more Algorand networks (e.g. LocalNet, TestNet, MainNet) to create Deployed Application(s) +3. Runtime + 1. **Validate** the deployed app via automated testing of the smart contracts to provide confidence in their correctness + 2. **Call** deployed smart contract with runtime parameters to utilise it + +![App deployment lifecycle](/algokit-utils-py/images/lifecycle.jpg) + +The App deployment capability provided by AlgoKit Utils helps implement **#2 Deployment**. + +Furthermore, the implementation contains the following implementation characteristics per the original architecture design: + +- Deploy-time parameters can be provided and substituted into a TEAL Template by convention (by replacing `TMPL_{KEY}`) +- Contracts can be built by any smart contract framework that supports [ARC-56](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md) and [ARC-32](https://github.com/algorandfoundation/ARCs/pull/150), which also means the deployment language can be different to the development language e.g. you can deploy a Python smart contract with TypeScript for instance +- There is explicit control of the immutability (updatability / upgradeability) and permanence (deletability) of the smart contract, which can be varied per environment to allow for easier development and testing in non-MainNet environments (by replacing `TMPL_UPDATABLE` and `TMPL_DELETABLE` at deploy-time by convention, if present) +- Contracts are resolvable by a string "name" for a given creator to allow automated determination of whether that contract had been deployed previously or not, but can also be resolved by ID instead + +This design allows you to have the same deployment code across environments without having to specify an ID for each environment. This makes it really easy to apply [continuous delivery](https://continuousdelivery.com/) practices to your smart contract deployment and make the deployment process completely automated. + +## `AppDeployer` + +The `AppDeployer` is a class that is used to manage app deployments and deployment metadata. + +To get an instance of `AppDeployer` you can use either [`AlgorandClient`](../../core/algorand-client/) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](../app/#appmanager), [`AlgorandClientTransactionSender`](../../core/algorand-client/#sending-a-single-transaction) and optionally an indexer client instance): + +```python +from algokit_utils.app_deployer import AppDeployer + +app_deployer = AppDeployer(app_manager, transaction_sender, indexer) +``` + +## Deployment metadata + +When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. + +The deployment metadata is defined in `AppDeployMetadata`, which is an object with: + +- `name: str` - The unique name identifier of the app within the creator account +- `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) +- `deletable: bool | None` - Whether or not the app is deletable (`true`) / permanent (`false`) / unspecified (`None`) +- `updatable: bool | None` - Whether or not the app is updatable (`true`) / immutable (`false`) / unspecified (`None`) + +An example of the ARC-2 transaction note that is attached as an app creation / update transaction note to specify this metadata is: + +``` +ALGOKIT_DEPLOYER:j{name:"MyApp",version:"1.0",updatable:true,deletable:false} +``` + +> NOTE: Starting from v3.0.0, AlgoKit Utils no longer automatically increments the contract version by default. It is the user's responsibility to explicitly manage versioning of their smart contracts (if desired). + +## Lookup deployed apps by name + +In order to resolve what apps have been previously deployed and their metadata, AlgoKit provides a method that does a series of indexer lookups and returns a map of name to app metadata via `get_creator_apps_by_name(creator_address)`. + +```python +app_lookup = algorand.app_deployer.get_creator_apps_by_name("CREATORADDRESS") +app1_metadata = app_lookup.apps["app1"] +``` + +This method caches the result of the lookup, since it's a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. + +The return type of `get_creator_apps_by_name` is `ApplicationLookup`, which is an object with: + +```python +@dataclasses.dataclass +class ApplicationLookup: + creator: str + apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) +``` + +The `apps` property contains a lookup by app name that resolves to the current `ApplicationMetaData`. + +> Refer to the `ApplicationLookup` for latest information on exact types. + +## Performing a deployment + +In order to perform a deployment, AlgoKit provides the `deploy` method. + +For example: + +```python +deployment_result = algorand.app_deployer.deploy( + AppDeployParams( + metadata=AppDeploymentMetaData( + name="MyApp", + version="1.0.0", + deletable=False, + updatable=False, + ), + create_params=AppCreateParams( + sender="CREATORADDRESS", + approval_program=approval_teal_template_or_byte_code, + clear_state_program=clear_state_teal_template_or_byte_code, + schema=StateSchema( + global_ints=1, + global_byte_slices=2, + local_ints=3, + local_byte_slices=4, + ), + # Other parameters if a create call is made... + ), + update_params=AppUpdateParams( + sender="SENDERADDRESS", + # Other parameters if an update call is made... + ), + delete_params=AppDeleteParams( + sender="SENDERADDRESS", + # Other parameters if a delete call is made... + ), + deploy_time_params={ + "VALUE": 1, # TEAL template variables to replace + }, + on_schema_break=OnSchemaBreak.Append, + on_update=OnUpdate.Update, + send_params=SendParams( + populate_app_call_resources=True, + # Other execution control parameters + ), + ) +) +``` + +This method 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 program logic has changed) and either fail, perform an update, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. +- Detect if the app has a breaking schema change (i.e. more global or local storage is needed than were originally requested) and either fail, deploy a new version or perform a replacement (delete old app and create new app) based on the deployment configuration. + +It will automatically [add metadata to the transaction note of the create or update transactions](#deployment-metadata) that indicates the name, version, updatability and deletability of the contract. This metadata works in concert with [`appDeployer.get_creator_apps_by_name`](#lookup-deployed-apps-by-name) to allow the app to be reliably retrieved against that creator in it's currently deployed state. It will automatically update it's lookup cache so subsequent calls to `get_creator_apps_by_name` or `deploy` will use the latest metadata without needing to call indexer again. + +`deploy` also automatically executes [template substitution](#compilation-and-template-substitution) including deploy-time control of permanence and immutability if the requisite template parameters are specified in the provided TEAL template. + +### Input parameters + +The first parameter `deployment` is an `AppDeployParams`, which is an object with: + +- `metadata: AppDeployMetadata` - determines the [deployment metadata](#deployment-metadata) of the deployment +- `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](../app/) (raw parameters or ABI method call) +- `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](../app/) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic +- `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](../app/) (raw parameters or ABI method call) without the `app_id` parameter +- `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution](#compilation-and-template-substitution) + - `TealTemplateParams` is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines `OnSchemaBreak` if schema requirements increase (values: 'replace', 'fail', 'append') +- `on_update: OnUpdate | str | None` - determines `OnUpdate` if contract logic changes (values: 'update', 'replace', 'fail', 'append') +- `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries +- `ignore_cache: bool | None` - if True, bypasses cached deployment metadata +- Additional fields from `SendParams` - transaction execution parameters + +### 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. + +### Compilation and template substitution + +When compiling TEAL template code, the capabilities described in the [above design](#smart-contract-development-lifecycle) 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. + +In order for a smart contract to opt-in to use this functionality, it must have a TEAL Template that contains the following: + +- `TMPL_{key}` - Which can be replaced with a number or a string / byte array which will be automatically hexadecimal encoded (for any number of `{key}` => `{value}` pairs) +- `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 passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the `CompiledTeal` of substituting then compiling the TEAL template(s) in the following properties of the return value: + +- `compiled_approval: CompiledTeal | None` +- `compiled_clear: CompiledTeal | None` + +Template substitution is done by executing `algorand.app.compile_teal_template(teal_template_code, template_params, deployment_metadata)`, which in turn calls the following in order and returns the compilation result per above (all of which can also be invoked directly): + +- `AppManager.strip_teal_comments(teal_code)` - Strips out any TEAL comments to reduce the payload that is sent to algod and reduce the likelihood of hitting the max payload limit +- `AppManager.replace_template_variables(teal_template_code, template_values)` - Replaces the template variables by looking for `TMPL_{key}` +- `AppManager.replace_teal_template_deploy_time_control_params(teal_template_code, params)` - If `params` is provided, it allows for deploy-time immutability and permanence control by replacing `TMPL_UPDATABLE` with `params.get("updatable")` if not `None` and replacing `TMPL_DELETABLE` with `params.get("deletable")` if not `None` +- `algorand.app.compile_teal(teal_code)` - Sends the final TEAL to algod for compilation and returns the result including the source map and caches the compilation result within the `AppManager` instance + +#### Making updatable/deletable apps + +Below is a sample in [Algorand Python SDK](https://github.com/algorandfoundation/puya) that demonstrates how to make an app updatable/deletable smart contract with the use of `TMPL_UPDATABLE` and `TMPL_DELETABLE` template parameters. + +```python +# ... your contract code ... +@arc4.baremethod(allow_actions=["UpdateApplication"]) +def update(self) -> None: + assert TemplateVar[bool]("UPDATABLE") + +@arc4.baremethod(allow_actions=["DeleteApplication"]) +def delete(self) -> None: + assert TemplateVar[bool]("DELETABLE") +# ... your contract code ... +``` + +Alternative example in [Algorand TypeScript SDK](https://github.com/algorandfoundation/puya-ts): + +```typescript +// ... your contract code ... +@baremethod({ allowActions: 'UpdateApplication' }) +public onUpdate() { + assert(TemplateVar('UPDATABLE')) +} + +@baremethod({ allowActions: 'DeleteApplication' }) +public onDelete() { + assert(TemplateVar('DELETABLE')) +} +// ... your contract code ... +``` + +With the above code, when deploying your application, you can pass in the following deploy-time parameters: + +```python +my_factory.deploy( + ... # other deployment parameters ... + compilation_params={ + "updatable": True, # resulting app will be updatable, and this metadata will be set in the ARC-2 transaction note + "deletable": False, # resulting app will not be deletable, and this metadata will be set in the ARC-2 transaction note + } +) +``` + +### Return value + +When `deploy` executes it will return a `AppDeployResult` object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. + +The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): + +- `OperationPerformed.CREATE` - The smart contract app was created +- `OperationPerformed.UPDATE` - The smart contract app was updated +- `OperationPerformed.REPLACE` - The smart contract app was deleted and created again (in an atomic transaction) +- `OperationPerformed.NOTHING` - Nothing was done since it was detected the existing smart contract app deployment was up to date + +As well as the `operation_performed` parameter and the [optional compilation result](#compilation-and-template-substitution), the return value will have the `ApplicationMetaData` [fields](#deployment-metadata) present. + +Based on the value of `operation_performed`, there will be other data available in the return value: + +- If `CREATE`, `UPDATE` or `REPLACE` then it will have the relevant `SendAppTransactionResult` values: + - `create_result` for create operations + - `update_result` for update operations +- If `REPLACE` then it will also have `delete_result` to capture the result of deleting the existing app diff --git a/_ARCHIVE/app.md b/_ARCHIVE/app.md new file mode 100644 index 00000000..d129fea7 --- /dev/null +++ b/_ARCHIVE/app.md @@ -0,0 +1,166 @@ +--- +title: "App management" +description: "App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes)." +--- + +App management is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to create, update, delete, call (ABI and otherwise) smart contract apps and the metadata associated with them (including state and boxes). + +## `AppManager` + +The `AppManager` is a class that is used to manage app information. To get an instance of `AppManager` you can use either [`AlgorandClient`](../../core/algorand-client/) via `algorand.app` or instantiate it directly (passing in an algod client instance): + +```python +from algokit_utils import AppManager + +app_manager = AppManager(algod_client) +``` + +## Calling apps + +### App Clients + +The recommended way of interacting with apps is via [App clients](../app-client/) and [App factory](../app-client/#appfactory). The methods shown on this page are the underlying mechanisms that app clients use and are for advanced use cases when you want more control. + +### Compilation + +The `AppManager` class allows you to compile TEAL code with caching semantics that allows you to avoid duplicate compilation and keep track of source maps from compiled code. + +```python +# Basic compilation +teal_code = "return 1" +compilation_result = app_manager.compile_teal(teal_code) + +# Get cached compilation result +cached_result = app_manager.get_compilation_result(teal_code) + +# Compile with template substitution +template_code = "int TMPL_VALUE" +template_params = {"VALUE": 1} +compilation_result = app_manager.compile_teal_template( + template_code, + template_params=template_params +) + +# 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( + control_template, + deployment_metadata=deployment_metadata +) +``` + +The compilation result contains: + +- `teal` - Original TEAL code +- `compiled` - Base64 encoded compiled bytecode +- `compiled_hash` - Hash of compiled bytecode +- `compiled_base64_to_bytes` - Raw bytes of compiled bytecode +- `source_map` - Source map for debugging + +## Accessing state + +### Global state + +To access global state you can use: + +```python +# Get global state for app +global_state = app_manager.get_global_state(app_id) + +# Parse raw state from algod +decoded_state = AppManager.decode_app_state(raw_state) + +# Access state values +key_raw = decoded_state["value1"].key_raw # Raw bytes +key_base64 = decoded_state["value1"].key_base64 # Base64 encoded +value = decoded_state["value1"].value # Parsed value (str or int) +value_raw = decoded_state["value1"].value_raw # Raw bytes if bytes value +value_base64 = decoded_state["value1"].value_base64 # Base64 if bytes value +``` + +### Local state + +To access local state you can use: + +```python +local_state = app_manager.get_local_state(app_id, "ACCOUNT_ADDRESS") +``` + +### Boxes + +To access box storage: + +```python +# Get box names +box_names = app_manager.get_box_names(app_id) + +# Get box values +box_value = app_manager.get_box_value(app_id, box_name) +box_values = app_manager.get_box_values(app_id, [box_name1, box_name2]) + +# Get decoded ABI values +abi_value = app_manager.get_box_value_from_abi_type( + app_id, box_name, algosdk.abi.StringType() +) +abi_values = app_manager.get_box_values_from_abi_type( + app_id, [box_name1, box_name2], algosdk.abi.StringType() +) + +# Get box reference for transaction +box_ref = AppManager.get_box_reference(box_id) +``` + +## Getting app information + +To get app information: + +```python +# Get app info by ID +app_info = app_manager.get_by_id(app_id) + +# Get ABI return value from transaction +abi_return = AppManager.get_abi_return(confirmation, abi_method) +``` + +## Box references + +Box references can be specified in several ways: + +```python +# String name (encoded to bytes) +box_ref = "my_box" + +# Raw bytes +box_ref = b"my_box" + +# Account signer (uses address as name) +box_ref = account_signer + +# Box reference with app ID +box_ref = BoxReference(app_id=123, name=b"my_box") +``` + +## Common app parameters + +When interacting with apps (creating, updating, deleting, calling), there are common parameters that can be passed: + +- `app_id` - ID of the application +- `sender` - Address of transaction sender +- `signer` - Transaction signer (optional) +- `args` - Arguments to pass to the smart contract +- `account_references` - Account addresses to reference +- `app_references` - App IDs to reference +- `asset_references` - Asset IDs to reference +- `box_references` - Box references to load +- `on_complete` - On complete action +- Other common transaction parameters like `note`, `lease`, etc. + +For ABI method calls, additional parameters: + +- `method` - The ABI method to call +- `args` - ABI typed arguments to pass + +See [App client](../app-client/) for more details on constructing app calls. diff --git a/docs/src/content/docs/concepts/advanced/dispenser-client.md b/_ARCHIVE/concepts/advanced/dispenser-client.md similarity index 100% rename from docs/src/content/docs/concepts/advanced/dispenser-client.md rename to _ARCHIVE/concepts/advanced/dispenser-client.md diff --git a/docs/src/content/docs/concepts/advanced/indexer.md b/_ARCHIVE/concepts/advanced/indexer.md similarity index 100% rename from docs/src/content/docs/concepts/advanced/indexer.md rename to _ARCHIVE/concepts/advanced/indexer.md diff --git a/docs/src/content/docs/concepts/advanced/modular-imports.md b/_ARCHIVE/concepts/advanced/modular-imports.md similarity index 100% rename from docs/src/content/docs/concepts/advanced/modular-imports.md rename to _ARCHIVE/concepts/advanced/modular-imports.md diff --git a/docs/src/content/docs/concepts/building/asset.md b/_ARCHIVE/concepts/building/asset.md similarity index 100% rename from docs/src/content/docs/concepts/building/asset.md rename to _ARCHIVE/concepts/building/asset.md diff --git a/docs/src/content/docs/concepts/building/testing.md b/_ARCHIVE/concepts/building/testing.md similarity index 100% rename from docs/src/content/docs/concepts/building/testing.md rename to _ARCHIVE/concepts/building/testing.md diff --git a/docs/src/content/docs/concepts/building/transfer.md b/_ARCHIVE/concepts/building/transfer.md similarity index 100% rename from docs/src/content/docs/concepts/building/transfer.md rename to _ARCHIVE/concepts/building/transfer.md diff --git a/docs/src/content/docs/concepts/core/amount.md b/_ARCHIVE/concepts/core/amount.md similarity index 100% rename from docs/src/content/docs/concepts/core/amount.md rename to _ARCHIVE/concepts/core/amount.md diff --git a/docs/src/content/docs/concepts/core/client.md b/_ARCHIVE/concepts/core/client.md similarity index 100% rename from docs/src/content/docs/concepts/core/client.md rename to _ARCHIVE/concepts/core/client.md diff --git a/docs/src/content/docs/concepts/core/secret-management.md b/_ARCHIVE/concepts/core/secret-management.md similarity index 100% rename from docs/src/content/docs/concepts/core/secret-management.md rename to _ARCHIVE/concepts/core/secret-management.md diff --git a/_ARCHIVE/typed-app-clients.md b/_ARCHIVE/typed-app-clients.md new file mode 100644 index 00000000..f696a293 --- /dev/null +++ b/_ARCHIVE/typed-app-clients.md @@ -0,0 +1,187 @@ +--- +title: "Typed application clients" +description: "Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined ARC-56 or ARC-32 application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract." +--- + +Typed application clients are automatically generated, typed Python deployment and invocation clients for smart contracts that have a defined [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258) or [ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) application specification so that the development experience is easier with less upskill ramp-up and less deployment errors. These clients give you a type-safe, intellisense-driven experience for invoking the smart contract. + +Typed application clients are the recommended way of interacting with smart contracts. If you don't have/want a typed client, but have an ARC-56/ARC-32 app spec then you can use the [non-typed application clients](../app-client/) and if you want to call a smart contract you don't have an app spec file for you can use the underlying [app management](../app/) and [app deployment](../app-deploy/) functionality to manually construct transactions. + +## Generating an app spec + +You can generate an app spec file: + +- Using [Algorand Python](https://algorandfoundation.github.io/puya/#quick-start) +- Using [TEALScript](https://tealscript.netlify.app/tutorials/hello-world/0004-artifacts/) +- By hand by following the specification [ARC-56](https://github.com/algorandfoundation/ARCs/pull/258)/[ARC-32](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0032.md) +- Using [Beaker](https://algorand-devrel.github.io/beaker/html/usage.html) (PyTEAL) _(DEPRECATED)_ + +## Generating a typed client + +To generate a typed client from an app spec file you can use [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#1-typed-clients): + +``` +> algokit generate client application.json --output /absolute/path/to/client.py +``` + +Note: AlgoKit Utils >= 3.0.0 is compatible with the older 1.x.x generated typed clients, however if you want to utilise the new features or leverage ARC-56 support, you will need to generate using >= 2.x.x. See [AlgoKit CLI generator version pinning](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/generate.md#version-pinning) for more information on how to lock to a specific version. + +## Getting a typed client instance + +To get an instance of a typed client you can use an [`AlgorandClient`](../../core/algorand-client/) instance or a typed app [`Factory`](#creating-a-typed-factory-instance) instance. + +The approach to obtaining a client instance depends on how many app clients you require for a given app spec and if the app has already been deployed: + +### App is deployed + +#### Resolve App by ID + +**Single Typed App Client Instance:** + +```python +# Typed: Using the AlgorandClient extension method +typed_client = algorand.client.get_typed_app_client_by_id( + MyContractClient, # Generated typed client class + app_id=1234, + # ... +) +# or Typed: Using the generated client class directly +typed_client = MyContractClient( + algorand, + app_id=1234, + # ... +) +``` + +**Multiple Typed App Client Instances:** + +```python +# Typed: Using a typed factory to get multiple client instances +typed_client1 = typed_factory.get_app_client_by_id( + app_id=1234, + # ... +) +typed_client2 = typed_factory.get_app_client_by_id( + app_id=4321, + # ... +) +``` + +#### Resolve App by Creator and Name + +**Single Typed App Client Instance:** + +```python +# Typed: Using the AlgorandClient extension method +typed_client = algorand.client.get_typed_app_client_by_creator_and_name( + MyContractClient, # Generated typed client class + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +# or Typed: Using the static method on the generated client class +typed_client = MyContractClient.from_creator_and_name( + algorand, + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +``` + +**Multiple Typed App Client Instances:** + +```python +# Typed: Using a typed factory to get multiple client instances by name +typed_client1 = typed_factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name", + # ... +) +typed_client2 = typed_factory.get_app_client_by_creator_and_name( + creator_address="CREATORADDRESS", + app_name="contract-name-2", + # ... +) +``` + +### App is not deployed + +#### Deploy a New App + +```python +# Typed: For typed clients, you call a specific creation method rather than generic 'create' +typed_client, response = typed_factory.send.create.{METHODNAME}( + # ... +) +``` + +#### Deploy or Resolve App Idempotently by Creator and Name + +```python +# Typed: Using the deploy method on a typed factory +typed_client, response = typed_factory.deploy( + on_update=OnUpdate.UpdateApp, + on_schema_break=OnSchemaBreak.ReplaceApp, + # The parameters for create/update/delete would be specific to your generated client + app_name="contract-name", + # ... +) +``` + +### Creating a typed factory instance + +If your scenario calls for an app factory, you can create one using the below: + +```python +# Typed: Using the AlgorandClient extension method +typed_factory = algorand.client.get_typed_app_factory(MyContractFactory) # Generated factory class +# or Typed: Using the factory class constructor directly +typed_factory = MyContractFactory(algorand) +``` + +## Client usage + +See the [official usage docs](https://github.com/algorandfoundation/algokit-client-generator-py/blob/main/docs/usage.md) for full details about typed clients. + +Below is a realistic example that deploys a contract, funds it if newly created, and calls a `"hello"` method: + +```python +# Typed: Complete example using a typed application client +import algokit_utils +from artifacts.hello_world.hello_world_client import ( + HelloArgs, # Generated args class + HelloWorldFactory, # Generated factory class +) + +# Get Algorand client from environment variables +algorand = algokit_utils.AlgorandClient.from_environment() +deployer = algorand.account.from_environment("DEPLOYER") + +# Create the typed app factory +typed_factory = algorand.client.get_typed_app_factory( + HelloWorldFactory, default_sender=deployer.address +) + +# Deploy idempotently - creates if it doesn't exist or updates if changed +typed_client, result = typed_factory.deploy( + on_update=algokit_utils.OnUpdate.AppendApp, + on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, +) + +# Fund the app with 1 ALGO if it's newly created +if result.operation_performed in [ + algokit_utils.OperationPerformed.Create, + algokit_utils.OperationPerformed.Replace, +]: + algorand.send.payment( + algokit_utils.PaymentParams( + amount=algokit_utils.AlgoAmount(algo=1), + sender=deployer.address, + receiver=typed_client.app_address, + ) + ) + +# Call the hello method on the smart contract +name = "world" +response = typed_client.send.hello(args=HelloArgs(name=name)) # Using generated args class +``` diff --git a/docs/sidebar.config.json b/docs/sidebar.config.json index c3061236..a934ecc5 100644 --- a/docs/sidebar.config.json +++ b/docs/sidebar.config.json @@ -10,31 +10,11 @@ { "slug": "concepts/algorand-client" }, { "slug": "concepts/transactions" }, { "slug": "concepts/account" }, - { "slug": "concepts/core/amount" }, { "slug": "concepts/assets" }, - { "slug": "concepts/core/client" }, - { "slug": "concepts/core/secret-management" }, { "slug": "concepts/applications" }, { "slug": "concepts/errors-and-debugging" } ] }, - { - "label": "Building Applications", - "items": [ - { "slug": "concepts/building/asset" }, - { "slug": "concepts/building/transfer" }, - { "slug": "concepts/building/testing" } - ] - }, - { - "label": "Advanced Topics", - "collapsed": true, - "items": [ - { "slug": "concepts/advanced/modular-imports" }, - { "slug": "concepts/advanced/indexer" }, - { "slug": "concepts/advanced/dispenser-client" } - ] - }, { "label": "Migration Guides", "collapsed": true, diff --git a/docs/src/content/docs/concepts/account.mdx b/docs/src/content/docs/concepts/account.mdx index 1ae1da95..ed66d3ce 100644 --- a/docs/src/content/docs/concepts/account.mdx +++ b/docs/src/content/docs/concepts/account.mdx @@ -97,7 +97,7 @@ Other operations, such as creating and renaming a wallet, can also be performed frame="none" /> -The KMD SDK is fairly low level, so to make use of it directly there is a fair bit of boilerplate. This is abstracted away into the `KmdAccountManager` class, which you can access from [`AlgorandClient`](../algorand-client/) via `algorand.account.kmd` or instantiate directly (passing in a [`ClientManager`](../client/)): +The KMD SDK is fairly low level, so to make use of it directly there is a fair bit of boilerplate. This is abstracted away into the `KmdAccountManager` class, which you can access from [`AlgorandClient`](../algorand-client/) via `algorand.account.kmd` or instantiate directly (passing in a [`ClientManager`](/algokit-utils-py/api/algokit_utils/clients/client_manager/)):