-
Notifications
You must be signed in to change notification settings - Fork 11
/
account.py
183 lines (140 loc) · 6.9 KB
/
account.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
import logging
import os
from typing import TYPE_CHECKING, Any
from algosdk.account import address_from_private_key
from algosdk.mnemonic import from_private_key, to_private_key
from algosdk.util import algos_to_microalgos
from algokit_utils._transfer import TransferParameters, transfer
from algokit_utils.models import Account
from algokit_utils.network_clients import get_kmd_client_from_algod_client, is_localnet
if TYPE_CHECKING:
from collections.abc import Callable
from algosdk.kmd import KMDClient
from algosdk.v2client.algod import AlgodClient
__all__ = [
"create_kmd_wallet_account",
"get_account",
"get_account_from_mnemonic",
"get_dispenser_account",
"get_kmd_wallet_account",
"get_localnet_default_account",
"get_or_create_kmd_wallet_account",
]
logger = logging.getLogger(__name__)
_DEFAULT_ACCOUNT_MINIMUM_BALANCE = 1_000_000_000
def get_account_from_mnemonic(mnemonic: str) -> Account:
"""Convert a mnemonic (25 word passphrase) into an Account"""
private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call]
address = address_from_private_key(private_key) # type: ignore[no-untyped-call]
return Account(private_key=private_key, address=address)
def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account:
"""Creates a wallet with specified name"""
wallet_id = kmd_client.create_wallet(name, "")["id"]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
kmd_client.generate_key(wallet_handle)
key_ids: list[str] = kmd_client.list_keys(wallet_handle)
account_key = key_ids[0]
private_account_key = kmd_client.export_key(wallet_handle, "", account_key)
return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call]
def get_or_create_kmd_wallet_account(
client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None
) -> Account:
"""Returns a wallet with specified name, or creates one if not found"""
kmd_client = kmd_client or get_kmd_client_from_algod_client(client)
account = get_kmd_wallet_account(client, kmd_client, name)
if account:
account_info = client.account_info(account.address)
assert isinstance(account_info, dict)
if account_info["amount"] > 0:
return account
logger.debug(f"Found existing account in LocalNet with name '{name}', but no funds in the account.")
else:
account = create_kmd_wallet_account(kmd_client, name)
logger.debug(
f"Couldn't find existing account in LocalNet with name '{name}'. "
f"So created account {account.address} with keys stored in KMD."
)
logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs")
if fund_with_algos:
transfer(
client,
TransferParameters(
from_account=get_dispenser_account(client),
to_address=account.address,
micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call]
),
)
return account
def _is_default_account(account: dict[str, Any]) -> bool:
return bool(account["status"] != "Offline" and account["amount"] > _DEFAULT_ACCOUNT_MINIMUM_BALANCE)
def get_localnet_default_account(client: "AlgodClient") -> Account:
"""Returns the default Account in a LocalNet instance"""
if not is_localnet(client):
raise Exception("Can't get a default account from non LocalNet network")
account = get_kmd_wallet_account(
client, get_kmd_client_from_algod_client(client), "unencrypted-default-wallet", _is_default_account
)
assert account
return account
def get_dispenser_account(client: "AlgodClient") -> Account:
"""Returns an Account based on DISPENSER_MNENOMIC environment variable or the default account on LocalNet"""
if is_localnet(client):
return get_localnet_default_account(client)
return get_account(client, "DISPENSER")
def get_kmd_wallet_account(
client: "AlgodClient",
kmd_client: "KMDClient",
name: str,
predicate: "Callable[[dict[str, Any]], bool] | None" = None,
) -> Account | None:
"""Returns wallet matching specified name and predicate or None if not found"""
wallets: list[dict] = kmd_client.list_wallets()
wallet = next((w for w in wallets if w["name"] == name), None)
if wallet is None:
return None
wallet_id = wallet["id"]
wallet_handle = kmd_client.init_wallet_handle(wallet_id, "")
key_ids: list[str] = kmd_client.list_keys(wallet_handle)
matched_account_key = None
if predicate:
for key in key_ids:
account = client.account_info(key)
assert isinstance(account, dict)
if predicate(account):
matched_account_key = key
else:
matched_account_key = next(key_ids.__iter__(), None)
if not matched_account_key:
return None
private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key)
return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call]
def get_account(
client: "AlgodClient", name: str, fund_with_algos: float = 1000, kmd_client: "KMDClient | None" = None
) -> Account:
"""Returns an Algorand account with private key loaded by convention based on the given name identifier.
# Convention
**Non-LocalNet:** will load `os.environ[f"{name}_MNEMONIC"]` as a mnemonic secret
Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a
secret storage service rather than the file system.
**LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will
create it and fund the account for you
This allows you to write code that will work seamlessly in production and local development (LocalNet) without
manual config locally (including when you reset the LocalNet).
# Example
If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call the following to get
that private key loaded into an account object:
```python
account = get_account('ACCOUNT', algod)
```
If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account
that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser.
"""
mnemonic_key = f"{name.upper()}_MNEMONIC"
mnemonic = os.getenv(mnemonic_key)
if mnemonic:
return get_account_from_mnemonic(mnemonic)
if is_localnet(client):
account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client)
os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call]
return account
raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'")