Skip to content

Commit

Permalink
docs: initial rewrites for appmanager; clients and debugger docs
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Dec 23, 2024
1 parent 814dd4f commit b8d70d2
Show file tree
Hide file tree
Showing 3 changed files with 355 additions and 33 deletions.
212 changes: 212 additions & 0 deletions docs/source/capabilities/app-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# App management

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 need to instantiate it with an algod client:

```python
from algokit_utils import AppManager

app_manager = AppManager(algod_client)
```

## Calling apps

### App Clients

The recommended way of interacting with apps is via [Typed app clients](./typed-app-clients.md) or if you can't use a typed app client then an [untyped app client](./app-client.md). 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.

### Creation

To create an app you can use the following parameters:

- `sender: str` - The address of the account that will create the app
- `approval_program: bytes | str` - The program to execute for all OnCompletes other than ClearState as raw TEAL that will be compiled (string) or compiled TEAL (encoded as bytes)
- `clear_state_program: bytes | str` - The program to execute for ClearState OnComplete as raw TEAL that will be compiled (string) or compiled TEAL (encoded as bytes)
- `schema: dict | None` - The storage schema to request for the created app. This is immutable once the app is created. It is a dictionary with:
- `global_ints: int` - The number of integers saved in global state
- `global_byte_slices: int` - The number of byte slices saved in global state
- `local_ints: int` - The number of integers saved in local state
- `local_byte_slices: int` - The number of byte slices saved in local state
- `extra_program_pages: int | None` - Number of extra pages required for the programs. This is immutable once the app is created.

If you pass in `approval_program` or `clear_state_program` as a string then it will automatically be compiled using Algod and the compilation result will be available via `app_manager.get_compilation_result` (including the source map). To skip this behaviour you can pass in the compiled TEAL as bytes.

### Updating

To update an app you can use the following parameters:

- `sender: str` - The address of the account that will update the app
- `app_id: int` - The ID of the app to update
- `approval_program: bytes | str` - The new program to execute for all OnCompletes other than ClearState
- `clear_state_program: bytes | str` - The new program to execute for ClearState OnComplete

### Deleting

To delete an app you can use the following parameters:

- `sender: str` - The address of the account that will delete the app
- `app_id: int` - The ID of the app to delete

### Calling

To call an app you can use the following parameters:

- `sender: str` - The address of the account that will call the app
- `app_id: int` - The ID of the app to call
- `on_complete: OnComplete | None` - The on-completion action to specify for the call
- `args: list[bytes | str] | None` - Any arguments to pass to the smart contract call
- `accounts: list[str] | None` - Any account addresses to add to the accounts array
- `foreign_apps: list[int] | None` - The ID of any apps to load to the foreign apps array
- `foreign_assets: list[int] | None` - The ID of any assets to load to the foreign assets array
- `boxes: list[BoxReference | BoxIdentifier] | None` - Any boxes to load to the boxes array

### ABI Return Values

The `AppManager` provides a static method to parse ABI return values from transaction confirmations:

```python
abi_return = AppManager.get_abi_return(confirmation, abi_method)
if abi_return:
raw_return_value = abi_return.raw_return_value
return_value = abi_return.return_value
```

## Accessing state

### Global state

To access global state you can use the following method from an `AppManager` instance:

- `app_manager.get_global_state(app_id)` - Returns the current global state for the given app ID decoded into a dictionary keyed by the UTF-8 representation of the state key with various parsed versions of the value (base64, UTF-8 and raw binary)

```python
global_state = app_manager.get_global_state(12345)
```

Global state is parsed from the underlying algod response via the following static method from `AppManager`:

- `AppManager.decode_app_state(state)` - Takes the raw response from the algod API for global state and returns a friendly dictionary keyed by the UTF-8 value of the key

```python
app_state = AppManager.decode_app_state(global_app_state)

key_as_binary = app_state['value1'].key_raw
key_as_base64 = app_state['value1'].key_base64
if isinstance(app_state['value1'].value, str):
value_as_string = app_state['value1'].value
value_as_binary = app_state['value1'].value_raw
value_as_base64 = app_state['value1'].value_base64
else:
value_as_number = app_state['value1'].value
```

### Local state

To access local state you can use the following method from an `AppManager` instance:

- `app_manager.get_local_state(app_id, address)` - Returns the current local state for the given app ID and account address decoded into a dictionary keyed by the UTF-8 representation of the state key with various parsed versions of the value (base64, UTF-8 and raw binary)

```python
local_state = app_manager.get_local_state(12345, 'ACCOUNTADDRESS')
```

### Boxes

To access and parse box values and names for an app you can use the following methods from an `AppManager` instance:

- `app_manager.get_box_names(app_id)` - Returns the current box names for the given app ID
- `app_manager.get_box_value(app_id, box_name)` - Returns the binary value of the given box name for the given app ID
- `app_manager.get_box_values(app_id, box_names)` - Returns the binary values of the given box names for the given app ID
- `app_manager.get_box_value_from_abi_type(app_id, box_name, abi_type)` - Returns the parsed ABI value of the given box name for the given app ID for the provided ABI type
- `app_manager.get_box_values_from_abi_type(app_id, box_names, abi_type)` - Returns the parsed ABI values of the given box names for the given app ID for the provided ABI type
- `AppManager.get_box_reference(box_id)` - Returns a tuple of `(app_id, name_bytes)` representation of the given box identifier/reference

```python
app_id = 12345
box_name = 'my-box'
box_name2 = 'my-box2'

box_names = app_manager.get_box_names(app_id)
box_value = app_manager.get_box_value(app_id, box_name)
box_values = app_manager.get_box_values(app_id, [box_name, box_name2])
box_abi_value = app_manager.get_box_value_from_abi_type(app_id, box_name, algosdk.abi.StringType())
box_abi_values = app_manager.get_box_values_from_abi_type(app_id, [box_name, box_name2], algosdk.abi.StringType())
```

## Getting app information

To get reference information and metadata about an existing app you can use:

- `app_manager.get_by_id(app_id)` - Returns current app information by app ID including approval program, clear state program, creator, schemas, and global state

## Common app parameters

When interacting with apps (creating, updating, deleting, calling), there are some common parameters that you will be able to pass in to all calls:

- `app_id: int` - ID of the application; only specified if the application is not being created
- `on_complete: OnComplete | None` - The on-complete action of the call
- `args: list[bytes | str] | None` - Any arguments to pass to the smart contract call
- `accounts: list[str] | None` - Any account addresses to add to the accounts array
- `foreign_apps: list[int] | None` - The ID of any apps to load to the foreign apps array
- `foreign_assets: list[int] | None` - The ID of any assets to load to the foreign assets array
- `boxes: list[BoxReference | BoxIdentifier] | None` - Any boxes to load to the boxes array

When making an ABI call, the `args` parameter is replaced with ABI-specific arguments and there is also a `method` parameter:

- `method: ABIMethod`
- `args: list[ABIArgument]` - The arguments to pass to the ABI call, which can be one of:
- `ABIValue` - Which can be one of:
- `bool`
- `int`
- `str`
- `bytes`
- A list of one of the above types
- `Transaction`
- `TransactionWithSigner`

## Box references

A box can be referenced by either a `BoxIdentifier` (which identifies the name of the box and app ID `0` will be used - i.e. the current app) or `BoxReference`:

```python
# BoxIdentifier can be:
# * bytes (the actual binary of the box name)
# * str (that will be encoded to bytes)
# * AccountTransactionSigner (that will be encoded into the public key address)
BoxIdentifier = str | bytes | AccountTransactionSigner

# BoxReference is a class with:
# * app_id: int - A unique application id
# * name: BoxIdentifier - Identifier for a box name
BoxReference = BoxReference
```

## 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.

If you call `app_manager.compile_teal(teal_code)` then the compilation result will be stored and retrievable from `app_manager.get_compilation_result(teal_code)`.

```python
teal_code = 'return 1'
compilation_result = app_manager.compile_teal(teal_code)
# ...
previous_compilation_result = app_manager.get_compilation_result(teal_code)
```

### Template compilation

The `AppManager` also supports compiling TEAL templates with variables and deployment metadata:

```python
compilation_result = app_manager.compile_teal_template(
teal_template_code,
template_params={"VAR1": "value1"},
deployment_metadata={"updatable": True, "deletable": True}
)
```
106 changes: 92 additions & 14 deletions docs/source/capabilities/client.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,107 @@
# Client management

Client management is one of the core capabilities provided by AlgoKit Utils.
It allows you to create [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer)
and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration.
Client management is one of the core capabilities provided by AlgoKit Utils. It allows you to create (auto-retry) [algod](https://developer.algorand.org/docs/rest-apis/algod), [indexer](https://developer.algorand.org/docs/rest-apis/indexer) and [kmd](https://developer.algorand.org/docs/rest-apis/kmd) clients against various networks resolved from environment or specified configuration.

Any AlgoKit Utils function that needs one of these clients will take the underlying `algosdk` classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`,
`algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the
Client management capability if you prefer.
Any AlgoKit Utils function that needs one of these clients will take the underlying algosdk classes (`algosdk.v2client.algod.AlgodClient`, `algosdk.v2client.indexer.IndexerClient`, `algosdk.kmd.KMDClient`) so inline with the [Modularity](../index.md#core-principles) principle you can use existing logic to get instances of these clients without needing to use the Client management capability if you prefer.

To see some usage examples check out the [automated tests](https://github.com/algorandfoundation/algokit-utils-py/blob/main/tests/test_network_clients.py).

## `ClientManager`

The `ClientManager` is a class that is used to manage client instances.

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)
```

## Network configuration

The network configuration is specified using the `AlgoClientConfig` class. This same interface is used to specify the config for algod, indexer and kmd clients.
The network configuration is specified using the `AlgoClientConfig` type. This same type is used to specify the config for [algod](https://developer.algorand.org/docs/sdks/python/), [indexer](https://developer.algorand.org/docs/sdks/python/) and [kmd](https://developer.algorand.org/docs/sdks/python/) SDK clients.

There are a number of ways to produce one of these configuration objects:

- Manually creating the object, e.g. `AlgoClientConfig(server="https://myalgodnode.com", token="SECRET_TOKEN")`
- `algokit_utils.get_algonode_config(network, config, token)`: Loads an Algod or indexer config against [Nodely](https://nodely.io/docs/free/start) to either MainNet or TestNet
- `algokit_utils.get_default_localnet_config(configOrPort)`: Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration
- Manually specifying a dictionary that conforms with the type, e.g.
```python
{
"server": "https://myalgodnode.com"
}
# Or with the optional values:
{
"server": "https://myalgodnode.com",
"port": 443,
"token": "SECRET_TOKEN"
}
```
- `ClientManager.get_config_from_environment_or_localnet()` - Loads the Algod client config, the Indexer client config and the Kmd config from well-known environment variables or if not found then default LocalNet; this is useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change
- `ClientManager.get_algod_config_from_environment()` - Loads an Algod client config from well-known environment variables
- `ClientManager.get_indexer_config_from_environment()` - Loads an Indexer client config from well-known environment variables; useful to have code that can work across multiple blockchain environments (including LocalNet), without having to change
- `ClientManager.get_algonode_config(network)` - Loads an Algod or indexer config against [AlgoNode free tier](https://nodely.io/docs/free/start) to either MainNet or TestNet
- `ClientManager.get_default_localnet_config()` - Loads an Algod, Indexer or Kmd config against [LocalNet](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/localnet.md) using the default configuration

## Clients

Once you have the configuration for a client, to get the client you can use the following functions:
### Creating an SDK client instance

Once you have the configuration for a client, to get a new client you can use the following functions:

- `ClientManager.get_algod_client(config)` - Returns an Algod client for the given configuration; the client automatically retries on transient HTTP errors
- `ClientManager.get_indexer_client(config)` - Returns an Indexer client for given configuration
- `ClientManager.get_kmd_client(config)` - Returns a Kmd client for the given configuration

You can also shortcut needing to write the likes of `ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())` with environment shortcut methods:

- `ClientManager.get_algod_client_from_environment()` - Returns an Algod client by loading the config from environment variables
- `ClientManager.get_indexer_client_from_environment()` - Returns an indexer client by loading the config from environment variables
- `ClientManager.get_kmd_client_from_environment()` - Returns a kmd client by loading the config from environment variables

### Accessing SDK clients via ClientManager instance

Once you have a `ClientManager` instance, you can access the SDK clients:

```python
client_manager = ClientManager(algod=algod_client, indexer=indexer_client, kmd=kmd_client)

algod_client = client_manager.algod
indexer_client = client_manager.indexer
kmd_client = client_manager.kmd
```

If the method to create the `ClientManager` doesn't configure indexer or kmd (both of which are optional), then accessing those clients will trigger an error.

### Creating a TestNet dispenser API client instance

You can also create a [TestNet dispenser API client instance](./dispenser-client.md) from `ClientManager` too.

## Automatic retry

When receiving an Algod or Indexer client from AlgoKit Utils, it will be a special wrapper client that handles retrying transient failures.

## Network information

You can get information about the current network you are connected to:

```python
# Get network information
network = client_manager.network()
print(f"Connected to: {network.name}") # e.g., "mainnet", "testnet", "localnet"
print(f"Genesis ID: {network.genesis_id}")
print(f"Genesis hash: {network.genesis_hash}")

# Check specific network types
is_mainnet = client_manager.is_mainnet()
is_testnet = client_manager.is_testnet()
is_localnet = client_manager.is_localnet()
```

- `algokit_utils.get_algod_client(config)`: Returns an Algod client for the given configuration or if none is provided retrieves a configuration from the environment using `ALGOD_SERVER`, `ALGOD_TOKEN` and optionally `ALGOD_PORT`.
- `algokit_utils.get_indexer_client(config)`: Returns an Indexer client for given configuration or if none is provided retrieves a configuration from the environment using `INDEXER_SERVER`, `INDEXER_TOKEN` and optionally `INDEXER_PORT`
- `algokit_utils.get_kmd_client_from_algod_client(config)`: - Returns a Kmd client based on the provided algod client configuration, with the assumption the KMD services is running on the same host but a different port (either `KMD_PORT` environment variable or `4002` by default)
The first time `network()` is called it will make a HTTP call to algod to get the network parameters, but from then on it will be cached within that `ClientManager` instance for subsequent calls.
Loading

0 comments on commit b8d70d2

Please sign in to comment.