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

feat: AlgorandClient #71

Merged
merged 42 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b53e0cf
initial composer
joe-p Apr 3, 2024
041317a
account and client manager
joe-p Apr 3, 2024
b06af69
build_group and execute
joe-p Apr 3, 2024
8bce649
WIP: algorand-client
joe-p Apr 4, 2024
a0c6590
fix up algorand-client
joe-p Apr 6, 2024
621515b
algorand-client -> algorand_client
joe-p Apr 6, 2024
e36a14c
test_send_payment
joe-p Apr 6, 2024
ed4550a
refactor params dataclasses
joe-p Apr 6, 2024
69839f1
fix sender params
joe-p Apr 6, 2024
4d923d1
refactor txn params
joe-p Apr 7, 2024
7573dfc
rm AlgoAmount
joe-p Apr 7, 2024
de14c48
beta namespace
joe-p Apr 7, 2024
7a9f7f0
rm from __init__
joe-p Apr 7, 2024
9560f23
improve send_payment
joe-p Apr 8, 2024
553a675
test_asset_opt_in
joe-p Apr 8, 2024
ee28990
addr -> address
joe-p Apr 8, 2024
257ac17
parity with JS tests
joe-p Apr 8, 2024
f9cfa62
ruff check --fix
joe-p Apr 10, 2024
95b7682
move account_manager to beta
joe-p Apr 10, 2024
8a3f2ff
unsafe ruff fixes
joe-p Apr 10, 2024
abc6d02
various fixes
joe-p Apr 10, 2024
6d45998
use match
joe-p Apr 10, 2024
d359a76
fix remaining ruff errors (other than line length and comments)
joe-p Apr 10, 2024
b8a1e41
assert rather than cast
joe-p Apr 10, 2024
067f4c5
dont import from source
joe-p Apr 10, 2024
938b081
use frozen dataclasses
joe-p Apr 10, 2024
1fc0f66
default get value
joe-p Apr 10, 2024
1ed2c94
instantiate dict
joe-p Apr 10, 2024
7ee14f5
using typing.Self
joe-p Apr 10, 2024
fb444c0
fix some docstrings
joe-p Apr 15, 2024
d60d050
update idna due to vulnerability by pip-audit
joe-p Apr 15, 2024
1e3797e
ruff
joe-p Apr 15, 2024
452d97d
ignore ruff errors in beta for now
joe-p Apr 15, 2024
80a1cf5
fix non sdk mypy stuff
joe-p Apr 15, 2024
23664c2
update cryptography for pip-audit
joe-p Apr 15, 2024
aa9e1e7
rm comment
joe-p Apr 15, 2024
1bf2bd5
ignore mypy errors
joe-p Apr 15, 2024
442b8bf
ruff
joe-p Apr 15, 2024
b799bff
update setuptools
joe-p Apr 15, 2024
770c84a
chore: hotfixing the pkgutil.ImpImporter error when using 3.12 based …
aorumbayev Apr 16, 2024
de9b9d9
tuple unpacking
joe-p Apr 16, 2024
a16accd
Merge remote-tracking branch 'origin/main' into feat/algorand_client
aorumbayev Apr 17, 2024
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
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"autodoc2",
]
templates_path = ["_templates"]
exclude_patterns = []
exclude_patterns = [] # type: ignore
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"algosdk": ("https://py-algorand-sdk.readthedocs.io/en/latest", None),
Expand All @@ -37,7 +37,7 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_rtd_theme"
html_static_path = []
html_static_path = [] # type: ignore


# -- Options for myst ---
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ target-version = "py310"
allow-star-arg-any = true
suppress-none-returning = true

[tool.ruff.per-file-ignores]
"src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"]
"path/to/file.py" = ["E402"]

[tool.poe.tasks]
docs = "sphinx-build docs/source docs/html"

Expand Down
200 changes: 200 additions & 0 deletions src/algokit_utils/beta/account_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from algokit_utils.account import get_dispenser_account, get_kmd_wallet_account, get_localnet_default_account
from algosdk.account import generate_account
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionSigner
from typing_extensions import Self

from .client_manager import ClientManager


@dataclass
class AddressAndSigner:
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it's possible to use an Account here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason I went with AddressAndSigner is because Account has a private key property, which is not applicable outside of localnet in most cases. Also I want to try to get away from Account terminology and instead use Address (with account being for HD wallet accounts that are used to derive addresses)

address: str
signer: TransactionSigner


class AccountManager:
"""Creates and keeps track of addresses and signers"""

def __init__(self, client_manager: ClientManager):
"""
Create a new account manager.

:param client_manager: The ClientManager client to use for algod and kmd clients
"""
self._client_manager = client_manager
self._accounts = dict[str, TransactionSigner]()
self._default_signer: TransactionSigner | None = None

def set_default_signer(self, signer: TransactionSigner) -> Self:
"""
Sets the default signer to use if no other signer is specified.

:param signer: The signer to use, either a `TransactionSigner` or a `TransactionSignerAccount`
:return: The `AccountManager` so method calls can be chained
"""
self._default_signer = signer
return self

def set_signer(self, sender: str, signer: TransactionSigner) -> Self:
"""
Tracks the given account for later signing.

:param sender: The sender address to use this signer for
:param signer: The signer to sign transactions with for the given sender
:return: The AccountCreator instance for method chaining
"""
self._accounts[sender] = signer
return self

def get_signer(self, sender: str) -> TransactionSigner:
"""
Returns the `TransactionSigner` for the given sender address.

If no signer has been registered for that address then the default signer is used if registered.

:param sender: The sender address
:return: The `TransactionSigner` or throws an error if not found
"""
signer = self._accounts.get(sender, None) or self._default_signer
if not signer:
raise ValueError(f"No signer found for address {sender}")
return signer

def get_information(self, sender: str) -> dict[str, Any]:
"""
Returns the given sender account's current status, balance and spendable amounts.

Example:
address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA"
account_info = account.get_information(address)

`Response data schema details <https://developer.algorand.org/docs/rest-apis/algod/#get-v2accountsaddress>`_

:param sender: The address of the sender/account to look up
:return: The account information
"""
info = self._client_manager.algod.account_info(sender)
assert isinstance(info, dict)
return info

def get_asset_information(self, sender: str, asset_id: int) -> dict[str, Any]:
info = self._client_manager.algod.account_asset_info(sender, asset_id)
assert isinstance(info, dict)
return info

# TODO
# def from_mnemonic(self, mnemonic_secret: str, sender: Optional[str] = None) -> AddrAndSigner:
# """
# Tracks and returns an Algorand account with secret key loaded (i.e. that can sign transactions) by taking the mnemonic secret.

# Example:
# account = account.from_mnemonic("mnemonic secret ...")
# rekeyed_account = account.from_mnemonic("mnemonic secret ...", "SENDERADDRESS...")

# :param mnemonic_secret: The mnemonic secret representing the private key of an account; **Note: Be careful how the mnemonic is handled**,
# never commit it into source control and ideally load it from the environment (ideally via a secret storage service) rather than the file system.
# :param sender: The optional sender address to use this signer for (aka a rekeyed account)
# :return: The account
# """
# account = mnemonic_account(mnemonic_secret)
# return self.signer_account(rekeyed_account(account, sender) if sender else account)

def from_kmd(
self,
name: str,
predicate: Callable[[dict[str, Any]], bool] | None = None,
) -> AddressAndSigner:
"""
Tracks and returns an Algorand account with private key loaded from the given KMD wallet (identified by name).

Example (Get default funded account in a LocalNet):
default_dispenser_account = account.from_kmd('unencrypted-default-wallet',
lambda a: a['status'] != 'Offline' and a['amount'] > 1_000_000_000
)

:param name: The name of the wallet to retrieve an account from
:param predicate: An optional filter to use to find the account (otherwise it will return a random account from the wallet)
:return: The account
"""
account = get_kmd_wallet_account(
name=name, predicate=predicate, client=self._client_manager.algod, kmd_client=self._client_manager.kmd
)
if not account:
raise ValueError(f"Unable to find KMD account {name}{' with predicate' if predicate else ''}")

self.set_signer(account.address, account.signer)
return AddressAndSigner(address=account.address, signer=account.signer)

# TODO
# def multisig(
# self, multisig_params: algosdk.MultisigMetadata, signing_accounts: Union[algosdk.Account, SigningAccount]
# ) -> TransactionSignerAccount:
# """
# Tracks and returns an account that supports partial or full multisig signing.

# Example:
# account = account.multisig(
# {
# "version": 1,
# "threshold": 1,
# "addrs": ["ADDRESS1...", "ADDRESS2..."]
# },
# account.from_environment('ACCOUNT1')
# )

# :param multisig_params: The parameters that define the multisig account
# :param signing_accounts: The signers that are currently present
# :return: A multisig account wrapper
# """
# return self.signer_account(multisig_account(multisig_params, signing_accounts))

def random(self) -> AddressAndSigner:
"""
Tracks and returns a new, random Algorand account with secret key loaded.

Example:
account = account.random()

:return: The account
"""
(sk, addr) = generate_account() # type: ignore[no-untyped-call]
signer = AccountTransactionSigner(sk)

self.set_signer(addr, signer)

return AddressAndSigner(address=addr, signer=signer)

def dispenser(self) -> AddressAndSigner:
"""
Returns an account (with private key loaded) that can act as a dispenser.

Example:
account = account.dispenser()

If running on LocalNet then it will return the default dispenser account automatically,
otherwise it will load the account mnemonic stored in os.environ['DISPENSER_MNEMONIC'].

:return: The account
"""
acct = get_dispenser_account(self._client_manager.algod)

self.set_signer(acct.address, acct.signer)

return AddressAndSigner(address=acct.address, signer=acct.signer)

def localnet_dispenser(self) -> AddressAndSigner:
"""
Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts).

Example:
account = account.localnet_dispenser()

:return: The account
"""
acct = get_localnet_default_account(self._client_manager.algod)
self.set_signer(acct.address, acct.signer)
return AddressAndSigner(address=acct.address, signer=acct.signer)
Loading