Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: initial documentation for beta release #128

Merged
merged 6 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 193 additions & 29 deletions docs/source/capabilities/account.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,195 @@
# Account management

Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, idempotent KMD and environment variable injected accounts
that can be used to sign transactions as well as representing a sender address at the same time.

(account)=
## `Account`

Encapsulates a private key with convenience properties for `address`, `signer` and `public_key`.

There are various methods of obtaining an `Account` instance

* `get_account`: Returns an `Account` instance with the private key loaded by convention based on the given name identifier:
* from an environment variable containing a mnemonic `{NAME}_MNEMONIC` OR
* loading the account from KMD ny name if it exists (LocalNet only) OR
* creating the account in KMD with associated name (LocalNet only)

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

* `Account.new_account`: Returns a new `Account` using `algosdk.account.generate_account()`
* `Account(private_key)`: Load an existing account from a private key
* `Account(private_key, address)`: Load an existing account from a private key and address, useful for re-keyed accounts
* `get_account_from_mnemonic`: Load an existing account from a mnemonic
* `get_dispenser_account`: Gets a dispenser account that is funded by either:
* Using the LocalNet default account (LocalNet only) OR
* Loading an account from `DISPENSER_MNEMONIC`

If working with a LocalNet instance, there are some additional functions that rely on a KMD service being exposed:
* `create_kmd_wallet_account`, `get_kmd_wallet_account` or `get_or_create_kmd_wallet_account`: These functions allow retrieving a KMD wallet account by name,
* `get_localnet_default_account`: Gets default localnet account that is funded with algos
Account management is one of the core capabilities provided by AlgoKit Utils. It allows you to create mnemonic, rekeyed, multisig, transaction signer, idempotent KMD and environment variable injected accounts that can be used to sign transactions as well as representing a sender address at the same time. This significantly simplifies management of transaction signing.

## `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!

To get an instance of `AccountManager`, you can either use the `AlgorandClient` via `algorand.account` or instantiate it directly:

```python
from algokit_utils import AccountManager

account_manager = AccountManager(client_manager)
```

## `Account` and Transaction Signing

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should mirror the TS utils here? Might be worth a chat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, will introduce a further overhaul to have TransactionSignerAccount introduced as a protocol, would require a bigger chunk of changes so will address in a separate PR as part of last batch of improvements in preparation for pypi release. Will keep the issue unresolved for now


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

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

There are two methods that can be used for this:

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

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

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

```python
signer = account_manager.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.

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

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

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

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

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

You can issue a transaction to rekey an account by using the `rekey_account` method:

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

### Examples

```python
# Basic example (with string addresses)
account_manager.rekey_account(account="ACCOUNTADDRESS", rekey_to="NEWADDRESS")

# Basic example (with signer accounts)
account_manager.rekey_account(account=account1, rekey_to=new_signer_account)

# Advanced example
account_manager.rekey_account(
account="ACCOUNTADDRESS",
rekey_to="NEWADDRESS",
lease="lease",
note="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,
)

# 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)
# rekeyed_account can be used to sign transactions on behalf of account...
```

# KMD account management

When running LocalNet, you have an instance of the [Key Management Daemon](https://github.com/algorand/go-algorand/blob/master/daemon/kmd/README.md), which is useful for:

- Accessing the private key of the default accounts that are pre-seeded with Algo so that other accounts can be funded and it's possible to use LocalNet
- Idempotently creating new accounts against a name that will stay intact while the LocalNet instance is running without you needing to store private keys anywhere (i.e. completely automated)

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

```python
from algokit_utils import KmdAccountManager

# Algod client only
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)

```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,
)
# 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))
# 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:

```python
# Get and register LocalNet dispenser
localnet_dispenser = account_manager.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()
# Get / create and register account from KMD idempotently by name
account1 = account_manager.from_kmd("account1", AlgoAmount.from_algo(2))
```
85 changes: 85 additions & 0 deletions docs/source/capabilities/algorand-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Algorand client

`AlgorandClient` is a client class that brokers easy access to Algorand functionality. It's the default entrypoint 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:

```python
# Point to the network configured through environment variables or
# 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()
# 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(
aorumbayev marked this conversation as resolved.
Show resolved Hide resolved
algod=...,
indexer=...,
kmd=...,
)
)
# Point to custom configuration
algorand = AlgorandClient.from_config(
aorumbayev marked this conversation as resolved.
Show resolved Hide resolved
AlgoClientConfigs(
algod_config=AlgoClientConfig(
server="http://localhost:4001", token="my-token", port=4001
),
indexer_config=None,
kmd_config=None,
)
)
```

## Accessing SDK clients

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

algod_client = algorand.client.algod
indexer_client = algorand.client.indexer
kmd_client = algorand.client.kmd
```

## Accessing manager class instances

The `AlgorandClient` has several manager class instances that help you quickly access advanced functionality:

- `AccountManager` via `algorand.account`, with chainable convenience methods:
- `algorand.set_default_signer(signer)`
- `algorand.set_signer(sender, signer)`
- `AssetManager` via `algorand.asset`
- `ClientManager` via `algorand.client`
- `AppManager` via `algorand.app`
- `AppDeployer` via `algorand.app_deployer`

## Creating and issuing transactions

`AlgorandClient` exposes methods to create, execute, and compose groups of transactions via the `TransactionComposer`.

### Transaction configuration

AlgorandClient caches network provided transaction values automatically to reduce network traffic. You can configure this behavior:

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

### Creating transaction groups

You can compose a group of transactions using the `new_group()` method which returns a `TransactionComposer` instance:

```python
result = (
algorand.new_group()
.add_payment(sender="SENDERADDRESS", receiver="RECEIVERADDRESS", amount=1_000)
.add_asset_opt_in(sender="SENDERADDRESS", asset_id=12345)
.send()
)
```
69 changes: 69 additions & 0 deletions docs/source/capabilities/amount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Algo amount handling

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.

To see some usage examples check out the automated tests in the repository. Alternatively, you can refer to 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!).

To import the AlgoAmount class you can access it via:

```python
from algokit_utils.models import AlgoAmount
```

### Creating an `AlgoAmount`

There are several ways to create an `AlgoAmount`:

- Algo
- Constructor: `AlgoAmount({"algo": 10})`
- Static helper: `AlgoAmount.from_algo(10)`
- Static helper (plural): `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)`

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

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

You can also call `str(amount)` or use an `AlgoAmount` directly in string interpolation to convert it to a nice user-facing formatted amount expressed in microAlgo.

### Additional Features

The `AlgoAmount` class also supports:

- Arithmetic operations (`+`, `-`) with other `AlgoAmount` objects or integers
- Comparison operations (`<`, `<=`, `>`, `>=`, `==`, `!=`)
- In-place arithmetic (`+=`, `-=`)

Example usage:

```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"
```
Loading
Loading