-
Notifications
You must be signed in to change notification settings - Fork 11
/
kmd_account_manager.py
190 lines (152 loc) · 7.09 KB
/
kmd_account_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
from collections.abc import Callable
from typing import Any, cast
from algosdk.kmd import KMDClient
from algokit_utils.clients.client_manager import ClientManager
from algokit_utils.config import config
from algokit_utils.models.account import Account
from algokit_utils.models.amount import AlgoAmount
from algokit_utils.transactions.transaction_composer import PaymentParams, TransactionComposer
logger = config.logger
class KmdAccount(Account):
"""Account retrieved from KMD with signing capabilities, extending base Account"""
def __init__(self, private_key: str, address: str | None = None) -> None:
"""Initialize KMD account with private key and optional address override
Args:
private_key: Base64 encoded private key
address: Optional address override (for rekeyed accounts)
"""
super().__init__(private_key=private_key, address=address or "")
class KmdAccountManager:
"""Provides abstractions over KMD that makes it easier to get and manage accounts."""
_kmd: KMDClient | None
def __init__(self, client_manager: ClientManager) -> None:
"""Create a new KMD manager.
Args:
client_manager: ClientManager to use for account management
"""
self._client_manager = client_manager
try:
self._kmd = client_manager.kmd
except ValueError:
self._kmd = None
def kmd(self) -> KMDClient:
"""Get the KMD client, initializing it if needed.
Returns:
KMDClient: The initialized KMD client
Raises:
Exception: If KMD is not configured
"""
if self._kmd is None:
if self._client_manager.is_local_net():
kmd_config = ClientManager.get_config_from_environment_or_localnet()
self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config)
return self._kmd
raise Exception("Attempt to use KMD client with no KMD configured")
return self._kmd
def get_wallet_account(
self,
wallet_name: str,
predicate: Callable[[dict[str, Any]], bool] | None = None,
sender: str | None = None,
) -> KmdAccount | None:
"""Returns an Algorand signing account with private key loaded from the given KMD wallet.
Args:
wallet_name: The name of the wallet to retrieve an account from
predicate: Optional filter to use to find the account (otherwise returns a random account from the wallet)
sender: Optional sender address to use this signer for (aka a rekeyed account)
Returns:
Optional[KmdAccount]: The signing account or None if no matching wallet or account was found
Example:
```python
# Get default funded account in a LocalNet
default_dispenser = kmd_manager.get_wallet_account(
"unencrypted-default-wallet",
lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000
)
```
"""
kmd_client = self.kmd()
wallets = kmd_client.list_wallets()
wallet = next((w for w in wallets if w["name"] == wallet_name), None)
if not wallet:
return None
wallet_id = wallet["id"]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
addresses = kmd_client.list_keys(wallet_handle)
matched_address = None
if predicate:
for address in addresses:
account_info = self._client_manager.algod.account_info(address)
if predicate(cast(dict[str, Any], account_info)):
matched_address = address
break
else:
matched_address = next(iter(addresses), None)
if not matched_address:
return None
private_key = kmd_client.export_key(wallet_handle, "", matched_address)
return KmdAccount(private_key=private_key, address=sender)
def get_or_create_wallet_account(self, name: str, fund_with: AlgoAmount | None = None) -> KmdAccount:
"""Gets or creates a funded account in a KMD wallet of the given name.
This is useful to get idempotent accounts from LocalNet without having to specify the private key
(which will change when resetting the LocalNet).
Args:
name: The name of the wallet to retrieve / create
fund_with: The number of Algos to fund the account with when created (default: 1000)
Returns:
KmdAccount: An Algorand account with private key loaded
Example:
```python
# Idempotently get (if exists) or create (if doesn't exist) an account by name using KMD
# if creating it then fund it with 2 ALGO from the default dispenser account
new_account = kmd_manager.get_or_create_wallet_account("account1", 2)
# This will return the same account as above since the name matches
existing_account = kmd_manager.get_or_create_wallet_account("account1")
```
"""
existing = self.get_wallet_account(name)
if existing:
return existing
kmd_client = self.kmd()
wallet_id = kmd_client.create_wallet(name, "")["id"]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
kmd_client.generate_key(wallet_handle)
account = self.get_wallet_account(name)
assert account is not None
logger.info(
f"LocalNet account '{name}' doesn't yet exist; created account {account.address} "
f"with keys stored in KMD and funding with {fund_with} ALGO"
)
dispenser = self.get_localnet_dispenser_account()
TransactionComposer(
algod=self._client_manager.algod,
get_signer=lambda _: dispenser.signer,
get_suggested_params=self._client_manager.algod.suggested_params,
).add_payment(
PaymentParams(
sender=dispenser.address,
receiver=account.address,
amount=fund_with or AlgoAmount.from_algo(1000),
)
).send()
return account
def get_localnet_dispenser_account(self) -> KmdAccount:
"""Returns an Algorand account with private key loaded for the default LocalNet dispenser account.
Returns:
KmdAccount: The default LocalNet dispenser account
Raises:
Exception: If not running against LocalNet or dispenser account not found
Example:
```python
dispenser = kmd_manager.get_localnet_dispenser_account()
```
"""
if not self._client_manager.is_local_net():
raise Exception("Can't get LocalNet dispenser account from non LocalNet network")
dispenser = self.get_wallet_account(
"unencrypted-default-wallet",
lambda a: a["status"] != "Offline" and a["amount"] > 1_000_000_000, # noqa: PLR2004
)
if not dispenser:
raise Exception("Error retrieving LocalNet dispenser account; couldn't find the default account in KMD")
return dispenser