-
Notifications
You must be signed in to change notification settings - Fork 10
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
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
b53e0cf
initial composer
joe-p 041317a
account and client manager
joe-p b06af69
build_group and execute
joe-p 8bce649
WIP: algorand-client
joe-p a0c6590
fix up algorand-client
joe-p 621515b
algorand-client -> algorand_client
joe-p e36a14c
test_send_payment
joe-p ed4550a
refactor params dataclasses
joe-p 69839f1
fix sender params
joe-p 4d923d1
refactor txn params
joe-p 7573dfc
rm AlgoAmount
joe-p de14c48
beta namespace
joe-p 7a9f7f0
rm from __init__
joe-p 9560f23
improve send_payment
joe-p 553a675
test_asset_opt_in
joe-p ee28990
addr -> address
joe-p 257ac17
parity with JS tests
joe-p f9cfa62
ruff check --fix
joe-p 95b7682
move account_manager to beta
joe-p 8a3f2ff
unsafe ruff fixes
joe-p abc6d02
various fixes
joe-p 6d45998
use match
joe-p d359a76
fix remaining ruff errors (other than line length and comments)
joe-p b8a1e41
assert rather than cast
joe-p 067f4c5
dont import from source
joe-p 938b081
use frozen dataclasses
joe-p 1fc0f66
default get value
joe-p 1ed2c94
instantiate dict
joe-p 7ee14f5
using typing.Self
joe-p fb444c0
fix some docstrings
joe-p d60d050
update idna due to vulnerability by pip-audit
joe-p 1e3797e
ruff
joe-p 452d97d
ignore ruff errors in beta for now
joe-p 80a1cf5
fix non sdk mypy stuff
joe-p 23664c2
update cryptography for pip-audit
joe-p aa9e1e7
rm comment
joe-p 1bf2bd5
ignore mypy errors
joe-p 442b8bf
ruff
joe-p b799bff
update setuptools
joe-p 770c84a
chore: hotfixing the pkgutil.ImpImporter error when using 3.12 based …
aorumbayev de9b9d9
tuple unpacking
joe-p a16accd
Merge remote-tracking branch 'origin/main' into feat/algorand_client
aorumbayev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 becauseAccount
has a private key property, which is not applicable outside of localnet in most cases. Also I want to try to get away fromAccount
terminology and instead useAddress
(with account being for HD wallet accounts that are used to derive addresses)